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 }