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