mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-24 16:56:38 +02:00
Restrict organization danger zone actions to site admins via admin config
This commit is contained in:
parent
6eed75af24
commit
76bb60215b
@ -1623,6 +1623,9 @@ LEVEL = Info
|
|||||||
;; - change_username: a user cannot change their username
|
;; - change_username: a user cannot change their username
|
||||||
;; - change_full_name: a user cannot change their full name
|
;; - change_full_name: a user cannot change their full name
|
||||||
;;EXTERNAL_USER_DISABLE_FEATURES =
|
;;EXTERNAL_USER_DISABLE_FEATURES =
|
||||||
|
;; Disabled features for organizations, currently supported: danger_zone
|
||||||
|
;; - danger_zone: only site administrators can delete, rename, or change organization visibility
|
||||||
|
;ORG_DISABLED_FEATURES =
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ var Admin struct {
|
|||||||
DefaultEmailNotification string
|
DefaultEmailNotification string
|
||||||
UserDisabledFeatures container.Set[string]
|
UserDisabledFeatures container.Set[string]
|
||||||
ExternalUserDisableFeatures container.Set[string]
|
ExternalUserDisableFeatures container.Set[string]
|
||||||
|
OrgDisabledFeatures container.Set[string]
|
||||||
}
|
}
|
||||||
|
|
||||||
var validUserFeatures = container.SetOf(
|
var validUserFeatures = container.SetOf(
|
||||||
@ -26,12 +27,21 @@ var validUserFeatures = container.SetOf(
|
|||||||
UserFeatureChangeFullName,
|
UserFeatureChangeFullName,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var validOrgFeatures = container.SetOf(
|
||||||
|
OrgFeatureDangerZone,
|
||||||
|
)
|
||||||
|
|
||||||
|
func CanManageOrgDangerZone(isAdmin bool) bool {
|
||||||
|
return isAdmin || !Admin.OrgDisabledFeatures.Contains(OrgFeatureDangerZone)
|
||||||
|
}
|
||||||
|
|
||||||
func loadAdminFrom(rootCfg ConfigProvider) {
|
func loadAdminFrom(rootCfg ConfigProvider) {
|
||||||
sec := rootCfg.Section("admin")
|
sec := rootCfg.Section("admin")
|
||||||
Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
|
Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
|
||||||
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
|
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
|
||||||
Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
|
Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
|
||||||
Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...).Union(Admin.UserDisabledFeatures)
|
Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...).Union(Admin.UserDisabledFeatures)
|
||||||
|
Admin.OrgDisabledFeatures = container.SetOf(sec.Key("ORG_DISABLED_FEATURES").Strings(",")...)
|
||||||
|
|
||||||
for feature := range Admin.UserDisabledFeatures {
|
for feature := range Admin.UserDisabledFeatures {
|
||||||
if !validUserFeatures.Contains(feature) {
|
if !validUserFeatures.Contains(feature) {
|
||||||
@ -43,6 +53,11 @@ func loadAdminFrom(rootCfg ConfigProvider) {
|
|||||||
log.Warn("EXTERNAL_USER_DISABLE_FEATURES contains unknown feature %q", feature)
|
log.Warn("EXTERNAL_USER_DISABLE_FEATURES contains unknown feature %q", feature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for feature := range Admin.OrgDisabledFeatures {
|
||||||
|
if !validOrgFeatures.Contains(feature) {
|
||||||
|
log.Warn("ORG_DISABLED_FEATURES contains unknown feature %q", feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -53,4 +68,6 @@ const (
|
|||||||
UserFeatureManageCredentials = "manage_credentials"
|
UserFeatureManageCredentials = "manage_credentials"
|
||||||
UserFeatureChangeUsername = "change_username"
|
UserFeatureChangeUsername = "change_username"
|
||||||
UserFeatureChangeFullName = "change_full_name"
|
UserFeatureChangeFullName = "change_full_name"
|
||||||
|
|
||||||
|
OrgFeatureDangerZone = "danger_zone"
|
||||||
)
|
)
|
||||||
|
|||||||
40
modules/setting/admin_test.go
Normal file
40
modules/setting/admin_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadAdminOrgDisabledFeatures(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&Admin)()
|
||||||
|
|
||||||
|
cfg, err := NewConfigProviderFromData(`
|
||||||
|
[admin]
|
||||||
|
ORG_DISABLED_FEATURES = danger_zone
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
loadAdminFrom(cfg)
|
||||||
|
|
||||||
|
assert.True(t, Admin.OrgDisabledFeatures.Contains(OrgFeatureDangerZone))
|
||||||
|
assert.False(t, CanManageOrgDangerZone(false))
|
||||||
|
assert.True(t, CanManageOrgDangerZone(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadAdminOrgDisabledFeaturesDefault(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&Admin)()
|
||||||
|
|
||||||
|
cfg, err := NewConfigProviderFromData(`
|
||||||
|
[admin]
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
loadAdminFrom(cfg)
|
||||||
|
|
||||||
|
assert.False(t, Admin.OrgDisabledFeatures.Contains(OrgFeatureDangerZone))
|
||||||
|
assert.True(t, CanManageOrgDangerZone(false))
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
@ -340,6 +341,11 @@ func Rename(ctx *context.APIContext) {
|
|||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
if ctx.Doer == nil || !setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin) {
|
||||||
|
ctx.APIError(http.StatusForbidden, "Organization danger zone actions are restricted to site administrators")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
form := web.GetForm(ctx).(*api.RenameOrgOption)
|
form := web.GetForm(ctx).(*api.RenameOrgOption)
|
||||||
orgUser := ctx.Org.Organization.AsUser()
|
orgUser := ctx.Org.Organization.AsUser()
|
||||||
if err := user_service.RenameUser(ctx, orgUser, form.NewName, ctx.Doer); err != nil {
|
if err := user_service.RenameUser(ctx, orgUser, form.NewName, ctx.Doer); err != nil {
|
||||||
@ -380,6 +386,10 @@ func Edit(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
form := web.GetForm(ctx).(*api.EditOrgOption)
|
form := web.GetForm(ctx).(*api.EditOrgOption)
|
||||||
|
if form.Visibility != nil && *form.Visibility != "" && ctx.Org.Organization.Visibility.String() != *form.Visibility && (ctx.Doer == nil || !setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin)) {
|
||||||
|
ctx.APIError(http.StatusForbidden, "Organization danger zone actions are restricted to site administrators")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil {
|
if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil {
|
||||||
if errors.Is(err, util.ErrInvalidArgument) {
|
if errors.Is(err, util.ErrInvalidArgument) {
|
||||||
@ -425,6 +435,11 @@ func Delete(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
if ctx.Doer == nil || !setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin) {
|
||||||
|
ctx.APIError(http.StatusForbidden, "Organization danger zone actions are restricted to site administrators")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := org.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
|
if err := org.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -47,6 +47,7 @@ func Settings(ctx *context.Context) {
|
|||||||
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
||||||
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
|
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
|
||||||
ctx.Data["ContextUser"] = ctx.ContextUser
|
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||||
|
ctx.Data["CanManageOrgDangerZone"] = setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin)
|
||||||
|
|
||||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||||
ctx.ServerError("RenderUserOrgHeader", err)
|
ctx.ServerError("RenderUserOrgHeader", err)
|
||||||
@ -63,6 +64,7 @@ func SettingsPost(ctx *context.Context) {
|
|||||||
ctx.Data["PageIsOrgSettings"] = true
|
ctx.Data["PageIsOrgSettings"] = true
|
||||||
ctx.Data["PageIsSettingsOptions"] = true
|
ctx.Data["PageIsSettingsOptions"] = true
|
||||||
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
||||||
|
ctx.Data["CanManageOrgDangerZone"] = setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin)
|
||||||
|
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.HTML(http.StatusOK, tplSettingsOptions)
|
ctx.HTML(http.StatusOK, tplSettingsOptions)
|
||||||
@ -125,6 +127,11 @@ func SettingsDeleteAvatar(ctx *context.Context) {
|
|||||||
|
|
||||||
// SettingsDeleteOrgPost response for deleting an organization
|
// SettingsDeleteOrgPost response for deleting an organization
|
||||||
func SettingsDeleteOrgPost(ctx *context.Context) {
|
func SettingsDeleteOrgPost(ctx *context.Context) {
|
||||||
|
if !setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin) {
|
||||||
|
ctx.HTTPError(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.Org.Organization.Name != ctx.FormString("org_name") {
|
if ctx.Org.Organization.Name != ctx.FormString("org_name") {
|
||||||
ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
|
ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
|
||||||
return
|
return
|
||||||
@ -198,6 +205,11 @@ func Labels(ctx *context.Context) {
|
|||||||
|
|
||||||
// SettingsRenamePost response for renaming organization
|
// SettingsRenamePost response for renaming organization
|
||||||
func SettingsRenamePost(ctx *context.Context) {
|
func SettingsRenamePost(ctx *context.Context) {
|
||||||
|
if !setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin) {
|
||||||
|
ctx.HTTPError(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
form := web.GetForm(ctx).(*forms.RenameOrgForm)
|
form := web.GetForm(ctx).(*forms.RenameOrgForm)
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.JSONError(ctx.GetErrMsg())
|
ctx.JSONError(ctx.GetErrMsg())
|
||||||
@ -248,6 +260,11 @@ func SettingsChangeVisibilityPost(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin) {
|
||||||
|
ctx.HTTPError(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := org_service.ChangeOrganizationVisibility(ctx, ctx.Org.Organization, visibility); err != nil {
|
if err := org_service.ChangeOrganizationVisibility(ctx, ctx.Org.Organization, visibility); err != nil {
|
||||||
log.Error("ChangeOrganizationVisibility: %v", err)
|
log.Error("ChangeOrganizationVisibility: %v", err)
|
||||||
ctx.JSONError(ctx.Tr("error.occurred"))
|
ctx.JSONError(ctx.Tr("error.occurred"))
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
{{if .CanManageOrgDangerZone}}
|
||||||
<h4 class="ui top attached error header">
|
<h4 class="ui top attached error header">
|
||||||
{{ctx.Locale.Tr "repo.settings.danger_zone"}}
|
{{ctx.Locale.Tr "repo.settings.danger_zone"}}
|
||||||
</h4>
|
</h4>
|
||||||
@ -140,3 +141,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user