Skip to content

Commit 376fa0d

Browse files
GiteaBotyp05327lunny
authored
Forbid removing the last admin user (#28337) (#28793)
Backport #28337 by @yp05327 Co-authored-by: yp05327 <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent be541d9 commit 376fa0d

File tree

8 files changed

+80
-7
lines changed

8 files changed

+80
-7
lines changed

models/error.go

+15
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ func (err ErrUserOwnPackages) Error() string {
5757
return fmt.Sprintf("user still has ownership of packages [uid: %d]", err.UID)
5858
}
5959

60+
// ErrDeleteLastAdminUser represents a "DeleteLastAdminUser" kind of error.
61+
type ErrDeleteLastAdminUser struct {
62+
UID int64
63+
}
64+
65+
// IsErrDeleteLastAdminUser checks if an error is a ErrDeleteLastAdminUser.
66+
func IsErrDeleteLastAdminUser(err error) bool {
67+
_, ok := err.(ErrDeleteLastAdminUser)
68+
return ok
69+
}
70+
71+
func (err ErrDeleteLastAdminUser) Error() string {
72+
return fmt.Sprintf("can not delete the last admin user [uid: %d]", err.UID)
73+
}
74+
6075
// ErrNoPendingRepoTransfer is an error type for repositories without a pending
6176
// transfer request
6277
type ErrNoPendingRepoTransfer struct {

models/user/user.go

+25-4
Original file line numberDiff line numberDiff line change
@@ -705,9 +705,18 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
705705
return committer.Commit()
706706
}
707707

708+
// IsLastAdminUser check whether user is the last admin
709+
func IsLastAdminUser(ctx context.Context, user *User) bool {
710+
if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 {
711+
return true
712+
}
713+
return false
714+
}
715+
708716
// CountUserFilter represent optional filters for CountUsers
709717
type CountUserFilter struct {
710718
LastLoginSince *int64
719+
IsAdmin util.OptionalBool
711720
}
712721

713722
// CountUsers returns number of users.
@@ -716,13 +725,25 @@ func CountUsers(ctx context.Context, opts *CountUserFilter) int64 {
716725
}
717726

718727
func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
719-
sess := db.GetEngine(ctx).Where(builder.Eq{"type": "0"})
728+
sess := db.GetEngine(ctx)
729+
cond := builder.NewCond()
730+
cond = cond.And(builder.Eq{"type": UserTypeIndividual})
720731

721-
if opts != nil && opts.LastLoginSince != nil {
722-
sess = sess.Where(builder.Gte{"last_login_unix": *opts.LastLoginSince})
732+
if opts != nil {
733+
if opts.LastLoginSince != nil {
734+
cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince})
735+
}
736+
737+
if !opts.IsAdmin.IsNone() {
738+
cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
739+
}
740+
}
741+
742+
count, err := sess.Where(cond).Count(new(User))
743+
if err != nil {
744+
log.Error("user.countUsers: %v", err)
723745
}
724746

725-
count, _ := sess.Count(new(User))
726747
return count
727748
}
728749

options/locale/locale_en-US.ini

+3
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ authorization_failed_desc = The authorization failed because we detected an inva
422422
sspi_auth_failed = SSPI authentication failed
423423
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.
424424
password_pwned_err = Could not complete request to HaveIBeenPwned
425+
last_admin = You cannot remove the last admin. There must be at least one admin.
425426
426427
[mail]
427428
view_it_on = View it on %s
@@ -587,6 +588,8 @@ org_still_own_packages = "This organization still owns one or more packages, del
587588
588589
target_branch_not_exist = Target branch does not exist.
589590
591+
admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first.
592+
590593
[user]
591594
change_avatar = Change your avatar…
592595
joined_on = Joined on %s

routers/api/v1/admin/user.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ func EditUser(ctx *context.APIContext) {
183183
// responses:
184184
// "200":
185185
// "$ref": "#/responses/User"
186+
// "400":
187+
// "$ref": "#/responses/error"
186188
// "403":
187189
// "$ref": "#/responses/forbidden"
188190
// "422":
@@ -264,6 +266,10 @@ func EditUser(ctx *context.APIContext) {
264266
ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility]
265267
}
266268
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+
}
267273
ctx.ContextUser.IsAdmin = *form.Admin
268274
}
269275
if form.AllowGitHook != nil {
@@ -341,7 +347,8 @@ func DeleteUser(ctx *context.APIContext) {
341347
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
342348
if models.IsErrUserOwnRepos(err) ||
343349
models.IsErrUserHasOrgs(err) ||
344-
models.IsErrUserOwnPackages(err) {
350+
models.IsErrUserOwnPackages(err) ||
351+
models.IsErrDeleteLastAdminUser(err) {
345352
ctx.Error(http.StatusUnprocessableEntity, "", err)
346353
} else {
347354
ctx.Error(http.StatusInternalServerError, "DeleteUser", err)

routers/web/admin/users.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,12 @@ func EditUserPost(ctx *context.Context) {
429429

430430
}
431431

432+
// Check whether user is the last admin
433+
if !form.Admin && user_model.IsLastAdminUser(ctx, u) {
434+
ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
435+
return
436+
}
437+
432438
u.LoginName = form.LoginName
433439
u.FullName = form.FullName
434440
emailChanged := !strings.EqualFold(u.Email, form.Email)
@@ -496,7 +502,10 @@ func DeleteUser(ctx *context.Context) {
496502
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
497503
case models.IsErrUserOwnPackages(err):
498504
ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages"))
499-
ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"))
505+
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
506+
case models.IsErrDeleteLastAdminUser(err):
507+
ctx.Flash.Error(ctx.Tr("auth.last_admin"))
508+
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
500509
default:
501510
ctx.ServerError("DeleteUser", err)
502511
}

routers/web/user/setting/account.go

+10
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,13 @@ func DeleteAccount(ctx *context.Context) {
246246
return
247247
}
248248

249+
// admin should not delete themself
250+
if ctx.Doer.IsAdmin {
251+
ctx.Flash.Error(ctx.Tr("form.admin_cannot_delete_self"))
252+
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
253+
return
254+
}
255+
249256
if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil {
250257
switch {
251258
case models.IsErrUserOwnRepos(err):
@@ -257,6 +264,9 @@ func DeleteAccount(ctx *context.Context) {
257264
case models.IsErrUserOwnPackages(err):
258265
ctx.Flash.Error(ctx.Tr("form.still_own_packages"))
259266
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
267+
case models.IsErrDeleteLastAdminUser(err):
268+
ctx.Flash.Error(ctx.Tr("auth.last_admin"))
269+
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
260270
default:
261271
ctx.ServerError("DeleteUser", err)
262272
}

services/user/user.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
129129
return fmt.Errorf("%s is an organization not a user", u.Name)
130130
}
131131

132+
if user_model.IsLastAdminUser(ctx, u) {
133+
return models.ErrDeleteLastAdminUser{UID: u.ID}
134+
}
135+
132136
if purge {
133137
// Disable the user first
134138
// 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 {
295299
}
296300
if err := DeleteUser(ctx, u, false); err != nil {
297301
// Ignore users that were set inactive by admin.
298-
if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
302+
if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) ||
303+
models.IsErrUserOwnPackages(err) || models.IsErrDeleteLastAdminUser(err) {
299304
continue
300305
}
301306
return err

templates/swagger/v1_json.tmpl

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)