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 }