Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,9 @@ LEVEL = Info
;; - change_username: a user cannot change their username
;; - change_full_name: a user cannot change their full name
;;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 =

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
17 changes: 17 additions & 0 deletions modules/setting/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var Admin struct {
DefaultEmailNotification string
UserDisabledFeatures container.Set[string]
ExternalUserDisableFeatures container.Set[string]
OrgDisabledFeatures container.Set[string]
}

var validUserFeatures = container.SetOf(
Expand All @@ -26,12 +27,21 @@ var validUserFeatures = container.SetOf(
UserFeatureChangeFullName,
)

var validOrgFeatures = container.SetOf(
OrgFeatureDangerZone,
)

func CanManageOrgDangerZone(isAdmin bool) bool {
return isAdmin || !Admin.OrgDisabledFeatures.Contains(OrgFeatureDangerZone)
}

func loadAdminFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("admin")
Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
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.OrgDisabledFeatures = container.SetOf(sec.Key("ORG_DISABLED_FEATURES").Strings(",")...)

for feature := range Admin.UserDisabledFeatures {
if !validUserFeatures.Contains(feature) {
Expand All @@ -43,6 +53,11 @@ func loadAdminFrom(rootCfg ConfigProvider) {
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 (
Expand All @@ -53,4 +68,6 @@ const (
UserFeatureManageCredentials = "manage_credentials"
UserFeatureChangeUsername = "change_username"
UserFeatureChangeFullName = "change_full_name"

OrgFeatureDangerZone = "danger_zone"
)
40 changes: 40 additions & 0 deletions modules/setting/admin_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
15 changes: 15 additions & 0 deletions routers/api/v1/org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
Expand Down Expand Up @@ -340,6 +341,11 @@ func Rename(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"

if !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)
orgUser := ctx.Org.Organization.AsUser()
if err := user_service.RenameUser(ctx, orgUser, form.NewName, ctx.Doer); err != nil {
Expand Down Expand Up @@ -380,6 +386,10 @@ func Edit(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"

form := web.GetForm(ctx).(*api.EditOrgOption)
if form.Visibility != nil && *form.Visibility != "" && ctx.Org.Organization.Visibility.String() != *form.Visibility && !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 errors.Is(err, util.ErrInvalidArgument) {
Expand Down Expand Up @@ -425,6 +435,11 @@ func Delete(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"

if !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 {
ctx.APIErrorInternal(err)
return
Expand Down
17 changes: 17 additions & 0 deletions routers/web/org/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func Settings(ctx *context.Context) {
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["CanManageOrgDangerZone"] = setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin)

if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
Expand All @@ -63,6 +64,7 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
ctx.Data["CanManageOrgDangerZone"] = setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin)

if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsOptions)
Expand Down Expand Up @@ -125,6 +127,11 @@ func SettingsDeleteAvatar(ctx *context.Context) {

// SettingsDeleteOrgPost response for deleting an organization
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") {
ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
return
Expand Down Expand Up @@ -198,6 +205,11 @@ func Labels(ctx *context.Context) {

// SettingsRenamePost response for renaming organization
func SettingsRenamePost(ctx *context.Context) {
if !setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin) {
ctx.HTTPError(http.StatusForbidden)
return
}

form := web.GetForm(ctx).(*forms.RenameOrgForm)
if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg())
Expand Down Expand Up @@ -248,6 +260,11 @@ func SettingsChangeVisibilityPost(ctx *context.Context) {
return
}

if !setting.CanManageOrgDangerZone(ctx.Doer.IsAdmin) {
ctx.HTTPError(http.StatusForbidden)
return
}

if err := org_service.ChangeOrganizationVisibility(ctx, ctx.Org.Organization, visibility); err != nil {
log.Error("ChangeOrganizationVisibility: %v", err)
ctx.JSONError(ctx.Tr("error.occurred"))
Expand Down
2 changes: 2 additions & 0 deletions templates/org/settings/options_dangerzone.tmpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{if .CanManageOrgDangerZone}}
<h4 class="ui top attached error header">
{{ctx.Locale.Tr "repo.settings.danger_zone"}}
</h4>
Expand Down Expand Up @@ -140,3 +141,4 @@
</form>
</div>
</div>
{{end}}
Loading