From a1420340ddc4b7b760c245bbeb3237e5c0d593cf Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Sat, 26 Apr 2025 12:18:24 +0100 Subject: [PATCH 1/2] Add ALLOWED_ORG_VISIBILITY_MODES setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Similar to ALLOWED_USER_VISIBILITY_MODES, this setting restricts which visibility modes (public, limited, private) can be selected for organizations. - Added new settings in service.go - Updated settings loading to parse the config - Modified org templates to filter visibility options - Added validation in controllers and service layer - Added translation for error message 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- modules/setting/service.go | 32 +++++++++++++++++++++- options/locale/locale_en-US.ini | 1 + routers/web/org/org.go | 8 ++++++ routers/web/org/setting.go | 9 +++++++ services/user/update.go | 3 +++ templates/org/create.tmpl | 30 ++++++++++++--------- templates/org/settings/options.tmpl | 42 ++++++++++++++++------------- 7 files changed, 94 insertions(+), 31 deletions(-) diff --git a/modules/setting/service.go b/modules/setting/service.go index b1b9fedd62afb..2902ebf54222a 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -32,6 +32,8 @@ var Service = struct { AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"` DefaultOrgVisibility string DefaultOrgVisibilityMode structs.VisibleType + AllowedOrgVisibilityModes []string + AllowedOrgVisibilityModesSlice AllowedVisibility `ini:"-"` ActiveCodeLives int ResetPwdCodeLives int RegisterEmailConfirm bool @@ -108,6 +110,7 @@ var Service = struct { } }{ AllowedUserVisibilityModesSlice: []bool{true, true, true}, + AllowedOrgVisibilityModesSlice: []bool{true, true, true}, } // AllowedVisibility store in a 3 item bool array what is allowed @@ -245,7 +248,34 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0] } Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility] - Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) + + // Process allowed organization visibility modes + modes = sec.Key("ALLOWED_ORG_VISIBILITY_MODES").Strings(",") + if len(modes) != 0 { + Service.AllowedOrgVisibilityModes = []string{} + Service.AllowedOrgVisibilityModesSlice = []bool{false, false, false} + for _, sMode := range modes { + if tp, ok := structs.VisibilityModes[sMode]; ok { // remove unsupported modes + Service.AllowedOrgVisibilityModes = append(Service.AllowedOrgVisibilityModes, sMode) + Service.AllowedOrgVisibilityModesSlice[tp] = true + } else { + log.Warn("ALLOWED_ORG_VISIBILITY_MODES %s is unsupported", sMode) + } + } + } + + if len(Service.AllowedOrgVisibilityModes) == 0 { + Service.AllowedOrgVisibilityModes = []string{"public", "limited", "private"} + Service.AllowedOrgVisibilityModesSlice = []bool{true, true, true} + } + + Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").String() + if Service.DefaultOrgVisibility == "" { + Service.DefaultOrgVisibility = Service.AllowedOrgVisibilityModes[0] + } else if !Service.AllowedOrgVisibilityModesSlice[structs.VisibilityModes[Service.DefaultOrgVisibility]] { + log.Warn("DEFAULT_ORG_VISIBILITY %s is wrong or not in ALLOWED_ORG_VISIBILITY_MODES, using first allowed", Service.DefaultOrgVisibility) + Service.DefaultOrgVisibility = Service.AllowedOrgVisibilityModes[0] + } Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9928b3588a8c0..5daa352d8ca9c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2808,6 +2808,7 @@ team_unit_disabled = (Disabled) form.name_reserved = The organization name "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name. form.create_org_not_allowed = You are not allowed to create an organization. +form.visibility_not_allowed = The selected visibility mode is not allowed. settings = Settings settings.options = Organization diff --git a/routers/web/org/org.go b/routers/web/org/org.go index 0540d5c591ee0..de7d7b8f8454d 100644 --- a/routers/web/org/org.go +++ b/routers/web/org/org.go @@ -33,6 +33,7 @@ func Create(ctx *context.Context) { } ctx.Data["visibility"] = setting.Service.DefaultOrgVisibilityMode + ctx.Data["AllowedOrgVisibilityModes"] = setting.Service.AllowedOrgVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["repo_admin_change_team_access"] = true ctx.HTML(http.StatusOK, tplCreateOrg) @@ -48,6 +49,13 @@ func CreatePost(ctx *context.Context) { return } + // Check if the visibility is allowed + if !setting.Service.AllowedOrgVisibilityModesSlice.IsAllowedVisibility(form.Visibility) { + ctx.Data["Err_OrgVisibility"] = true + ctx.RenderWithErr(ctx.Tr("org.form.visibility_not_allowed"), tplCreateOrg, &form) + return + } + if ctx.HasError() { ctx.HTML(http.StatusOK, tplCreateOrg) return diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 82c3bce722c6c..3f28fe5b0e7a3 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -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["AllowedOrgVisibilityModes"] = setting.Service.AllowedOrgVisibilityModesSlice.ToVisibleTypeSlice() if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { ctx.ServerError("RenderUserOrgHeader", err) @@ -63,6 +64,14 @@ func SettingsPost(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsOptions"] = true ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility + ctx.Data["AllowedOrgVisibilityModes"] = setting.Service.AllowedOrgVisibilityModesSlice.ToVisibleTypeSlice() + + // Check if the visibility is allowed + if !setting.Service.AllowedOrgVisibilityModesSlice.IsAllowedVisibility(form.Visibility) { + ctx.Data["Err_Visibility"] = true + ctx.RenderWithErr(ctx.Tr("org.form.visibility_not_allowed"), tplSettingsOptions, form) + return + } if ctx.HasError() { ctx.HTML(http.StatusOK, tplSettingsOptions) diff --git a/services/user/update.go b/services/user/update.go index 4a39f4f783647..ceb16d0e02b61 100644 --- a/services/user/update.go +++ b/services/user/update.go @@ -124,6 +124,9 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er if !u.IsOrganization() && !setting.Service.AllowedUserVisibilityModesSlice.IsAllowedVisibility(opts.Visibility.Value()) { return fmt.Errorf("visibility mode not allowed: %s", opts.Visibility.Value().String()) } + if u.IsOrganization() && !setting.Service.AllowedOrgVisibilityModesSlice.IsAllowedVisibility(opts.Visibility.Value()) { + return fmt.Errorf("visibility mode not allowed for organization: %s", opts.Visibility.Value().String()) + } u.Visibility = opts.Visibility.Value() cols = append(cols, "visibility") diff --git a/templates/org/create.tmpl b/templates/org/create.tmpl index 2d6dca5440ca9..a6e3b4478f902 100644 --- a/templates/org/create.tmpl +++ b/templates/org/create.tmpl @@ -17,18 +17,24 @@
-
- - -
-
- - -
-
- - -
+ {{range $mode := .AllowedOrgVisibilityModes}} + {{if $mode.IsPublic}} +
+ + +
+ {{else if $mode.IsLimited}} +
+ + +
+ {{else if $mode.IsPrivate}} +
+ + +
+ {{end}} + {{end}}
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 76315f3eac48d..be1c592078b3f 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -39,24 +39,30 @@
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
+ {{range $mode := .AllowedOrgVisibilityModes}} + {{if $mode.IsPublic}} +
+
+ + +
+
+ {{else if $mode.IsLimited}} +
+
+ + +
+
+ {{else if $mode.IsPrivate}} +
+
+ + +
+
+ {{end}} + {{end}}
From 93dd2042ab00624c070eeebc172654db564ceeec Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Sat, 26 Apr 2025 12:25:14 +0100 Subject: [PATCH 2/2] Add ALLOWED_ORG_VISIBILITY_MODES setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Similar to ALLOWED_USER_VISIBILITY_MODES, this setting restricts which visibility modes (public, limited, private) can be selected for organizations. - Added new settings in service.go - Updated settings loading to parse the config - Modified org templates to filter visibility options - Added validation in controllers and service layer - Added translation for error message - Added tests for configuration loading and validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- modules/setting/service.go | 4 +- modules/setting/service_test.go | 81 +++++++++++++++++++++++++++++++++ services/user/update_test.go | 67 +++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) diff --git a/modules/setting/service.go b/modules/setting/service.go index 2902ebf54222a..d8f97380c3fb1 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -248,7 +248,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0] } Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility] - + // Process allowed organization visibility modes modes = sec.Key("ALLOWED_ORG_VISIBILITY_MODES").Strings(",") if len(modes) != 0 { @@ -268,7 +268,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.AllowedOrgVisibilityModes = []string{"public", "limited", "private"} Service.AllowedOrgVisibilityModesSlice = []bool{true, true, true} } - + Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").String() if Service.DefaultOrgVisibility == "" { Service.DefaultOrgVisibility = Service.AllowedOrgVisibilityModes[0] diff --git a/modules/setting/service_test.go b/modules/setting/service_test.go index 73736b793a8db..438830cce7727 100644 --- a/modules/setting/service_test.go +++ b/modules/setting/service_test.go @@ -126,6 +126,87 @@ ALLOWED_USER_VISIBILITY_MODES = public, limit, privated } } +func TestLoadServiceOrgVisibilityModes(t *testing.T) { + defer test.MockVariableValue(&Service)() + + kases := map[string]func(){ + ` +[service] +DEFAULT_ORG_VISIBILITY = public +ALLOWED_ORG_VISIBILITY_MODES = public,limited,private +`: func() { + assert.Equal(t, "public", Service.DefaultOrgVisibility) + assert.Equal(t, structs.VisibleTypePublic, Service.DefaultOrgVisibilityMode) + assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedOrgVisibilityModes) + }, + ` + [service] + DEFAULT_ORG_VISIBILITY = public + `: func() { + assert.Equal(t, "public", Service.DefaultOrgVisibility) + assert.Equal(t, structs.VisibleTypePublic, Service.DefaultOrgVisibilityMode) + assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedOrgVisibilityModes) + }, + ` + [service] + DEFAULT_ORG_VISIBILITY = limited + `: func() { + assert.Equal(t, "limited", Service.DefaultOrgVisibility) + assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultOrgVisibilityMode) + assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedOrgVisibilityModes) + }, + ` +[service] +ALLOWED_ORG_VISIBILITY_MODES = public,limited,private +`: func() { + assert.Equal(t, "public", Service.DefaultOrgVisibility) + assert.Equal(t, structs.VisibleTypePublic, Service.DefaultOrgVisibilityMode) + assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedOrgVisibilityModes) + }, + ` +[service] +DEFAULT_ORG_VISIBILITY = public +ALLOWED_ORG_VISIBILITY_MODES = limited,private +`: func() { + assert.Equal(t, "limited", Service.DefaultOrgVisibility) + assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultOrgVisibilityMode) + assert.Equal(t, []string{"limited", "private"}, Service.AllowedOrgVisibilityModes) + }, + ` +[service] +DEFAULT_ORG_VISIBILITY = my_type +ALLOWED_ORG_VISIBILITY_MODES = limited,private +`: func() { + assert.Equal(t, "limited", Service.DefaultOrgVisibility) + assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultOrgVisibilityMode) + assert.Equal(t, []string{"limited", "private"}, Service.AllowedOrgVisibilityModes) + }, + ` +[service] +DEFAULT_ORG_VISIBILITY = public +ALLOWED_ORG_VISIBILITY_MODES = public, limit, privated +`: func() { + assert.Equal(t, "public", Service.DefaultOrgVisibility) + assert.Equal(t, structs.VisibleTypePublic, Service.DefaultOrgVisibilityMode) + assert.Equal(t, []string{"public"}, Service.AllowedOrgVisibilityModes) + }, + } + + for kase, fun := range kases { + t.Run(kase, func(t *testing.T) { + cfg, err := NewConfigProviderFromData(kase) + assert.NoError(t, err) + loadServiceFrom(cfg) + fun() + // reset + Service.AllowedOrgVisibilityModesSlice = []bool{true, true, true} + Service.AllowedOrgVisibilityModes = []string{} + Service.DefaultOrgVisibility = "" + Service.DefaultOrgVisibilityMode = structs.VisibleTypePublic + }) + } +} + func TestLoadServiceRequireSignInView(t *testing.T) { defer test.MockVariableValue(&Service)() diff --git a/services/user/update_test.go b/services/user/update_test.go index fc24a6c212107..011399bc91cb7 100644 --- a/services/user/update_test.go +++ b/services/user/update_test.go @@ -11,7 +11,9 @@ import ( user_model "code.gitea.io/gitea/models/user" password_module "code.gitea.io/gitea/modules/auth/password" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) @@ -118,3 +120,68 @@ func TestUpdateAuth(t *testing.T) { Password: optional.Some("aaaa"), }), password_module.ErrMinLength) } + +func TestVisibilityModeValidation(t *testing.T) { + // Mock testing setup + defer test.MockVariableValue(&setting.Service)() + + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Organization user + org := &user_model.User{ + ID: 500, + Type: user_model.UserTypeOrganization, + Name: "test-org", + LowerName: "test-org", + } + + // Regular user + user := &user_model.User{ + ID: 501, + Type: user_model.UserTypeIndividual, + Name: "test-user", + LowerName: "test-user", + } + + // Test case 1: Allow only limited and private visibility for organizations + setting.Service.AllowedOrgVisibilityModesSlice = []bool{false, true, true} + setting.Service.AllowedOrgVisibilityModes = []string{"limited", "private"} + + // Should fail when trying to set public visibility for organization + err := UpdateUser(db.DefaultContext, org, &UpdateOptions{ + Visibility: optional.Some(structs.VisibleTypePublic), + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "visibility mode not allowed for organization") + + // Should succeed when setting limited visibility for organization + err = UpdateUser(db.DefaultContext, org, &UpdateOptions{ + Visibility: optional.Some(structs.VisibleTypeLimited), + }) + assert.NoError(t, err) + assert.Equal(t, structs.VisibleTypeLimited, org.Visibility) + + // Test case 2: Allow only public and limited visibility for users + setting.Service.AllowedUserVisibilityModesSlice = []bool{true, true, false} + setting.Service.AllowedUserVisibilityModes = []string{"public", "limited"} + + // Should fail when trying to set private visibility for user + err = UpdateUser(db.DefaultContext, user, &UpdateOptions{ + Visibility: optional.Some(structs.VisibleTypePrivate), + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "visibility mode not allowed") + + // Should succeed when setting public visibility for user + err = UpdateUser(db.DefaultContext, user, &UpdateOptions{ + Visibility: optional.Some(structs.VisibleTypePublic), + }) + assert.NoError(t, err) + assert.Equal(t, structs.VisibleTypePublic, user.Visibility) + + // Reset to default settings + setting.Service.AllowedOrgVisibilityModesSlice = []bool{true, true, true} + setting.Service.AllowedOrgVisibilityModes = []string{"public", "limited", "private"} + setting.Service.AllowedUserVisibilityModesSlice = []bool{true, true, true} + setting.Service.AllowedUserVisibilityModes = []string{"public", "limited", "private"} +}