1e13af4d6e
Modified - Updated the example app.ini documentation for the new administrator management policies.
784 lines
23 KiB
Go
784 lines
23 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"
|
|
|
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
|
"code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/db"
|
|
org_model "code.gitea.io/gitea/models/organization"
|
|
packages_model "code.gitea.io/gitea/models/packages"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/auth/password"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/optional"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
api "code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"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/context"
|
|
"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) {
|
|
if sourceID == 0 {
|
|
return
|
|
}
|
|
|
|
source, err := auth.GetSourceByID(ctx, sourceID)
|
|
if err != nil {
|
|
if auth.IsErrSourceNotExist(err) {
|
|
ctx.APIError(http.StatusUnprocessableEntity, err)
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
u.LoginType = source.Type
|
|
u.LoginSource = source.ID
|
|
}
|
|
|
|
func isSuperAdmin(ctx *context.APIContext, userID int64) bool {
|
|
if !setting.Admin.SuperAdminEnabled {
|
|
return false
|
|
}
|
|
value, err := user_model.GetUserSetting(ctx, userID, user_model.SettingsKeySuperAdminEnabled)
|
|
if err != nil {
|
|
if !user_model.IsErrUserSettingIsNotExist(err) {
|
|
log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeySuperAdminEnabled, userID, err)
|
|
}
|
|
return false
|
|
}
|
|
return value == "true"
|
|
}
|
|
|
|
func isBootstrapProtectedUser(ctx *context.APIContext, userID int64) bool {
|
|
protected, err := user_model.HasBootstrapAdminGrant(ctx, userID)
|
|
if err != nil {
|
|
log.Error("HasBootstrapAdminGrant for user %d: %v", userID, err)
|
|
return false
|
|
}
|
|
return protected
|
|
}
|
|
|
|
func countSuperAdmins(ctx *context.APIContext) int {
|
|
settings := make([]*user_model.Setting, 0, 5)
|
|
if err := db.GetEngine(ctx).Where("setting_key=? AND setting_value=?", user_model.SettingsKeySuperAdminEnabled, "true").Find(&settings); err != nil {
|
|
log.Error("Find super admin settings: %v", err)
|
|
return 0
|
|
}
|
|
count := 0
|
|
for _, superAdminSetting := range settings {
|
|
u, err := user_model.GetUserByID(ctx, superAdminSetting.UserID)
|
|
if err != nil {
|
|
if !user_model.IsErrUserNotExist(err) {
|
|
log.Error("Get super admin user %d: %v", superAdminSetting.UserID, err)
|
|
}
|
|
continue
|
|
}
|
|
if u.IsAdmin && u.IsActive {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func canManageAdminUser(ctx *context.APIContext, doer, target *user_model.User) (bool, error) {
|
|
if !target.IsAdmin || doer.ID == target.ID {
|
|
return true, nil
|
|
}
|
|
if isSuperAdmin(ctx, doer.ID) {
|
|
return true, nil
|
|
}
|
|
if !doer.IsAdmin {
|
|
return false, nil
|
|
}
|
|
|
|
switch setting.Admin.AdminManagementPolicy {
|
|
case setting.AdminManagementPolicyGrantorOnly:
|
|
grantorID, has, err := user_model.GetAdminGrantorID(ctx, target.ID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return has && grantorID == doer.ID, nil
|
|
case setting.AdminManagementPolicyGrantorInheritance:
|
|
grantorID, has, err := user_model.GetEffectiveAdminGrantorID(ctx, target.ID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return has && grantorID == doer.ID, nil
|
|
default:
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func canManageAdminStatus(ctx *context.APIContext, doer, target *user_model.User, targetRequestedAdmin bool, targetIsSuperAdmin bool) (bool, error) {
|
|
targetWasAdmin := target.IsAdmin
|
|
if !setting.Admin.SuperAdminEnabled || targetWasAdmin == targetRequestedAdmin {
|
|
return true, nil
|
|
}
|
|
if isSuperAdmin(ctx, doer.ID) {
|
|
return true, nil
|
|
}
|
|
if !doer.IsAdmin || targetIsSuperAdmin {
|
|
return false, nil
|
|
}
|
|
switch setting.Admin.AdminManagementPolicy {
|
|
case setting.AdminManagementPolicyGrantorOnly, setting.AdminManagementPolicyGrantorInheritance:
|
|
if !targetWasAdmin && targetRequestedAdmin {
|
|
return true, nil
|
|
}
|
|
return canManageAdminUser(ctx, doer, target)
|
|
default:
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func canDeleteUserFromAdminAPI(ctx *context.APIContext, doer, target *user_model.User) (bool, error) {
|
|
if target.ID == doer.ID {
|
|
return false, errors.New("you cannot delete yourself")
|
|
}
|
|
if isBootstrapProtectedUser(ctx, target.ID) {
|
|
return false, errors.New("GOOD-granted accounts cannot be modified or deleted")
|
|
}
|
|
if target.IsAdmin {
|
|
canManage, err := canManageAdminUser(ctx, doer, target)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !canManage {
|
|
return false, errors.New("you can only manage administrator accounts that belong to your grant chain")
|
|
}
|
|
}
|
|
isCreator, err := user_model.IsGrantorOf(ctx, doer.ID, target.ID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if isCreator {
|
|
return false, errors.New("you cannot delete the admin who granted your privileges")
|
|
}
|
|
targetIsSuperAdmin := isSuperAdmin(ctx, target.ID)
|
|
if targetIsSuperAdmin && countSuperAdmins(ctx) <= 1 {
|
|
return false, errors.New("cannot delete the last super admin")
|
|
}
|
|
if targetIsSuperAdmin && !isSuperAdmin(ctx, doer.ID) {
|
|
return false, errors.New("super admin required")
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// 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,
|
|
LoginName: form.LoginName,
|
|
}
|
|
if form.MustChangePassword != nil {
|
|
u.MustChangePassword = *form.MustChangePassword
|
|
}
|
|
|
|
parseAuthSource(ctx, u, form.SourceID)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
if u.LoginType == auth.Plain {
|
|
if len(form.Password) < setting.MinPasswordLength {
|
|
err := errors.New("PasswordIsRequired")
|
|
ctx.APIError(http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
if !password.IsComplexEnough(form.Password) {
|
|
err := errors.New("PasswordComplexity")
|
|
ctx.APIError(http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
if err := password.IsPwned(ctx, form.Password); err != nil {
|
|
if password.IsErrIsPwnedRequest(err) {
|
|
log.Error(err.Error())
|
|
}
|
|
ctx.APIError(http.StatusBadRequest, errors.New("PasswordPwned"))
|
|
return
|
|
}
|
|
}
|
|
|
|
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
|
IsActive: optional.Some(true),
|
|
IsRestricted: optional.FromPtr(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.AdminCreateUser(ctx, u, &user_model.Meta{}, 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.APIError(http.StatusUnprocessableEntity, err)
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !user_model.IsEmailDomainAllowed(u.Email) {
|
|
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
|
|
}
|
|
|
|
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 the user whose data is to be edited
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/EditUserOption"
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/User"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
form := web.GetForm(ctx).(*api.EditUserOption)
|
|
if isBootstrapProtectedUser(ctx, ctx.ContextUser.ID) && ctx.ContextUser.ID != ctx.Doer.ID {
|
|
ctx.APIError(http.StatusForbidden, errors.New("GOOD-granted accounts cannot be modified or deleted"))
|
|
return
|
|
}
|
|
if isSuperAdmin(ctx, ctx.ContextUser.ID) && !isSuperAdmin(ctx, ctx.Doer.ID) {
|
|
ctx.APIError(http.StatusForbidden, errors.New("super admin required"))
|
|
return
|
|
}
|
|
canManageTargetAdmin, err := canManageAdminUser(ctx, ctx.Doer, ctx.ContextUser)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
if !canManageTargetAdmin {
|
|
ctx.APIError(http.StatusForbidden, errors.New("you can only manage administrator accounts that belong to your grant chain"))
|
|
return
|
|
}
|
|
|
|
wasAdmin := ctx.ContextUser.IsAdmin
|
|
wasActive := ctx.ContextUser.IsActive
|
|
wasRestricted := ctx.ContextUser.IsRestricted
|
|
wasProhibitLogin := ctx.ContextUser.ProhibitLogin
|
|
wasSuperAdmin := isSuperAdmin(ctx, ctx.ContextUser.ID)
|
|
|
|
requestedAdmin := wasAdmin
|
|
if form.Admin != nil {
|
|
requestedAdmin = *form.Admin
|
|
}
|
|
requestedActive := wasActive
|
|
if form.Active != nil {
|
|
requestedActive = *form.Active
|
|
}
|
|
requestedRestricted := wasRestricted
|
|
if form.Restricted != nil {
|
|
requestedRestricted = *form.Restricted
|
|
}
|
|
requestedProhibitLogin := wasProhibitLogin
|
|
if form.ProhibitLogin != nil {
|
|
requestedProhibitLogin = *form.ProhibitLogin
|
|
}
|
|
|
|
if ctx.Doer.ID == ctx.ContextUser.ID {
|
|
requestedAdmin = wasAdmin
|
|
requestedActive = wasActive
|
|
requestedRestricted = wasRestricted
|
|
requestedProhibitLogin = wasProhibitLogin
|
|
}
|
|
|
|
canManageRequestedAdmin, err := canManageAdminStatus(ctx, ctx.Doer, ctx.ContextUser, requestedAdmin, wasSuperAdmin)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
if !canManageRequestedAdmin {
|
|
requestedAdmin = wasAdmin
|
|
}
|
|
|
|
authOpts := &user_service.UpdateAuthOptions{
|
|
LoginSource: optional.FromNonDefault(form.SourceID),
|
|
LoginName: optional.Some(form.LoginName),
|
|
Password: optional.FromNonDefault(form.Password),
|
|
MustChangePassword: optional.FromPtr(form.MustChangePassword),
|
|
ProhibitLogin: optional.Some(requestedProhibitLogin),
|
|
}
|
|
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
|
|
switch {
|
|
case errors.Is(err, password.ErrMinLength):
|
|
ctx.APIError(http.StatusBadRequest, fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
|
|
case errors.Is(err, password.ErrComplexity):
|
|
ctx.APIError(http.StatusBadRequest, err)
|
|
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
|
|
ctx.APIError(http.StatusBadRequest, err)
|
|
default:
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if form.Email != nil {
|
|
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
|
|
switch {
|
|
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
|
|
if !user_model.IsEmailDomainAllowed(*form.Email) {
|
|
err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)
|
|
}
|
|
ctx.APIError(http.StatusBadRequest, err)
|
|
case user_model.IsErrEmailAlreadyUsed(err):
|
|
ctx.APIError(http.StatusBadRequest, err)
|
|
default:
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
opts := &user_service.UpdateOptions{
|
|
FullName: optional.FromPtr(form.FullName),
|
|
Website: optional.FromPtr(form.Website),
|
|
Location: optional.FromPtr(form.Location),
|
|
Description: optional.FromPtr(form.Description),
|
|
IsActive: optional.Some(requestedActive),
|
|
IsAdmin: user_service.UpdateOptionFieldFromValue(requestedAdmin),
|
|
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
|
|
AllowGitHook: optional.FromPtr(form.AllowGitHook),
|
|
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
|
|
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
|
|
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
|
|
IsRestricted: optional.Some(requestedRestricted),
|
|
}
|
|
|
|
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
|
|
if user_model.IsErrDeleteLastAdminUser(err) {
|
|
ctx.APIError(http.StatusBadRequest, err)
|
|
} else {
|
|
ctx.APIErrorInternal(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 the user to delete
|
|
// type: string
|
|
// required: true
|
|
// - name: purge
|
|
// in: query
|
|
// description: purge the user from the system completely
|
|
// type: boolean
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
if ctx.ContextUser.IsOrganization() {
|
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
|
|
return
|
|
}
|
|
|
|
canDelete, deleteErr := canDeleteUserFromAdminAPI(ctx, ctx.Doer, ctx.ContextUser)
|
|
if !canDelete {
|
|
ctx.APIError(http.StatusForbidden, deleteErr)
|
|
return
|
|
}
|
|
|
|
enableNewAccountRequestNotificationsFallback, err := user_model.ShouldEnableNewAccountRequestNotificationsFallback(ctx, ctx.Doer, ctx.ContextUser)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
|
|
if repo_model.IsErrUserOwnRepos(err) ||
|
|
org_model.IsErrUserHasOrgs(err) ||
|
|
packages_model.IsErrUserOwnPackages(err) ||
|
|
user_model.IsErrDeleteLastAdminUser(err) {
|
|
ctx.APIError(http.StatusUnprocessableEntity, err)
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
if enableNewAccountRequestNotificationsFallback {
|
|
if err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyEmailNotificationNewAccountRequests, user_model.SettingEmailNotificationNewAccountRequestsEnabled); err != nil {
|
|
ctx.APIErrorInternal(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 who is to receive a public key
|
|
// 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 the user whose public key is to be deleted
|
|
// 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, ctx.ContextUser, ctx.PathParamInt64("id")); err != nil {
|
|
if asymkey_model.IsErrKeyNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else if asymkey_model.IsErrKeyAccessDenied(err) {
|
|
ctx.APIError(http.StatusForbidden, "You do not have access to this key")
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
log.Trace("Key deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// SearchUsers API for getting information of the users according the filter conditions
|
|
func SearchUsers(ctx *context.APIContext) {
|
|
// swagger:operation GET /admin/users admin adminSearchUsers
|
|
// ---
|
|
// summary: Search users according filter conditions
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: source_id
|
|
// in: query
|
|
// description: ID of the user's login source to search for
|
|
// type: integer
|
|
// format: int64
|
|
// - name: login_name
|
|
// in: query
|
|
// description: identifier of the user, provided by the external authenticator
|
|
// type: string
|
|
// - 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
|
|
// - name: sort
|
|
// in: query
|
|
// description: sort users by attribute. Supported values are
|
|
// "name", "created", "updated" and "id".
|
|
// Default is "name"
|
|
// type: string
|
|
// - name: order
|
|
// in: query
|
|
// description: sort order, either "asc" (ascending) or "desc" (descending).
|
|
// Default is "asc", ignored if "sort" is not specified.
|
|
// type: string
|
|
// - name: q
|
|
// in: query
|
|
// description: search term (username, full name, email)
|
|
// type: string
|
|
// - name: visibility
|
|
// in: query
|
|
// description: visibility filter. Supported values are
|
|
// "public", "limited" and "private".
|
|
// type: string
|
|
// - name: is_active
|
|
// in: query
|
|
// description: filter active users
|
|
// type: boolean
|
|
// - name: is_admin
|
|
// in: query
|
|
// description: filter admin users
|
|
// type: boolean
|
|
// - name: is_restricted
|
|
// in: query
|
|
// description: filter restricted users
|
|
// type: boolean
|
|
// - name: is_2fa_enabled
|
|
// in: query
|
|
// description: filter 2FA enabled users
|
|
// type: boolean
|
|
// - name: is_prohibit_login
|
|
// in: query
|
|
// description: filter login prohibited users
|
|
// type: boolean
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/UserList"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
listOptions := utils.GetListOptions(ctx)
|
|
|
|
orderBy := db.SearchOrderByAlphabetically
|
|
sortMode := ctx.FormString("sort")
|
|
if len(sortMode) > 0 {
|
|
sortOrder := ctx.FormString("order")
|
|
if len(sortOrder) == 0 {
|
|
sortOrder = "asc"
|
|
}
|
|
if searchModeMap, ok := user_model.AdminUserOrderByMap[sortOrder]; ok {
|
|
if order, ok := searchModeMap[sortMode]; ok {
|
|
orderBy = order
|
|
} else {
|
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
|
|
return
|
|
}
|
|
} else {
|
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
|
|
return
|
|
}
|
|
}
|
|
|
|
var visible []api.VisibleType
|
|
visibilityParam := ctx.FormString("visibility")
|
|
if len(visibilityParam) > 0 {
|
|
if visibility, ok := api.VisibilityModes[visibilityParam]; ok {
|
|
visible = []api.VisibleType{visibility}
|
|
} else {
|
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam))
|
|
return
|
|
}
|
|
}
|
|
|
|
searchOpts := user_model.SearchUserOptions{
|
|
Actor: ctx.Doer,
|
|
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
|
LoginName: ctx.FormTrim("login_name"),
|
|
SourceID: ctx.FormInt64("source_id"),
|
|
Keyword: ctx.FormTrim("q"),
|
|
Visible: visible,
|
|
OrderBy: orderBy,
|
|
ListOptions: listOptions,
|
|
SearchByEmail: true,
|
|
}
|
|
|
|
if ctx.FormString("is_active") != "" {
|
|
searchOpts.IsActive = optional.Some(ctx.FormBool("is_active"))
|
|
}
|
|
if ctx.FormString("is_admin") != "" {
|
|
searchOpts.IsAdmin = optional.Some(ctx.FormBool("is_admin"))
|
|
}
|
|
if ctx.FormString("is_restricted") != "" {
|
|
searchOpts.IsRestricted = optional.Some(ctx.FormBool("is_restricted"))
|
|
}
|
|
if ctx.FormString("is_2fa_enabled") != "" {
|
|
searchOpts.IsTwoFactorEnabled = optional.Some(ctx.FormBool("is_2fa_enabled"))
|
|
}
|
|
if ctx.FormString("is_prohibit_login") != "" {
|
|
searchOpts.IsProhibitLogin = optional.Some(ctx.FormBool("is_prohibit_login"))
|
|
}
|
|
|
|
users, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
results := make([]*api.User, len(users))
|
|
for i := range users {
|
|
results[i] = convert.ToUser(ctx, users[i], ctx.Doer)
|
|
}
|
|
|
|
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
|
|
ctx.SetTotalCountHeader(maxResults)
|
|
ctx.JSON(http.StatusOK, &results)
|
|
}
|
|
|
|
// RenameUser api for renaming a user
|
|
func RenameUser(ctx *context.APIContext) {
|
|
// swagger:operation POST /admin/users/{username}/rename admin adminRenameUser
|
|
// ---
|
|
// summary: Rename a user
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: username
|
|
// in: path
|
|
// description: current username of the user
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// required: true
|
|
// schema:
|
|
// "$ref": "#/definitions/RenameUserOption"
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
if ctx.ContextUser.IsOrganization() {
|
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
|
|
return
|
|
}
|
|
|
|
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
|
|
|
|
// Check if username has been changed
|
|
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName, ctx.Doer); err != nil {
|
|
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
|
|
ctx.APIError(http.StatusUnprocessableEntity, err)
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|