forked from gitea/gitea
		
	Forbid removing the last admin user (#28337)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		
							parent
							
								
									b820019fec
								
							
						
					
					
						commit
						ce0225c1b8
					
				| @ -57,6 +57,21 @@ func (err ErrUserOwnPackages) Error() string { | ||||
| 	return fmt.Sprintf("user still has ownership of packages [uid: %d]", err.UID) | ||||
| } | ||||
| 
 | ||||
| // ErrDeleteLastAdminUser represents a "DeleteLastAdminUser" kind of error. | ||||
| type ErrDeleteLastAdminUser struct { | ||||
| 	UID int64 | ||||
| } | ||||
| 
 | ||||
| // IsErrDeleteLastAdminUser checks if an error is a ErrDeleteLastAdminUser. | ||||
| func IsErrDeleteLastAdminUser(err error) bool { | ||||
| 	_, ok := err.(ErrDeleteLastAdminUser) | ||||
| 	return ok | ||||
| } | ||||
| 
 | ||||
| func (err ErrDeleteLastAdminUser) Error() string { | ||||
| 	return fmt.Sprintf("can not delete the last admin user [uid: %d]", err.UID) | ||||
| } | ||||
| 
 | ||||
| // ErrNoPendingRepoTransfer is an error type for repositories without a pending | ||||
| // transfer request | ||||
| type ErrNoPendingRepoTransfer struct { | ||||
|  | ||||
| @ -730,9 +730,18 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| // IsLastAdminUser check whether user is the last admin | ||||
| func IsLastAdminUser(ctx context.Context, user *User) bool { | ||||
| 	if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // CountUserFilter represent optional filters for CountUsers | ||||
| type CountUserFilter struct { | ||||
| 	LastLoginSince *int64 | ||||
| 	IsAdmin        util.OptionalBool | ||||
| } | ||||
| 
 | ||||
| // CountUsers returns number of users. | ||||
| @ -741,13 +750,25 @@ func CountUsers(ctx context.Context, opts *CountUserFilter) int64 { | ||||
| } | ||||
| 
 | ||||
| func countUsers(ctx context.Context, opts *CountUserFilter) int64 { | ||||
| 	sess := db.GetEngine(ctx).Where(builder.Eq{"type": "0"}) | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 	cond := builder.NewCond() | ||||
| 	cond = cond.And(builder.Eq{"type": UserTypeIndividual}) | ||||
| 
 | ||||
| 	if opts != nil && opts.LastLoginSince != nil { | ||||
| 		sess = sess.Where(builder.Gte{"last_login_unix": *opts.LastLoginSince}) | ||||
| 	if opts != nil { | ||||
| 		if opts.LastLoginSince != nil { | ||||
| 			cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince}) | ||||
| 		} | ||||
| 
 | ||||
| 		if !opts.IsAdmin.IsNone() { | ||||
| 			cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()}) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	count, err := sess.Where(cond).Count(new(User)) | ||||
| 	if err != nil { | ||||
| 		log.Error("user.countUsers: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	count, _ := sess.Count(new(User)) | ||||
| 	return count | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -423,6 +423,7 @@ authorization_failed_desc = The authorization failed because we detected an inva | ||||
| sspi_auth_failed = SSPI authentication failed | ||||
| password_pwned = The password you chose is on a <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">list of stolen passwords</a> previously exposed in public data breaches. Please try again with a different password and consider changing this password elsewhere too. | ||||
| password_pwned_err = Could not complete request to HaveIBeenPwned | ||||
| last_admin = You cannot remove the last admin. There must be at least one admin. | ||||
| 
 | ||||
| [mail] | ||||
| view_it_on = View it on %s | ||||
| @ -588,6 +589,8 @@ org_still_own_packages = "This organization still owns one or more packages, del | ||||
| 
 | ||||
| target_branch_not_exist = Target branch does not exist. | ||||
| 
 | ||||
| admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first. | ||||
| 
 | ||||
| [user] | ||||
| change_avatar = Change your avatar… | ||||
| joined_on = Joined on %s | ||||
|  | ||||
| @ -183,6 +183,8 @@ func EditUser(ctx *context.APIContext) { | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/User" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "422": | ||||
| @ -264,6 +266,10 @@ func EditUser(ctx *context.APIContext) { | ||||
| 		ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility] | ||||
| 	} | ||||
| 	if form.Admin != nil { | ||||
| 		if !*form.Admin && user_model.IsLastAdminUser(ctx, ctx.ContextUser) { | ||||
| 			ctx.Error(http.StatusBadRequest, "LastAdmin", ctx.Tr("auth.last_admin")) | ||||
| 			return | ||||
| 		} | ||||
| 		ctx.ContextUser.IsAdmin = *form.Admin | ||||
| 	} | ||||
| 	if form.AllowGitHook != nil { | ||||
| @ -341,7 +347,8 @@ func DeleteUser(ctx *context.APIContext) { | ||||
| 	if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil { | ||||
| 		if models.IsErrUserOwnRepos(err) || | ||||
| 			models.IsErrUserHasOrgs(err) || | ||||
| 			models.IsErrUserOwnPackages(err) { | ||||
| 			models.IsErrUserOwnPackages(err) || | ||||
| 			models.IsErrDeleteLastAdminUser(err) { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, "", err) | ||||
| 		} else { | ||||
| 			ctx.Error(http.StatusInternalServerError, "DeleteUser", err) | ||||
|  | ||||
| @ -436,6 +436,12 @@ func EditUserPost(ctx *context.Context) { | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	// Check whether user is the last admin | ||||
| 	if !form.Admin && user_model.IsLastAdminUser(ctx, u) { | ||||
| 		ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	u.LoginName = form.LoginName | ||||
| 	u.FullName = form.FullName | ||||
| 	emailChanged := !strings.EqualFold(u.Email, form.Email) | ||||
| @ -503,7 +509,10 @@ func DeleteUser(ctx *context.Context) { | ||||
| 			ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid"))) | ||||
| 		case models.IsErrUserOwnPackages(err): | ||||
| 			ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages")) | ||||
| 			ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid")) | ||||
| 			ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid"))) | ||||
| 		case models.IsErrDeleteLastAdminUser(err): | ||||
| 			ctx.Flash.Error(ctx.Tr("auth.last_admin")) | ||||
| 			ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid"))) | ||||
| 		default: | ||||
| 			ctx.ServerError("DeleteUser", err) | ||||
| 		} | ||||
|  | ||||
| @ -244,6 +244,13 @@ func DeleteAccount(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// admin should not delete themself | ||||
| 	if ctx.Doer.IsAdmin { | ||||
| 		ctx.Flash.Error(ctx.Tr("form.admin_cannot_delete_self")) | ||||
| 		ctx.Redirect(setting.AppSubURL + "/user/settings/account") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil { | ||||
| 		switch { | ||||
| 		case models.IsErrUserOwnRepos(err): | ||||
| @ -255,6 +262,9 @@ func DeleteAccount(ctx *context.Context) { | ||||
| 		case models.IsErrUserOwnPackages(err): | ||||
| 			ctx.Flash.Error(ctx.Tr("form.still_own_packages")) | ||||
| 			ctx.Redirect(setting.AppSubURL + "/user/settings/account") | ||||
| 		case models.IsErrDeleteLastAdminUser(err): | ||||
| 			ctx.Flash.Error(ctx.Tr("auth.last_admin")) | ||||
| 			ctx.Redirect(setting.AppSubURL + "/user/settings/account") | ||||
| 		default: | ||||
| 			ctx.ServerError("DeleteUser", err) | ||||
| 		} | ||||
|  | ||||
| @ -129,6 +129,10 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { | ||||
| 		return fmt.Errorf("%s is an organization not a user", u.Name) | ||||
| 	} | ||||
| 
 | ||||
| 	if user_model.IsLastAdminUser(ctx, u) { | ||||
| 		return models.ErrDeleteLastAdminUser{UID: u.ID} | ||||
| 	} | ||||
| 
 | ||||
| 	if purge { | ||||
| 		// Disable the user first | ||||
| 		// NOTE: This is deliberately not within a transaction as it must disable the user immediately to prevent any further action by the user to be purged. | ||||
| @ -295,7 +299,8 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { | ||||
| 		} | ||||
| 		if err := DeleteUser(ctx, u, false); err != nil { | ||||
| 			// Ignore users that were set inactive by admin. | ||||
| 			if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) { | ||||
| 			if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || | ||||
| 				models.IsErrUserOwnPackages(err) || models.IsErrDeleteLastAdminUser(err) { | ||||
| 				continue | ||||
| 			} | ||||
| 			return err | ||||
|  | ||||
							
								
								
									
										3
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @ -677,6 +677,9 @@ | ||||
|           "200": { | ||||
|             "$ref": "#/responses/User" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "403": { | ||||
|             "$ref": "#/responses/forbidden" | ||||
|           }, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 yp05327
						yp05327