aa45777c92
Allow back-dating user creation via the `adminCreateUser` API operation. `CreateUserOption` now has an optional field `created_at`, which can contain a datetime-formatted string. If this field is present, the user's `created_unix` database field will be updated to its value. This is important for Blender's migration of users from Phabricator to Gitea. There are many users, and the creation timestamp of their account can give us some indication as to how long someone's been part of the community. The back-dating is done in a separate query that just updates the user's `created_unix` field. This was the easiest and cleanest way I could find, as in the initial `INSERT` query the field always is set to "now".
460 lines
13 KiB
Go
460 lines
13 KiB
Go
// Copyright 2015 The Gogs Authors. All rights reserved.
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package admin
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models"
|
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
|
"code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/db"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/context"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/password"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
api "code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/util"
|
|
"code.gitea.io/gitea/modules/web"
|
|
"code.gitea.io/gitea/routers/api/v1/user"
|
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
|
"code.gitea.io/gitea/services/convert"
|
|
"code.gitea.io/gitea/services/mailer"
|
|
user_service "code.gitea.io/gitea/services/user"
|
|
)
|
|
|
|
func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64, loginName string) {
|
|
if sourceID == 0 {
|
|
return
|
|
}
|
|
|
|
source, err := auth.GetSourceByID(sourceID)
|
|
if err != nil {
|
|
if auth.IsErrSourceNotExist(err) {
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "auth.GetSourceByID", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
u.LoginType = source.Type
|
|
u.LoginSource = source.ID
|
|
u.LoginName = loginName
|
|
}
|
|
|
|
// CreateUser create a user
|
|
func CreateUser(ctx *context.APIContext) {
|
|
// swagger:operation POST /admin/users admin adminCreateUser
|
|
// ---
|
|
// summary: Create a user
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/CreateUserOption"
|
|
// responses:
|
|
// "201":
|
|
// "$ref": "#/responses/User"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
form := web.GetForm(ctx).(*api.CreateUserOption)
|
|
|
|
u := &user_model.User{
|
|
Name: form.Username,
|
|
FullName: form.FullName,
|
|
Email: form.Email,
|
|
Passwd: form.Password,
|
|
MustChangePassword: true,
|
|
LoginType: auth.Plain,
|
|
}
|
|
if form.MustChangePassword != nil {
|
|
u.MustChangePassword = *form.MustChangePassword
|
|
}
|
|
|
|
parseAuthSource(ctx, u, form.SourceID, form.LoginName)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
if !password.IsComplexEnough(form.Password) {
|
|
err := errors.New("PasswordComplexity")
|
|
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
|
return
|
|
}
|
|
pwned, err := password.IsPwned(ctx, form.Password)
|
|
if pwned {
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
}
|
|
ctx.Data["Err_Password"] = true
|
|
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
|
return
|
|
}
|
|
|
|
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
|
IsActive: util.OptionalBoolTrue,
|
|
}
|
|
|
|
if form.Restricted != nil {
|
|
overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
|
|
}
|
|
|
|
if form.Visibility != "" {
|
|
visibility := api.VisibilityModes[form.Visibility]
|
|
overwriteDefault.Visibility = &visibility
|
|
}
|
|
|
|
// Update the user creation timestamp. This can only be done after the user
|
|
// record has been inserted into the database; the insert intself will always
|
|
// set the creation timestamp to "now".
|
|
if form.Created != nil {
|
|
u.CreatedUnix = timeutil.TimeStamp(form.Created.Unix())
|
|
u.UpdatedUnix = u.CreatedUnix
|
|
}
|
|
|
|
if err := user_model.CreateUser(u, overwriteDefault); err != nil {
|
|
if user_model.IsErrUserAlreadyExist(err) ||
|
|
user_model.IsErrEmailAlreadyUsed(err) ||
|
|
db.IsErrNameReserved(err) ||
|
|
db.IsErrNameCharsNotAllowed(err) ||
|
|
user_model.IsErrEmailCharIsNotSupported(err) ||
|
|
user_model.IsErrEmailInvalid(err) ||
|
|
db.IsErrNamePatternNotAllowed(err) {
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "CreateUser", err)
|
|
}
|
|
return
|
|
}
|
|
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
|
|
|
|
// Send email notification.
|
|
if form.SendNotify {
|
|
mailer.SendRegisterNotifyMail(u)
|
|
}
|
|
ctx.JSON(http.StatusCreated, convert.ToUser(ctx, u, ctx.Doer))
|
|
}
|
|
|
|
// EditUser api for modifying a user's information
|
|
func EditUser(ctx *context.APIContext) {
|
|
// swagger:operation PATCH /admin/users/{username} admin adminEditUser
|
|
// ---
|
|
// summary: Edit an existing user
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: username
|
|
// in: path
|
|
// description: username of user to edit
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/EditUserOption"
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/User"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
form := web.GetForm(ctx).(*api.EditUserOption)
|
|
|
|
parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
if len(form.Password) != 0 {
|
|
if len(form.Password) < setting.MinPasswordLength {
|
|
ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
|
|
return
|
|
}
|
|
if !password.IsComplexEnough(form.Password) {
|
|
err := errors.New("PasswordComplexity")
|
|
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
|
return
|
|
}
|
|
pwned, err := password.IsPwned(ctx, form.Password)
|
|
if pwned {
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
}
|
|
ctx.Data["Err_Password"] = true
|
|
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
|
return
|
|
}
|
|
if ctx.ContextUser.Salt, err = user_model.GetUserSalt(); err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
|
return
|
|
}
|
|
if err = ctx.ContextUser.SetPassword(form.Password); err != nil {
|
|
ctx.InternalServerError(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if form.MustChangePassword != nil {
|
|
ctx.ContextUser.MustChangePassword = *form.MustChangePassword
|
|
}
|
|
|
|
ctx.ContextUser.LoginName = form.LoginName
|
|
|
|
if form.FullName != nil {
|
|
ctx.ContextUser.FullName = *form.FullName
|
|
}
|
|
var emailChanged bool
|
|
if form.Email != nil {
|
|
email := strings.TrimSpace(*form.Email)
|
|
if len(email) == 0 {
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("email is not allowed to be empty string"))
|
|
return
|
|
}
|
|
|
|
if err := user_model.ValidateEmail(email); err != nil {
|
|
ctx.InternalServerError(err)
|
|
return
|
|
}
|
|
|
|
emailChanged = !strings.EqualFold(ctx.ContextUser.Email, email)
|
|
ctx.ContextUser.Email = email
|
|
}
|
|
if form.Website != nil {
|
|
ctx.ContextUser.Website = *form.Website
|
|
}
|
|
if form.Location != nil {
|
|
ctx.ContextUser.Location = *form.Location
|
|
}
|
|
if form.Description != nil {
|
|
ctx.ContextUser.Description = *form.Description
|
|
}
|
|
if form.Active != nil {
|
|
ctx.ContextUser.IsActive = *form.Active
|
|
}
|
|
if len(form.Visibility) != 0 {
|
|
ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility]
|
|
}
|
|
if form.Admin != nil {
|
|
ctx.ContextUser.IsAdmin = *form.Admin
|
|
}
|
|
if form.AllowGitHook != nil {
|
|
ctx.ContextUser.AllowGitHook = *form.AllowGitHook
|
|
}
|
|
if form.AllowImportLocal != nil {
|
|
ctx.ContextUser.AllowImportLocal = *form.AllowImportLocal
|
|
}
|
|
if form.MaxRepoCreation != nil {
|
|
ctx.ContextUser.MaxRepoCreation = *form.MaxRepoCreation
|
|
}
|
|
if form.AllowCreateOrganization != nil {
|
|
ctx.ContextUser.AllowCreateOrganization = *form.AllowCreateOrganization
|
|
}
|
|
if form.ProhibitLogin != nil {
|
|
ctx.ContextUser.ProhibitLogin = *form.ProhibitLogin
|
|
}
|
|
if form.Restricted != nil {
|
|
ctx.ContextUser.IsRestricted = *form.Restricted
|
|
}
|
|
|
|
if err := user_model.UpdateUser(ctx, ctx.ContextUser, emailChanged); err != nil {
|
|
if user_model.IsErrEmailAlreadyUsed(err) ||
|
|
user_model.IsErrEmailCharIsNotSupported(err) ||
|
|
user_model.IsErrEmailInvalid(err) {
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
|
}
|
|
return
|
|
}
|
|
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer))
|
|
}
|
|
|
|
// DeleteUser api for deleting a user
|
|
func DeleteUser(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /admin/users/{username} admin adminDeleteUser
|
|
// ---
|
|
// summary: Delete a user
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: username
|
|
// in: path
|
|
// description: username of user to delete
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
if ctx.ContextUser.IsOrganization() {
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
|
|
return
|
|
}
|
|
|
|
// admin should not delete themself
|
|
if ctx.ContextUser.ID == ctx.Doer.ID {
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("you cannot delete yourself"))
|
|
return
|
|
}
|
|
|
|
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
|
|
if models.IsErrUserOwnRepos(err) ||
|
|
models.IsErrUserHasOrgs(err) ||
|
|
models.IsErrUserOwnPackages(err) {
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "DeleteUser", err)
|
|
}
|
|
return
|
|
}
|
|
log.Trace("Account deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// CreatePublicKey api for creating a public key to a user
|
|
func CreatePublicKey(ctx *context.APIContext) {
|
|
// swagger:operation POST /admin/users/{username}/keys admin adminCreatePublicKey
|
|
// ---
|
|
// summary: Add a public key on behalf of a user
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: username
|
|
// in: path
|
|
// description: username of the user
|
|
// type: string
|
|
// required: true
|
|
// - name: key
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/CreateKeyOption"
|
|
// responses:
|
|
// "201":
|
|
// "$ref": "#/responses/PublicKey"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
form := web.GetForm(ctx).(*api.CreateKeyOption)
|
|
|
|
user.CreateUserPublicKey(ctx, *form, ctx.ContextUser.ID)
|
|
}
|
|
|
|
// DeleteUserPublicKey api for deleting a user's public key
|
|
func DeleteUserPublicKey(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /admin/users/{username}/keys/{id} admin adminDeleteUserPublicKey
|
|
// ---
|
|
// summary: Delete a user's public key
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: username
|
|
// in: path
|
|
// description: username of user
|
|
// type: string
|
|
// required: true
|
|
// - name: id
|
|
// in: path
|
|
// description: id of the key to delete
|
|
// type: integer
|
|
// format: int64
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
if err := asymkey_service.DeletePublicKey(ctx.ContextUser, ctx.ParamsInt64(":id")); err != nil {
|
|
if asymkey_model.IsErrKeyNotExist(err) {
|
|
ctx.NotFound()
|
|
} else if asymkey_model.IsErrKeyAccessDenied(err) {
|
|
ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "DeleteUserPublicKey", err)
|
|
}
|
|
return
|
|
}
|
|
log.Trace("Key deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// GetAllUsers API for getting information of all the users
|
|
func GetAllUsers(ctx *context.APIContext) {
|
|
// swagger:operation GET /admin/users admin adminGetAllUsers
|
|
// ---
|
|
// summary: List all users
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: page
|
|
// in: query
|
|
// description: page number of results to return (1-based)
|
|
// type: integer
|
|
// - name: limit
|
|
// in: query
|
|
// description: page size of results
|
|
// type: integer
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/UserList"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
listOptions := utils.GetListOptions(ctx)
|
|
|
|
users, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{
|
|
Actor: ctx.Doer,
|
|
Type: user_model.UserTypeIndividual,
|
|
OrderBy: db.SearchOrderByAlphabetically,
|
|
ListOptions: listOptions,
|
|
})
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "GetAllUsers", err)
|
|
return
|
|
}
|
|
|
|
results := make([]*api.User, len(users))
|
|
for i := range users {
|
|
results[i] = convert.ToUser(ctx, users[i], ctx.Doer)
|
|
}
|
|
|
|
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
|
|
ctx.SetTotalCountHeader(maxResults)
|
|
ctx.JSON(http.StatusOK, &results)
|
|
}
|