Modified - Restored normal self-edit access for super admins and disabled only the admin actions that are truly forbidden.
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled

This commit is contained in:
2026-04-30 17:19:49 +00:00
parent 80497e4194
commit 43161732e3
6 changed files with 27 additions and 16 deletions
+5
View File
@@ -382,3 +382,8 @@ Project Change ID[date-time] - application-version - Type - Summary:
- 2 - I modified `routers/web/admin/users.go` so GOOD-granted accounts cannot be edited or deleted through admin actions, and regular admins may delete other admin accounts except their direct grantor and protected super-admin cases.
- 3 - I modified `routers/api/v1/admin/user.go` so the admin API now enforces the same GOOD-protection and direct-grantor deletion rule.
- 4 - I added locale messages for the new admin restrictions and added focused integration coverage for deleting another admin versus the direct grantor, plus GOOD-protected admin API edits.
73 - [2026-04-30 16:51:42] - v1.27.0-dev-71-g80497e4194 - Type: Modified - Restored normal self-edit access for super admins and disabled only the admin actions that are truly forbidden.
- 1 - I modified `routers/web/admin/users.go` so a GOOD-granted or super-admin user can still edit their own ordinary account fields, while forbidden cross-user edits remain blocked and table action states are computed per target user.
- 2 - I modified `routers/api/v1/admin/user.go` so GOOD-protection no longer blocks a user from editing their own account through the admin API.
- 3 - I modified `templates/admin/user/list.tmpl`, `templates/admin/user/view.tmpl`, and `templates/admin/user/edit.tmpl` so `Edit` stays enabled for allowed self-edits, while forbidden `Edit` and `Delete` actions are shown disabled with the specific reason tooltip or warning message.
+1 -1
View File
@@ -262,7 +262,7 @@ func EditUser(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditUserOption)
if isBootstrapProtectedUser(ctx, ctx.ContextUser.ID) {
if isBootstrapProtectedUser(ctx, ctx.ContextUser.ID) && ctx.ContextUser.ID != ctx.Doer.ID {
ctx.APIError(http.StatusForbidden, errors.New("GOOD-granted accounts cannot be modified or deleted"))
return
}
+10 -2
View File
@@ -107,7 +107,7 @@ func Users(ctx *context.Context) {
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones
}, tplUsers, loadAdminUserListDeleteState, loadAdminUserListAdminBy, loadAdminUserListSuperAdminBy, loadAdminUserListCreatedBy, loadAdminUserListActivatedBy, loadAdminUserListProhibitLoginBy, loadAdminUserListRestrictedBy)
}, tplUsers, loadAdminUserListDeleteState, loadAdminUserListAdminBy, loadAdminUserListSuperAdminBy, loadAdminUserListCreatedBy, loadAdminUserListActivatedBy, loadAdminUserListProhibitLoginBy, loadAdminUserListRestrictedBy, loadAdminUserListActionState)
}
func loadAdminUserListDeleteState(ctx *context.Context, users user_model.UserList) {
@@ -905,7 +905,9 @@ func ViewUser(ctx *context.Context) {
if ctx.Written() {
return
}
ctx.Data["CanEditUser"] = canEditSuperAdminUser(ctx, isSuperAdmin(ctx, u.ID))
canEditUser, editDisabledReason := canEditUserFromAdminPanel(ctx, ctx.Doer, u)
ctx.Data["CanEditUser"] = canEditUser
ctx.Data["EditUserDisabledReason"] = editDisabledReason
repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptionsAll,
@@ -978,6 +980,9 @@ func EditUser(ctx *context.Context) {
}
loadAdminStatusReasonData(ctx, u)
ctx.Data["IsLastAdminUser"] = user_model.IsLastAdminUser(ctx, u)
canDeleteUser, deleteDisabledReason := canDeleteUserFromAdminPanel(ctx, ctx.Doer, u)
ctx.Data["CanDeleteUser"] = canDeleteUser
ctx.Data["DeleteUserDisabledReason"] = deleteDisabledReason
targetIsSuperAdmin := isSuperAdmin(ctx, u.ID)
ctx.Data["SuperAdminEnabled"] = setting.Admin.SuperAdminEnabled
ctx.Data["IsUserSuperAdmin"] = targetIsSuperAdmin
@@ -1024,6 +1029,9 @@ func renderAdminUserEditReasonErr(ctx *context.Context, u *user_model.User, form
ctx.Data["CanEditAdminStatus"] = !setting.Admin.SuperAdminEnabled || isSuperAdmin(ctx, ctx.Doer.ID) || (setting.Admin.AdminManagementPolicy == setting.AdminManagementPolicyAdminsCanPromote && !u.IsAdmin && !targetIsSuperAdmin)
ctx.Data["IsLastSuperAdminUser"] = targetIsSuperAdmin && countSuperAdmins(ctx) <= 1
ctx.Data["IsLastAdminUser"] = user_model.IsLastAdminUser(ctx, u)
canDeleteUser, deleteDisabledReason := canDeleteUserFromAdminPanel(ctx, ctx.Doer, u)
ctx.Data["CanDeleteUser"] = canDeleteUser
ctx.Data["DeleteUserDisabledReason"] = deleteDisabledReason
ctx.Data["admin_reason"] = form.AdminReason
ctx.Data["super_admin_reason"] = form.SuperAdminReason
ctx.Data["active_reason"] = form.ActiveReason
+5 -5
View File
@@ -242,13 +242,13 @@
<div class="divider"></div>
{{if .IsLastAdminUser}}
<p class="text left tw-mb-3">{{svg "octicon-alert"}} {{ctx.Locale.Tr "auth.last_admin"}}</p>
{{if not .CanDeleteUser}}
<p class="text left tw-mb-3">{{svg "octicon-alert"}} {{.DeleteUserDisabledReason}}</p>
{{end}}
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "admin.users.update_profile"}}</button>
{{if .IsLastAdminUser}}
<button class="ui red button disabled" disabled>{{ctx.Locale.Tr "admin.users.delete_account"}}</button>
{{if not .CanDeleteUser}}
<button class="ui red button disabled" disabled data-tooltip-content="{{.DeleteUserDisabledReason}}">{{ctx.Locale.Tr "admin.users.delete_account"}}</button>
{{else}}
<button class="ui red button show-modal" data-modal="#delete-user-modal">{{ctx.Locale.Tr "admin.users.delete_account"}}</button>
{{end}}
@@ -293,7 +293,7 @@
</div>
</div>
{{if not .IsLastAdminUser}}
{{if .CanDeleteUser}}
<div class="ui g-modal-confirm delete modal" id="delete-user-modal">
<div class="header">
{{svg "octicon-trash"}}
+5 -7
View File
@@ -161,20 +161,18 @@
<td>
<div class="tw-flex tw-gap-2">
<a href="{{$.Link}}/{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.users.details"}}">{{svg "octicon-person"}}</a>
{{if or $.CanEditSuperAdmins (not (index $.UsersSuperAdminByAdmin .ID))}}
{{if index $.UsersCanEdit .ID}}
<a href="{{$.Link}}/{{.ID}}/edit" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a>
{{else}}
<span class="tw-text-muted" aria-disabled="true" data-tooltip-content="{{ctx.Locale.Tr "admin.users.super_admin.required"}}">{{svg "octicon-pencil"}}</span>
<span class="tw-text-muted" aria-disabled="true" data-tooltip-content="{{index $.UsersEditDisabledReason .ID}}">{{svg "octicon-pencil"}}</span>
{{end}}
{{if and $.UsersIsLastAdmin (index $.UsersIsLastAdmin .ID)}}
<span class="tw-text-muted" aria-disabled="true" data-tooltip-content="{{ctx.Locale.Tr "auth.last_admin"}}">{{svg "octicon-trash"}}</span>
{{else if and (not $.CanEditSuperAdmins) (index $.UsersSuperAdminByAdmin .ID)}}
<span class="tw-text-muted" aria-disabled="true" data-tooltip-content="{{ctx.Locale.Tr "admin.users.super_admin.required"}}">{{svg "octicon-trash"}}</span>
{{else}}
{{if index $.UsersCanDelete .ID}}
<a class="tw-text-red show-modal" href data-modal="#admin-user-delete-modal"
data-modal-form.action="{{$.Link}}/{{.ID}}/delete"
data-tooltip-content="{{ctx.Locale.Tr "admin.users.delete_account"}}"
>{{svg "octicon-trash"}}</a>
{{else}}
<span class="tw-text-muted" aria-disabled="true" data-tooltip-content="{{index $.UsersDeleteDisabledReason .ID}}">{{svg "octicon-trash"}}</span>
{{end}}
</div>
</td>
+1 -1
View File
@@ -9,7 +9,7 @@
{{if .CanEditUser}}
<a class="ui primary tiny button" href="{{.Link}}/edit">{{ctx.Locale.Tr "admin.users.edit"}}</a>
{{else}}
<button class="ui tiny button" type="button" disabled data-tooltip-content="{{ctx.Locale.Tr "admin.users.super_admin.required"}}">{{ctx.Locale.Tr "admin.users.edit"}}</button>
<button class="ui tiny button" type="button" disabled data-tooltip-content="{{.EditUserDisabledReason}}">{{ctx.Locale.Tr "admin.users.edit"}}</button>
{{end}}
</div>
</h4>