Modified - Added install-time admin management policy choices with direct-grantor and inherited-grantor enforcement.
Modified - Updated the example app.ini documentation for the new administrator management policies.
This commit is contained in:
@@ -387,3 +387,13 @@ Project Change ID[date-time] - application-version - Type - Summary:
|
||||
- 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.
|
||||
|
||||
74 - [2026-04-30 20:11:48] - v1.27.0-dev-72-g43161732e3 - Type: Modified - Added install-time admin management policy choices with direct-grantor and inherited-grantor enforcement.
|
||||
- 1 - I modified `modules/setting/admin.go`, `services/forms/user_form.go`, `routers/install/install.go`, `templates/install.tmpl`, and `options/locale/locale_en-US.json` so installation now exposes three administrator-management policies: `super_admin_only`, `grantor_only`, and `grantor_inheritance`, while normalizing the old legacy policy names.
|
||||
- 2 - I modified `models/user/admin_grant.go` so the code can resolve the effective admin grantor by walking the admin-grant chain until it finds an active administrator who still has sign-in enabled.
|
||||
- 3 - I modified `routers/web/admin/users.go` so regular admins can edit or delete only the administrator accounts allowed by the selected policy, while keeping super-admin protections, GOOD protections, self-edit exceptions, and disabled forbidden actions intact in the admin UI.
|
||||
- 4 - I modified `routers/api/v1/admin/user.go` so the admin API now applies the same grantor-based restrictions for editing and deleting administrator accounts.
|
||||
- 5 - I added focused integration coverage in `tests/integration/admin_user_test.go` and `tests/integration/api_admin_test.go` for direct-grantor edits, inherited-grantor edits, and API denial for unrelated admins.
|
||||
|
||||
75 - [2026-04-30 20:37:01] - v1.27.0-dev-72-g43161732e3 - Type: Modified - Updated the example app.ini documentation for the new administrator management policies.
|
||||
- 1 - I modified `custom/conf/app.example.ini` so the `ADMIN_MANAGEMENT_POLICY` comments now document `super_admin_only`, `grantor_only`, and `grantor_inheritance`, and changed the documented default to `grantor_only`.
|
||||
|
||||
@@ -1619,10 +1619,10 @@ LEVEL = Info
|
||||
;;
|
||||
;; Controls who can change administrator and super administrator permissions.
|
||||
;; Options:
|
||||
;; - super_admin_only: only super administrators can grant or revoke administrator and super administrator permissions.
|
||||
;; - admins_can_promote_users: administrators can promote regular users to administrator, while super administrators manage existing administrators and all super administrator changes.
|
||||
;; - super_admin_approval: reserved for the approval workflow.
|
||||
;ADMIN_MANAGEMENT_POLICY = admins_can_promote_users
|
||||
;; - super_admin_only: only super administrators can modify or delete administrator accounts and manage all administrator privilege changes.
|
||||
;; - grantor_only: administrators can promote regular users to administrator and later manage only the administrator accounts they directly promoted.
|
||||
;; - grantor_inheritance: like grantor_only, but if the direct grantor becomes inactive or has sign-in disabled, management passes up the admin grant chain until an eligible administrator is found.
|
||||
;ADMIN_MANAGEMENT_POLICY = grantor_only
|
||||
;; Disabled features for users could be "deletion", "manage_ssh_keys", "manage_gpg_keys", "manage_mfa", "manage_credentials" more features can be disabled in future
|
||||
;; - deletion: a user cannot delete their own account
|
||||
;; - manage_ssh_keys: a user cannot configure ssh keys
|
||||
|
||||
@@ -64,3 +64,43 @@ func IsGrantorOf(ctx context.Context, grantedUserID, possibleGrantorID int64) (b
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func IsEligibleAdminGrantor(ctx context.Context, userID int64) (bool, error) {
|
||||
u, err := GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
if IsErrUserNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return u.IsAdmin && u.IsActive && !u.ProhibitLogin, nil
|
||||
}
|
||||
|
||||
func GetEffectiveAdminGrantorID(ctx context.Context, userID int64) (int64, bool, error) {
|
||||
visited := map[int64]struct{}{userID: {}}
|
||||
currentUserID := userID
|
||||
|
||||
for {
|
||||
grantorID, has, err := GetAdminGrantorID(ctx, currentUserID)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if !has {
|
||||
return 0, false, nil
|
||||
}
|
||||
if _, ok := visited[grantorID]; ok {
|
||||
return 0, false, nil
|
||||
}
|
||||
visited[grantorID] = struct{}{}
|
||||
|
||||
eligible, err := IsEligibleAdminGrantor(ctx, grantorID)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if eligible {
|
||||
return grantorID, true, nil
|
||||
}
|
||||
|
||||
currentUserID = grantorID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,17 @@ var Admin struct {
|
||||
|
||||
const (
|
||||
AdminManagementPolicySuperAdminOnly = "super_admin_only"
|
||||
AdminManagementPolicyGrantorOnly = "grantor_only"
|
||||
AdminManagementPolicyGrantorInheritance = "grantor_inheritance"
|
||||
AdminManagementPolicyAdminsCanPromote = "admins_can_promote_users"
|
||||
AdminManagementPolicySuperAdminApproval = "super_admin_approval"
|
||||
defaultAdminManagementPolicy = AdminManagementPolicyAdminsCanPromote
|
||||
defaultAdminManagementPolicy = AdminManagementPolicyGrantorOnly
|
||||
)
|
||||
|
||||
var validAdminManagementPolicies = container.SetOf(
|
||||
AdminManagementPolicySuperAdminOnly,
|
||||
AdminManagementPolicyGrantorOnly,
|
||||
AdminManagementPolicyGrantorInheritance,
|
||||
AdminManagementPolicyAdminsCanPromote,
|
||||
AdminManagementPolicySuperAdminApproval,
|
||||
)
|
||||
@@ -46,7 +50,7 @@ func loadAdminFrom(rootCfg ConfigProvider) {
|
||||
Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
|
||||
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
|
||||
Admin.SuperAdminEnabled = sec.Key("SUPER_ADMIN_ENABLED").MustBool(true)
|
||||
Admin.AdminManagementPolicy = sec.Key("ADMIN_MANAGEMENT_POLICY").MustString(defaultAdminManagementPolicy)
|
||||
Admin.AdminManagementPolicy = normalizeAdminManagementPolicy(sec.Key("ADMIN_MANAGEMENT_POLICY").MustString(defaultAdminManagementPolicy))
|
||||
Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
|
||||
Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...).Union(Admin.UserDisabledFeatures)
|
||||
|
||||
@@ -67,6 +71,17 @@ func loadAdminFrom(rootCfg ConfigProvider) {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAdminManagementPolicy(policy string) string {
|
||||
switch policy {
|
||||
case AdminManagementPolicyAdminsCanPromote:
|
||||
return AdminManagementPolicyGrantorOnly
|
||||
case AdminManagementPolicySuperAdminApproval:
|
||||
return AdminManagementPolicySuperAdminOnly
|
||||
default:
|
||||
return policy
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
UserFeatureDeletion = "deletion"
|
||||
UserFeatureManageSSHKeys = "manage_ssh_keys"
|
||||
|
||||
@@ -309,6 +309,13 @@
|
||||
"install.admin_password": "Password",
|
||||
"install.confirm_password": "Confirm Password",
|
||||
"install.admin_email": "Email Address",
|
||||
"install.admin_management_policy": "Administrator Management Policy",
|
||||
"install.admin_management_policy.super_admin_only": "Only super admins manage admins",
|
||||
"install.admin_management_policy.super_admin_only_helper": "Regular admins can manage only non-admin accounts. Any admin-to-admin change or deletion requires a super administrator.",
|
||||
"install.admin_management_policy.grantor_only": "Grantor manages promoted admins",
|
||||
"install.admin_management_policy.grantor_only_helper": "Regular admins can promote users to administrator and can later modify or delete only the administrators they directly promoted.",
|
||||
"install.admin_management_policy.grantor_inheritance": "Grantor chain with inheritance",
|
||||
"install.admin_management_policy.grantor_inheritance_helper": "Like grantor-only, but if the direct grantor becomes inactive or has sign-in disabled, management passes up the admin grant chain until an eligible admin is found. If none is found, super admins keep control.",
|
||||
"install.install_btn_confirm": "Install Gitea",
|
||||
"install.test_git_failed": "Could not test 'git' command: %v",
|
||||
"install.sqlite3_not_available": "This Gitea version does not support SQLite3. Please download the official binary version from %s (not the 'gobuild' version).",
|
||||
@@ -3091,6 +3098,7 @@
|
||||
"admin.users.delete_account": "Delete User Account",
|
||||
"admin.users.cannot_delete_self": "You cannot delete yourself",
|
||||
"admin.users.cannot_delete_grantor": "You cannot delete the administrator who granted your privileges.",
|
||||
"admin.users.admin_grantor.required": "You can only manage administrator accounts that belong to your grant chain.",
|
||||
"admin.users.good_immutable": "Accounts granted by GOOD cannot be modified or deleted.",
|
||||
"admin.users.still_own_repo": "This user still owns one or more repositories. Delete or transfer these repositories first.",
|
||||
"admin.users.still_has_org": "This user is a member of an organization. Remove the user from any organizations first.",
|
||||
|
||||
@@ -96,6 +96,57 @@ func countSuperAdmins(ctx *context.APIContext) int {
|
||||
return count
|
||||
}
|
||||
|
||||
func canManageAdminUser(ctx *context.APIContext, doer, target *user_model.User) (bool, error) {
|
||||
if !target.IsAdmin || doer.ID == target.ID {
|
||||
return true, nil
|
||||
}
|
||||
if isSuperAdmin(ctx, doer.ID) {
|
||||
return true, nil
|
||||
}
|
||||
if !doer.IsAdmin {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch setting.Admin.AdminManagementPolicy {
|
||||
case setting.AdminManagementPolicyGrantorOnly:
|
||||
grantorID, has, err := user_model.GetAdminGrantorID(ctx, target.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return has && grantorID == doer.ID, nil
|
||||
case setting.AdminManagementPolicyGrantorInheritance:
|
||||
grantorID, has, err := user_model.GetEffectiveAdminGrantorID(ctx, target.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return has && grantorID == doer.ID, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func canManageAdminStatus(ctx *context.APIContext, doer, target *user_model.User, targetRequestedAdmin bool, targetIsSuperAdmin bool) (bool, error) {
|
||||
targetWasAdmin := target.IsAdmin
|
||||
if !setting.Admin.SuperAdminEnabled || targetWasAdmin == targetRequestedAdmin {
|
||||
return true, nil
|
||||
}
|
||||
if isSuperAdmin(ctx, doer.ID) {
|
||||
return true, nil
|
||||
}
|
||||
if !doer.IsAdmin || targetIsSuperAdmin {
|
||||
return false, nil
|
||||
}
|
||||
switch setting.Admin.AdminManagementPolicy {
|
||||
case setting.AdminManagementPolicyGrantorOnly, setting.AdminManagementPolicyGrantorInheritance:
|
||||
if !targetWasAdmin && targetRequestedAdmin {
|
||||
return true, nil
|
||||
}
|
||||
return canManageAdminUser(ctx, doer, target)
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func canDeleteUserFromAdminAPI(ctx *context.APIContext, doer, target *user_model.User) (bool, error) {
|
||||
if target.ID == doer.ID {
|
||||
return false, errors.New("you cannot delete yourself")
|
||||
@@ -103,6 +154,15 @@ func canDeleteUserFromAdminAPI(ctx *context.APIContext, doer, target *user_model
|
||||
if isBootstrapProtectedUser(ctx, target.ID) {
|
||||
return false, errors.New("GOOD-granted accounts cannot be modified or deleted")
|
||||
}
|
||||
if target.IsAdmin {
|
||||
canManage, err := canManageAdminUser(ctx, doer, target)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !canManage {
|
||||
return false, errors.New("you can only manage administrator accounts that belong to your grant chain")
|
||||
}
|
||||
}
|
||||
isCreator, err := user_model.IsGrantorOf(ctx, doer.ID, target.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -270,13 +330,61 @@ func EditUser(ctx *context.APIContext) {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("super admin required"))
|
||||
return
|
||||
}
|
||||
canManageTargetAdmin, err := canManageAdminUser(ctx, ctx.Doer, ctx.ContextUser)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !canManageTargetAdmin {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("you can only manage administrator accounts that belong to your grant chain"))
|
||||
return
|
||||
}
|
||||
|
||||
wasAdmin := ctx.ContextUser.IsAdmin
|
||||
wasActive := ctx.ContextUser.IsActive
|
||||
wasRestricted := ctx.ContextUser.IsRestricted
|
||||
wasProhibitLogin := ctx.ContextUser.ProhibitLogin
|
||||
wasSuperAdmin := isSuperAdmin(ctx, ctx.ContextUser.ID)
|
||||
|
||||
requestedAdmin := wasAdmin
|
||||
if form.Admin != nil {
|
||||
requestedAdmin = *form.Admin
|
||||
}
|
||||
requestedActive := wasActive
|
||||
if form.Active != nil {
|
||||
requestedActive = *form.Active
|
||||
}
|
||||
requestedRestricted := wasRestricted
|
||||
if form.Restricted != nil {
|
||||
requestedRestricted = *form.Restricted
|
||||
}
|
||||
requestedProhibitLogin := wasProhibitLogin
|
||||
if form.ProhibitLogin != nil {
|
||||
requestedProhibitLogin = *form.ProhibitLogin
|
||||
}
|
||||
|
||||
if ctx.Doer.ID == ctx.ContextUser.ID {
|
||||
requestedAdmin = wasAdmin
|
||||
requestedActive = wasActive
|
||||
requestedRestricted = wasRestricted
|
||||
requestedProhibitLogin = wasProhibitLogin
|
||||
}
|
||||
|
||||
canManageRequestedAdmin, err := canManageAdminStatus(ctx, ctx.Doer, ctx.ContextUser, requestedAdmin, wasSuperAdmin)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !canManageRequestedAdmin {
|
||||
requestedAdmin = wasAdmin
|
||||
}
|
||||
|
||||
authOpts := &user_service.UpdateAuthOptions{
|
||||
LoginSource: optional.FromNonDefault(form.SourceID),
|
||||
LoginName: optional.Some(form.LoginName),
|
||||
Password: optional.FromNonDefault(form.Password),
|
||||
MustChangePassword: optional.FromPtr(form.MustChangePassword),
|
||||
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
|
||||
ProhibitLogin: optional.Some(requestedProhibitLogin),
|
||||
}
|
||||
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
|
||||
switch {
|
||||
@@ -314,14 +422,14 @@ func EditUser(ctx *context.APIContext) {
|
||||
Website: optional.FromPtr(form.Website),
|
||||
Location: optional.FromPtr(form.Location),
|
||||
Description: optional.FromPtr(form.Description),
|
||||
IsActive: optional.FromPtr(form.Active),
|
||||
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
|
||||
IsActive: optional.Some(requestedActive),
|
||||
IsAdmin: user_service.UpdateOptionFieldFromValue(requestedAdmin),
|
||||
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
|
||||
AllowGitHook: optional.FromPtr(form.AllowGitHook),
|
||||
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
|
||||
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
|
||||
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
|
||||
IsRestricted: optional.FromPtr(form.Restricted),
|
||||
IsRestricted: optional.Some(requestedRestricted),
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
|
||||
|
||||
@@ -115,6 +115,7 @@ func Install(ctx *context.Context) {
|
||||
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
|
||||
form.NoReplyAddress = setting.Service.NoReplyAddress
|
||||
form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo)
|
||||
form.AdminManagementPolicy = setting.Admin.AdminManagementPolicy
|
||||
|
||||
middleware.AssignForm(form, ctx.Data)
|
||||
ctx.HTML(http.StatusOK, tplInstall)
|
||||
@@ -199,6 +200,9 @@ func SubmitInstall(ctx *context.Context) {
|
||||
if form.AppURL != "" && form.AppURL[len(form.AppURL)-1] != '/' {
|
||||
form.AppURL += "/"
|
||||
}
|
||||
if form.AdminManagementPolicy == "" {
|
||||
form.AdminManagementPolicy = setting.AdminManagementPolicyGrantorOnly
|
||||
}
|
||||
|
||||
ctx.Data["CurDbType"] = form.DbType
|
||||
|
||||
@@ -393,6 +397,8 @@ func SubmitInstall(ctx *context.Context) {
|
||||
cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking))
|
||||
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress)
|
||||
cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker))
|
||||
cfg.Section("admin").Key("SUPER_ADMIN_ENABLED").SetValue("true")
|
||||
cfg.Section("admin").Key("ADMIN_MANAGEMENT_POLICY").SetValue(form.AdminManagementPolicy)
|
||||
|
||||
cfg.Section("session").Key("PROVIDER").SetValue("file")
|
||||
|
||||
|
||||
@@ -572,18 +572,28 @@ func countSuperAdmins(ctx *context.Context) int {
|
||||
return count
|
||||
}
|
||||
|
||||
func canManageAdminStatus(ctx *context.Context, targetWasAdmin, targetRequestedAdmin, targetIsSuperAdmin bool) bool {
|
||||
func canManageAdminStatus(ctx *context.Context, target *user_model.User, targetRequestedAdmin bool, targetIsSuperAdmin bool) bool {
|
||||
targetWasAdmin := target.IsAdmin
|
||||
if !setting.Admin.SuperAdminEnabled || targetWasAdmin == targetRequestedAdmin {
|
||||
return true
|
||||
}
|
||||
if isSuperAdmin(ctx, ctx.Doer.ID) {
|
||||
return true
|
||||
}
|
||||
if setting.Admin.AdminManagementPolicy == setting.AdminManagementPolicyAdminsCanPromote && !targetWasAdmin && targetRequestedAdmin && !targetIsSuperAdmin {
|
||||
return ctx.Doer.IsAdmin
|
||||
}
|
||||
if !ctx.Doer.IsAdmin || targetIsSuperAdmin {
|
||||
return false
|
||||
}
|
||||
switch setting.Admin.AdminManagementPolicy {
|
||||
case setting.AdminManagementPolicyGrantorOnly, setting.AdminManagementPolicyGrantorInheritance:
|
||||
if !targetWasAdmin && targetRequestedAdmin {
|
||||
return true
|
||||
}
|
||||
canManage, _ := canManageAdminUser(ctx, ctx.Doer, target)
|
||||
return canManage
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func canManageSuperAdminStatus(ctx *context.Context, targetWasSuperAdmin, targetRequestedSuperAdmin bool) bool {
|
||||
if !setting.Admin.SuperAdminEnabled || targetWasSuperAdmin == targetRequestedSuperAdmin {
|
||||
@@ -599,6 +609,66 @@ func canEditSuperAdminUser(ctx *context.Context, targetIsSuperAdmin bool) bool {
|
||||
return isSuperAdmin(ctx, ctx.Doer.ID)
|
||||
}
|
||||
|
||||
func canManageAdminUser(ctx *context.Context, doer, target *user_model.User) (bool, string) {
|
||||
if !target.IsAdmin || doer.ID == target.ID {
|
||||
return true, ""
|
||||
}
|
||||
if isSuperAdmin(ctx, doer.ID) {
|
||||
return true, ""
|
||||
}
|
||||
if !doer.IsAdmin {
|
||||
return false, ctx.Locale.TrString("admin.users.super_admin.required")
|
||||
}
|
||||
|
||||
switch setting.Admin.AdminManagementPolicy {
|
||||
case setting.AdminManagementPolicyGrantorOnly:
|
||||
grantorID, has, err := user_model.GetAdminGrantorID(ctx, target.ID)
|
||||
if err != nil {
|
||||
log.Error("GetAdminGrantorID for user %d: %v", target.ID, err)
|
||||
return false, ctx.Locale.TrString("admin.users.admin_grantor.required")
|
||||
}
|
||||
if has && grantorID == doer.ID {
|
||||
return true, ""
|
||||
}
|
||||
return false, ctx.Locale.TrString("admin.users.admin_grantor.required")
|
||||
case setting.AdminManagementPolicyGrantorInheritance:
|
||||
grantorID, has, err := user_model.GetEffectiveAdminGrantorID(ctx, target.ID)
|
||||
if err != nil {
|
||||
log.Error("GetEffectiveAdminGrantorID for user %d: %v", target.ID, err)
|
||||
return false, ctx.Locale.TrString("admin.users.admin_grantor.required")
|
||||
}
|
||||
if has && grantorID == doer.ID {
|
||||
return true, ""
|
||||
}
|
||||
return false, ctx.Locale.TrString("admin.users.admin_grantor.required")
|
||||
default:
|
||||
return false, ctx.Locale.TrString("admin.users.super_admin.required")
|
||||
}
|
||||
}
|
||||
|
||||
func canEditAdminStatus(ctx *context.Context, target *user_model.User) bool {
|
||||
if !setting.Admin.SuperAdminEnabled {
|
||||
return true
|
||||
}
|
||||
if isSuperAdmin(ctx, ctx.Doer.ID) {
|
||||
return true
|
||||
}
|
||||
if !ctx.Doer.IsAdmin || isSuperAdmin(ctx, target.ID) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch setting.Admin.AdminManagementPolicy {
|
||||
case setting.AdminManagementPolicyGrantorOnly, setting.AdminManagementPolicyGrantorInheritance:
|
||||
if !target.IsAdmin {
|
||||
return true
|
||||
}
|
||||
canManage, _ := canManageAdminUser(ctx, ctx.Doer, target)
|
||||
return canManage
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isBootstrapProtectedUser(ctx *context.Context, userID int64) bool {
|
||||
protected, err := user_model.HasBootstrapAdminGrant(ctx, userID)
|
||||
if err != nil {
|
||||
@@ -615,6 +685,9 @@ func canEditUserFromAdminPanel(ctx *context.Context, doer, target *user_model.Us
|
||||
if !canEditSuperAdminUser(ctx, isSuperAdmin(ctx, target.ID)) {
|
||||
return false, ctx.Locale.TrString("admin.users.super_admin.required")
|
||||
}
|
||||
if target.IsAdmin {
|
||||
return canManageAdminUser(ctx, doer, target)
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
@@ -645,6 +718,13 @@ func canDeleteUserFromAdminPanel(ctx *context.Context, doer, target *user_model.
|
||||
return false, ctx.Locale.TrString("admin.users.good_immutable")
|
||||
}
|
||||
|
||||
if target.IsAdmin {
|
||||
canManage, reason := canManageAdminUser(ctx, doer, target)
|
||||
if !canManage {
|
||||
return false, reason
|
||||
}
|
||||
}
|
||||
|
||||
isCreator, err := user_model.IsGrantorOf(ctx, doer.ID, target.ID)
|
||||
if err != nil {
|
||||
log.Error("IsGrantorOf doer %d target %d: %v", doer.ID, target.ID, err)
|
||||
@@ -987,7 +1067,7 @@ func EditUser(ctx *context.Context) {
|
||||
ctx.Data["SuperAdminEnabled"] = setting.Admin.SuperAdminEnabled
|
||||
ctx.Data["IsUserSuperAdmin"] = targetIsSuperAdmin
|
||||
ctx.Data["CanEditSuperAdmin"] = isSuperAdmin(ctx, ctx.Doer.ID) && ctx.Doer.ID != u.ID
|
||||
ctx.Data["CanEditAdminStatus"] = !setting.Admin.SuperAdminEnabled || isSuperAdmin(ctx, ctx.Doer.ID) || (setting.Admin.AdminManagementPolicy == setting.AdminManagementPolicyAdminsCanPromote && !u.IsAdmin && !targetIsSuperAdmin)
|
||||
ctx.Data["CanEditAdminStatus"] = canEditAdminStatus(ctx, u)
|
||||
ctx.Data["IsLastSuperAdminUser"] = targetIsSuperAdmin && countSuperAdmins(ctx) <= 1
|
||||
|
||||
ctx.HTML(http.StatusOK, tplUserEdit)
|
||||
@@ -1026,7 +1106,7 @@ func renderAdminUserEditReasonErr(ctx *context.Context, u *user_model.User, form
|
||||
ctx.Data["SuperAdminEnabled"] = setting.Admin.SuperAdminEnabled
|
||||
ctx.Data["IsUserSuperAdmin"] = targetIsSuperAdmin
|
||||
ctx.Data["CanEditSuperAdmin"] = isSuperAdmin(ctx, ctx.Doer.ID) && ctx.Doer.ID != u.ID
|
||||
ctx.Data["CanEditAdminStatus"] = !setting.Admin.SuperAdminEnabled || isSuperAdmin(ctx, ctx.Doer.ID) || (setting.Admin.AdminManagementPolicy == setting.AdminManagementPolicyAdminsCanPromote && !u.IsAdmin && !targetIsSuperAdmin)
|
||||
ctx.Data["CanEditAdminStatus"] = canEditAdminStatus(ctx, u)
|
||||
ctx.Data["IsLastSuperAdminUser"] = targetIsSuperAdmin && countSuperAdmins(ctx) <= 1
|
||||
ctx.Data["IsLastAdminUser"] = user_model.IsLastAdminUser(ctx, u)
|
||||
canDeleteUser, deleteDisabledReason := canDeleteUserFromAdminPanel(ctx, ctx.Doer, u)
|
||||
@@ -1105,7 +1185,7 @@ func EditUserPost(ctx *context.Context) {
|
||||
if !requestedAdmin {
|
||||
requestedSuperAdmin = false
|
||||
}
|
||||
if !canManageAdminStatus(ctx, wasAdmin, requestedAdmin, wasSuperAdmin) {
|
||||
if !canManageAdminStatus(ctx, u, requestedAdmin, wasSuperAdmin) {
|
||||
requestedAdmin = wasAdmin
|
||||
requestedSuperAdmin = wasSuperAdmin
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ type InstallForm struct {
|
||||
NoReplyAddress string
|
||||
|
||||
PasswordAlgorithm string
|
||||
AdminManagementPolicy string `binding:"In(,super_admin_only,grantor_only,grantor_inheritance)" locale:"install.admin_management_policy"`
|
||||
|
||||
AdminName string `binding:"OmitEmpty;Username;MaxSize(30)" locale:"install.admin_name"`
|
||||
AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"`
|
||||
|
||||
@@ -300,6 +300,32 @@
|
||||
<label for="admin_confirm_passwd">{{ctx.Locale.Tr "install.confirm_password"}}</label>
|
||||
<input id="admin_confirm_passwd" name="admin_confirm_passwd" autocomplete="new-password" type="password" value="{{.admin_confirm_passwd}}">
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "install.admin_management_policy"}}</label>
|
||||
<div class="grouped fields">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="admin_management_policy" type="radio" value="super_admin_only" {{if eq .admin_management_policy "super_admin_only"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "install.admin_management_policy.super_admin_only"}}</label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "install.admin_management_policy.super_admin_only_helper"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="admin_management_policy" type="radio" value="grantor_only" {{if or (not .admin_management_policy) (eq .admin_management_policy "grantor_only")}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "install.admin_management_policy.grantor_only"}}</label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "install.admin_management_policy.grantor_only_helper"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="admin_management_policy" type="radio" value="grantor_inheritance" {{if eq .admin_management_policy "grantor_inheritance"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "install.admin_management_policy.grantor_inheritance"}}</label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "install.admin_management_policy.grantor_inheritance_helper"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -160,3 +160,78 @@ func TestAdminDeleteAdminButNotGrantor(t *testing.T) {
|
||||
assert.Equal(t, "/-/admin/users/4", resp.Header().Get("Location"))
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
}
|
||||
|
||||
func TestAdminGrantorOnlyCanEditGrantedAdmin(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Admin.AdminManagementPolicy, setting.AdminManagementPolicyGrantorOnly)()
|
||||
|
||||
makeAdminUser(t, 2)
|
||||
makeAdminUser(t, 4)
|
||||
makeAdminUser(t, 8)
|
||||
setAdminGrantor(t, 2, 4, "user4", "user4@example.com")
|
||||
|
||||
session := loginUser(t, "user4")
|
||||
resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", "/-/admin/users/2/edit", map[string]string{
|
||||
"user_name": "user2",
|
||||
"login_name": "user2",
|
||||
"login_type": "0-0",
|
||||
"email": "grantor@example.com",
|
||||
}), http.StatusSeeOther)
|
||||
assert.Equal(t, "/-/admin/users/2", resp.Header().Get("Location"))
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
assert.Equal(t, "grantor@example.com", user2.Email)
|
||||
|
||||
session = loginUser(t, "user8")
|
||||
resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", "/-/admin/users/2/edit", map[string]string{
|
||||
"user_name": "user2",
|
||||
"login_name": "user2",
|
||||
"login_type": "0-0",
|
||||
"email": "blocked@example.com",
|
||||
}), http.StatusSeeOther)
|
||||
assert.Equal(t, "/-/admin/users/2", resp.Header().Get("Location"))
|
||||
|
||||
user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
assert.Equal(t, "grantor@example.com", user2.Email)
|
||||
}
|
||||
|
||||
func TestAdminGrantorInheritanceUsesParentGrantor(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Admin.AdminManagementPolicy, setting.AdminManagementPolicyGrantorInheritance)()
|
||||
|
||||
makeAdminUser(t, 2)
|
||||
makeAdminUser(t, 4)
|
||||
makeAdminUser(t, 8)
|
||||
setAdminGrantor(t, 2, 4, "user4", "user4@example.com")
|
||||
setAdminGrantor(t, 4, 8, "user8", "user8@example.com")
|
||||
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user4.ProhibitLogin = true
|
||||
_, err := db.GetEngine(t.Context()).ID(user4.ID).Cols("prohibit_login").Update(user4)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := loginUser(t, "user8")
|
||||
resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", "/-/admin/users/2/edit", map[string]string{
|
||||
"user_name": "user2",
|
||||
"login_name": "user2",
|
||||
"login_type": "0-0",
|
||||
"email": "inherited@example.com",
|
||||
}), http.StatusSeeOther)
|
||||
assert.Equal(t, "/-/admin/users/2", resp.Header().Get("Location"))
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
assert.Equal(t, "inherited@example.com", user2.Email)
|
||||
}
|
||||
|
||||
func makeAdminUser(t *testing.T, userID int64) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
|
||||
user.IsAdmin = true
|
||||
_, err := db.GetEngine(t.Context()).ID(userID).Cols("is_admin").Update(user)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func setAdminGrantor(t *testing.T, userID, grantorID int64, grantorName, grantorEmail string) {
|
||||
require.NoError(t, user_model.SetUserSetting(t.Context(), userID, user_model.SettingsKeyAdminGrantedBy, strconv.FormatInt(grantorID, 10)))
|
||||
require.NoError(t, user_model.SetUserSetting(t.Context(), userID, user_model.SettingsKeyAdminGrantedByName, grantorName))
|
||||
require.NoError(t, user_model.SetUserSetting(t.Context(), userID, user_model.SettingsKeyAdminGrantedByEmail, grantorEmail))
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ import (
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {
|
||||
@@ -257,6 +259,42 @@ func TestAPIAdminCannotEditGoodGrantedUser(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAPIAdminGrantorPolicyForAdminEdits(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Admin.AdminManagementPolicy, setting.AdminManagementPolicyGrantorOnly)()
|
||||
|
||||
for _, userID := range []int64{2, 4, 8} {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
|
||||
user.IsAdmin = true
|
||||
_, err := db.GetEngine(t.Context()).ID(userID).Cols("is_admin").Update(user)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, user_model.SetUserSetting(t.Context(), 2, user_model.SettingsKeyAdminGrantedBy, "4"))
|
||||
require.NoError(t, user_model.SetUserSetting(t.Context(), 2, user_model.SettingsKeyAdminGrantedByName, "user4"))
|
||||
require.NoError(t, user_model.SetUserSetting(t.Context(), 2, user_model.SettingsKeyAdminGrantedByEmail, "user4@example.com"))
|
||||
|
||||
blockedToken := getUserToken(t, "user8", auth_model.AccessTokenScopeWriteAdmin)
|
||||
fullName := "blocked"
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/users/user2", api.EditUserOption{
|
||||
LoginName: "user2",
|
||||
SourceID: 0,
|
||||
FullName: &fullName,
|
||||
}).AddTokenAuth(blockedToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
grantorToken := getUserToken(t, "user4", auth_model.AccessTokenScopeWriteAdmin)
|
||||
allowedName := "grantor edit"
|
||||
req = NewRequestWithJSON(t, "PATCH", "/api/v1/admin/users/user2", api.EditUserOption{
|
||||
LoginName: "user2",
|
||||
SourceID: 0,
|
||||
FullName: &allowedName,
|
||||
}).AddTokenAuth(grantorToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
assert.Equal(t, allowedName, user2.FullName)
|
||||
}
|
||||
|
||||
func TestAPICreateRepoForUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
|
||||
Reference in New Issue
Block a user