code.gitea.io/gitea@v1.21.7/routers/api/v1/admin/user.go (about)

     1  // Copyright 2015 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package admin
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  	"strings"
    12  
    13  	"code.gitea.io/gitea/models"
    14  	asymkey_model "code.gitea.io/gitea/models/asymkey"
    15  	"code.gitea.io/gitea/models/auth"
    16  	"code.gitea.io/gitea/models/db"
    17  	user_model "code.gitea.io/gitea/models/user"
    18  	"code.gitea.io/gitea/modules/auth/password"
    19  	"code.gitea.io/gitea/modules/context"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/setting"
    22  	api "code.gitea.io/gitea/modules/structs"
    23  	"code.gitea.io/gitea/modules/timeutil"
    24  	"code.gitea.io/gitea/modules/util"
    25  	"code.gitea.io/gitea/modules/web"
    26  	"code.gitea.io/gitea/routers/api/v1/user"
    27  	"code.gitea.io/gitea/routers/api/v1/utils"
    28  	asymkey_service "code.gitea.io/gitea/services/asymkey"
    29  	"code.gitea.io/gitea/services/convert"
    30  	"code.gitea.io/gitea/services/mailer"
    31  	user_service "code.gitea.io/gitea/services/user"
    32  )
    33  
    34  func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64, loginName string) {
    35  	if sourceID == 0 {
    36  		return
    37  	}
    38  
    39  	source, err := auth.GetSourceByID(sourceID)
    40  	if err != nil {
    41  		if auth.IsErrSourceNotExist(err) {
    42  			ctx.Error(http.StatusUnprocessableEntity, "", err)
    43  		} else {
    44  			ctx.Error(http.StatusInternalServerError, "auth.GetSourceByID", err)
    45  		}
    46  		return
    47  	}
    48  
    49  	u.LoginType = source.Type
    50  	u.LoginSource = source.ID
    51  	u.LoginName = loginName
    52  }
    53  
    54  // CreateUser create a user
    55  func CreateUser(ctx *context.APIContext) {
    56  	// swagger:operation POST /admin/users admin adminCreateUser
    57  	// ---
    58  	// summary: Create a user
    59  	// consumes:
    60  	// - application/json
    61  	// produces:
    62  	// - application/json
    63  	// parameters:
    64  	// - name: body
    65  	//   in: body
    66  	//   schema:
    67  	//     "$ref": "#/definitions/CreateUserOption"
    68  	// responses:
    69  	//   "201":
    70  	//     "$ref": "#/responses/User"
    71  	//   "400":
    72  	//     "$ref": "#/responses/error"
    73  	//   "403":
    74  	//     "$ref": "#/responses/forbidden"
    75  	//   "422":
    76  	//     "$ref": "#/responses/validationError"
    77  
    78  	form := web.GetForm(ctx).(*api.CreateUserOption)
    79  
    80  	u := &user_model.User{
    81  		Name:               form.Username,
    82  		FullName:           form.FullName,
    83  		Email:              form.Email,
    84  		Passwd:             form.Password,
    85  		MustChangePassword: true,
    86  		LoginType:          auth.Plain,
    87  	}
    88  	if form.MustChangePassword != nil {
    89  		u.MustChangePassword = *form.MustChangePassword
    90  	}
    91  
    92  	parseAuthSource(ctx, u, form.SourceID, form.LoginName)
    93  	if ctx.Written() {
    94  		return
    95  	}
    96  
    97  	if u.LoginType == auth.Plain {
    98  		if len(form.Password) < setting.MinPasswordLength {
    99  			err := errors.New("PasswordIsRequired")
   100  			ctx.Error(http.StatusBadRequest, "PasswordIsRequired", err)
   101  			return
   102  		}
   103  
   104  		if !password.IsComplexEnough(form.Password) {
   105  			err := errors.New("PasswordComplexity")
   106  			ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
   107  			return
   108  		}
   109  
   110  		pwned, err := password.IsPwned(ctx, form.Password)
   111  		if pwned {
   112  			if err != nil {
   113  				log.Error(err.Error())
   114  			}
   115  			ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
   116  			return
   117  		}
   118  	}
   119  
   120  	overwriteDefault := &user_model.CreateUserOverwriteOptions{
   121  		IsActive: util.OptionalBoolTrue,
   122  	}
   123  
   124  	if form.Restricted != nil {
   125  		overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
   126  	}
   127  
   128  	if form.Visibility != "" {
   129  		visibility := api.VisibilityModes[form.Visibility]
   130  		overwriteDefault.Visibility = &visibility
   131  	}
   132  
   133  	// Update the user creation timestamp. This can only be done after the user
   134  	// record has been inserted into the database; the insert intself will always
   135  	// set the creation timestamp to "now".
   136  	if form.Created != nil {
   137  		u.CreatedUnix = timeutil.TimeStamp(form.Created.Unix())
   138  		u.UpdatedUnix = u.CreatedUnix
   139  	}
   140  
   141  	if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
   142  		if user_model.IsErrUserAlreadyExist(err) ||
   143  			user_model.IsErrEmailAlreadyUsed(err) ||
   144  			db.IsErrNameReserved(err) ||
   145  			db.IsErrNameCharsNotAllowed(err) ||
   146  			user_model.IsErrEmailCharIsNotSupported(err) ||
   147  			user_model.IsErrEmailInvalid(err) ||
   148  			db.IsErrNamePatternNotAllowed(err) {
   149  			ctx.Error(http.StatusUnprocessableEntity, "", err)
   150  		} else {
   151  			ctx.Error(http.StatusInternalServerError, "CreateUser", err)
   152  		}
   153  		return
   154  	}
   155  	log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
   156  
   157  	// Send email notification.
   158  	if form.SendNotify {
   159  		mailer.SendRegisterNotifyMail(u)
   160  	}
   161  	ctx.JSON(http.StatusCreated, convert.ToUser(ctx, u, ctx.Doer))
   162  }
   163  
   164  // EditUser api for modifying a user's information
   165  func EditUser(ctx *context.APIContext) {
   166  	// swagger:operation PATCH /admin/users/{username} admin adminEditUser
   167  	// ---
   168  	// summary: Edit an existing user
   169  	// consumes:
   170  	// - application/json
   171  	// produces:
   172  	// - application/json
   173  	// parameters:
   174  	// - name: username
   175  	//   in: path
   176  	//   description: username of user to edit
   177  	//   type: string
   178  	//   required: true
   179  	// - name: body
   180  	//   in: body
   181  	//   schema:
   182  	//     "$ref": "#/definitions/EditUserOption"
   183  	// responses:
   184  	//   "200":
   185  	//     "$ref": "#/responses/User"
   186  	//   "400":
   187  	//     "$ref": "#/responses/error"
   188  	//   "403":
   189  	//     "$ref": "#/responses/forbidden"
   190  	//   "422":
   191  	//     "$ref": "#/responses/validationError"
   192  
   193  	form := web.GetForm(ctx).(*api.EditUserOption)
   194  
   195  	parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName)
   196  	if ctx.Written() {
   197  		return
   198  	}
   199  
   200  	if len(form.Password) != 0 {
   201  		if len(form.Password) < setting.MinPasswordLength {
   202  			ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
   203  			return
   204  		}
   205  		if !password.IsComplexEnough(form.Password) {
   206  			err := errors.New("PasswordComplexity")
   207  			ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
   208  			return
   209  		}
   210  		pwned, err := password.IsPwned(ctx, form.Password)
   211  		if pwned {
   212  			if err != nil {
   213  				log.Error(err.Error())
   214  			}
   215  			ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
   216  			return
   217  		}
   218  		if ctx.ContextUser.Salt, err = user_model.GetUserSalt(); err != nil {
   219  			ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
   220  			return
   221  		}
   222  		if err = ctx.ContextUser.SetPassword(form.Password); err != nil {
   223  			ctx.InternalServerError(err)
   224  			return
   225  		}
   226  	}
   227  
   228  	if form.MustChangePassword != nil {
   229  		ctx.ContextUser.MustChangePassword = *form.MustChangePassword
   230  	}
   231  
   232  	ctx.ContextUser.LoginName = form.LoginName
   233  
   234  	if form.FullName != nil {
   235  		ctx.ContextUser.FullName = *form.FullName
   236  	}
   237  	var emailChanged bool
   238  	if form.Email != nil {
   239  		email := strings.TrimSpace(*form.Email)
   240  		if len(email) == 0 {
   241  			ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("email is not allowed to be empty string"))
   242  			return
   243  		}
   244  
   245  		if err := user_model.ValidateEmail(email); err != nil {
   246  			ctx.InternalServerError(err)
   247  			return
   248  		}
   249  
   250  		emailChanged = !strings.EqualFold(ctx.ContextUser.Email, email)
   251  		ctx.ContextUser.Email = email
   252  	}
   253  	if form.Website != nil {
   254  		ctx.ContextUser.Website = *form.Website
   255  	}
   256  	if form.Location != nil {
   257  		ctx.ContextUser.Location = *form.Location
   258  	}
   259  	if form.Description != nil {
   260  		ctx.ContextUser.Description = *form.Description
   261  	}
   262  	if form.Active != nil {
   263  		ctx.ContextUser.IsActive = *form.Active
   264  	}
   265  	if len(form.Visibility) != 0 {
   266  		ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility]
   267  	}
   268  	if form.Admin != nil {
   269  		if !*form.Admin && user_model.IsLastAdminUser(ctx, ctx.ContextUser) {
   270  			ctx.Error(http.StatusBadRequest, "LastAdmin", ctx.Tr("auth.last_admin"))
   271  			return
   272  		}
   273  		ctx.ContextUser.IsAdmin = *form.Admin
   274  	}
   275  	if form.AllowGitHook != nil {
   276  		ctx.ContextUser.AllowGitHook = *form.AllowGitHook
   277  	}
   278  	if form.AllowImportLocal != nil {
   279  		ctx.ContextUser.AllowImportLocal = *form.AllowImportLocal
   280  	}
   281  	if form.MaxRepoCreation != nil {
   282  		ctx.ContextUser.MaxRepoCreation = *form.MaxRepoCreation
   283  	}
   284  	if form.AllowCreateOrganization != nil {
   285  		ctx.ContextUser.AllowCreateOrganization = *form.AllowCreateOrganization
   286  	}
   287  	if form.ProhibitLogin != nil {
   288  		ctx.ContextUser.ProhibitLogin = *form.ProhibitLogin
   289  	}
   290  	if form.Restricted != nil {
   291  		ctx.ContextUser.IsRestricted = *form.Restricted
   292  	}
   293  
   294  	if err := user_model.UpdateUser(ctx, ctx.ContextUser, emailChanged); err != nil {
   295  		if user_model.IsErrEmailAlreadyUsed(err) ||
   296  			user_model.IsErrEmailCharIsNotSupported(err) ||
   297  			user_model.IsErrEmailInvalid(err) {
   298  			ctx.Error(http.StatusUnprocessableEntity, "", err)
   299  		} else {
   300  			ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
   301  		}
   302  		return
   303  	}
   304  	log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
   305  
   306  	ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer))
   307  }
   308  
   309  // DeleteUser api for deleting a user
   310  func DeleteUser(ctx *context.APIContext) {
   311  	// swagger:operation DELETE /admin/users/{username} admin adminDeleteUser
   312  	// ---
   313  	// summary: Delete a user
   314  	// produces:
   315  	// - application/json
   316  	// parameters:
   317  	// - name: username
   318  	//   in: path
   319  	//   description: username of user to delete
   320  	//   type: string
   321  	//   required: true
   322  	// - name: purge
   323  	//   in: query
   324  	//   description: purge the user from the system completely
   325  	//   type: boolean
   326  	// responses:
   327  	//   "204":
   328  	//     "$ref": "#/responses/empty"
   329  	//   "403":
   330  	//     "$ref": "#/responses/forbidden"
   331  	//   "404":
   332  	//     "$ref": "#/responses/notFound"
   333  	//   "422":
   334  	//     "$ref": "#/responses/validationError"
   335  
   336  	if ctx.ContextUser.IsOrganization() {
   337  		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
   338  		return
   339  	}
   340  
   341  	// admin should not delete themself
   342  	if ctx.ContextUser.ID == ctx.Doer.ID {
   343  		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("you cannot delete yourself"))
   344  		return
   345  	}
   346  
   347  	if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
   348  		if models.IsErrUserOwnRepos(err) ||
   349  			models.IsErrUserHasOrgs(err) ||
   350  			models.IsErrUserOwnPackages(err) ||
   351  			models.IsErrDeleteLastAdminUser(err) {
   352  			ctx.Error(http.StatusUnprocessableEntity, "", err)
   353  		} else {
   354  			ctx.Error(http.StatusInternalServerError, "DeleteUser", err)
   355  		}
   356  		return
   357  	}
   358  	log.Trace("Account deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
   359  
   360  	ctx.Status(http.StatusNoContent)
   361  }
   362  
   363  // CreatePublicKey api for creating a public key to a user
   364  func CreatePublicKey(ctx *context.APIContext) {
   365  	// swagger:operation POST /admin/users/{username}/keys admin adminCreatePublicKey
   366  	// ---
   367  	// summary: Add a public key on behalf of a user
   368  	// consumes:
   369  	// - application/json
   370  	// produces:
   371  	// - application/json
   372  	// parameters:
   373  	// - name: username
   374  	//   in: path
   375  	//   description: username of the user
   376  	//   type: string
   377  	//   required: true
   378  	// - name: key
   379  	//   in: body
   380  	//   schema:
   381  	//     "$ref": "#/definitions/CreateKeyOption"
   382  	// responses:
   383  	//   "201":
   384  	//     "$ref": "#/responses/PublicKey"
   385  	//   "403":
   386  	//     "$ref": "#/responses/forbidden"
   387  	//   "422":
   388  	//     "$ref": "#/responses/validationError"
   389  
   390  	form := web.GetForm(ctx).(*api.CreateKeyOption)
   391  
   392  	user.CreateUserPublicKey(ctx, *form, ctx.ContextUser.ID)
   393  }
   394  
   395  // DeleteUserPublicKey api for deleting a user's public key
   396  func DeleteUserPublicKey(ctx *context.APIContext) {
   397  	// swagger:operation DELETE /admin/users/{username}/keys/{id} admin adminDeleteUserPublicKey
   398  	// ---
   399  	// summary: Delete a user's public key
   400  	// produces:
   401  	// - application/json
   402  	// parameters:
   403  	// - name: username
   404  	//   in: path
   405  	//   description: username of user
   406  	//   type: string
   407  	//   required: true
   408  	// - name: id
   409  	//   in: path
   410  	//   description: id of the key to delete
   411  	//   type: integer
   412  	//   format: int64
   413  	//   required: true
   414  	// responses:
   415  	//   "204":
   416  	//     "$ref": "#/responses/empty"
   417  	//   "403":
   418  	//     "$ref": "#/responses/forbidden"
   419  	//   "404":
   420  	//     "$ref": "#/responses/notFound"
   421  
   422  	if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.ParamsInt64(":id")); err != nil {
   423  		if asymkey_model.IsErrKeyNotExist(err) {
   424  			ctx.NotFound()
   425  		} else if asymkey_model.IsErrKeyAccessDenied(err) {
   426  			ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
   427  		} else {
   428  			ctx.Error(http.StatusInternalServerError, "DeleteUserPublicKey", err)
   429  		}
   430  		return
   431  	}
   432  	log.Trace("Key deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
   433  
   434  	ctx.Status(http.StatusNoContent)
   435  }
   436  
   437  // SearchUsers API for getting information of the users according the filter conditions
   438  func SearchUsers(ctx *context.APIContext) {
   439  	// swagger:operation GET /admin/users admin adminSearchUsers
   440  	// ---
   441  	// summary: Search users according filter conditions
   442  	// produces:
   443  	// - application/json
   444  	// parameters:
   445  	// - name: source_id
   446  	//   in: query
   447  	//   description: ID of the user's login source to search for
   448  	//   type: integer
   449  	//   format: int64
   450  	// - name: login_name
   451  	//   in: query
   452  	//   description: user's login name to search for
   453  	//   type: string
   454  	// - name: page
   455  	//   in: query
   456  	//   description: page number of results to return (1-based)
   457  	//   type: integer
   458  	// - name: limit
   459  	//   in: query
   460  	//   description: page size of results
   461  	//   type: integer
   462  	// responses:
   463  	//   "200":
   464  	//     "$ref": "#/responses/UserList"
   465  	//   "403":
   466  	//     "$ref": "#/responses/forbidden"
   467  
   468  	listOptions := utils.GetListOptions(ctx)
   469  
   470  	users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
   471  		Actor:       ctx.Doer,
   472  		Type:        user_model.UserTypeIndividual,
   473  		LoginName:   ctx.FormTrim("login_name"),
   474  		SourceID:    ctx.FormInt64("source_id"),
   475  		OrderBy:     db.SearchOrderByAlphabetically,
   476  		ListOptions: listOptions,
   477  	})
   478  	if err != nil {
   479  		ctx.Error(http.StatusInternalServerError, "SearchUsers", err)
   480  		return
   481  	}
   482  
   483  	results := make([]*api.User, len(users))
   484  	for i := range users {
   485  		results[i] = convert.ToUser(ctx, users[i], ctx.Doer)
   486  	}
   487  
   488  	ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
   489  	ctx.SetTotalCountHeader(maxResults)
   490  	ctx.JSON(http.StatusOK, &results)
   491  }
   492  
   493  // RenameUser api for renaming a user
   494  func RenameUser(ctx *context.APIContext) {
   495  	// swagger:operation POST /admin/users/{username}/rename admin adminRenameUser
   496  	// ---
   497  	// summary: Rename a user
   498  	// produces:
   499  	// - application/json
   500  	// parameters:
   501  	// - name: username
   502  	//   in: path
   503  	//   description: existing username of user
   504  	//   type: string
   505  	//   required: true
   506  	// - name: body
   507  	//   in: body
   508  	//   required: true
   509  	//   schema:
   510  	//     "$ref": "#/definitions/RenameUserOption"
   511  	// responses:
   512  	//   "204":
   513  	//     "$ref": "#/responses/empty"
   514  	//   "403":
   515  	//     "$ref": "#/responses/forbidden"
   516  	//   "422":
   517  	//     "$ref": "#/responses/validationError"
   518  
   519  	if ctx.ContextUser.IsOrganization() {
   520  		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
   521  		return
   522  	}
   523  
   524  	oldName := ctx.ContextUser.Name
   525  	newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
   526  
   527  	// Check if user name has been changed
   528  	if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
   529  		switch {
   530  		case user_model.IsErrUsernameNotChanged(err):
   531  			// Noop as username is not changed
   532  			ctx.Status(http.StatusNoContent)
   533  		case user_model.IsErrUserAlreadyExist(err):
   534  			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
   535  		case db.IsErrNameReserved(err):
   536  			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName))
   537  		case db.IsErrNamePatternNotAllowed(err):
   538  			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName))
   539  		case db.IsErrNameCharsNotAllowed(err):
   540  			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName))
   541  		default:
   542  			ctx.ServerError("ChangeUserName", err)
   543  		}
   544  		return
   545  	}
   546  
   547  	log.Trace("User name changed: %s -> %s", oldName, newName)
   548  	ctx.Status(http.StatusOK)
   549  }