Skip to content

Add ALLOWED_ORG_VISIBILITY_MODES setting #34290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
32 changes: 31 additions & 1 deletion modules/setting/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
81 changes: 81 additions & 0 deletions modules/setting/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)()

Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions routers/web/org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
9 changes: 9 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["AllowedOrgVisibilityModes"] = setting.Service.AllowedOrgVisibilityModesSlice.ToVisibleTypeSlice()

if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions services/user/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
67 changes: 67 additions & 0 deletions services/user/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"}
}
30 changes: 18 additions & 12 deletions templates/org/create.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,24 @@
<div class="inline field required {{if .Err_OrgVisibility}}error{{end}}">
<label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label>
<div class="inline-right">
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if .visibility.IsPublic}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label>
</div>
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if .visibility.IsLimited}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label>
</div>
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if .visibility.IsPrivate}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label>
</div>
{{range $mode := .AllowedOrgVisibilityModes}}
{{if $mode.IsPublic}}
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if $.visibility.IsPublic}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label>
</div>
{{else if $mode.IsLimited}}
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if $.visibility.IsLimited}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label>
</div>
{{else if $mode.IsPrivate}}
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if $.visibility.IsPrivate}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label>
</div>
{{end}}
{{end}}
</div>
</div>

Expand Down
42 changes: 24 additions & 18 deletions templates/org/settings/options.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,30 @@
<div class="divider"></div>
<div class="field" id="visibility_box">
<label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label>
<div class="field">
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label>
</div>
</div>
{{range $mode := .AllowedOrgVisibilityModes}}
{{if $mode.IsPublic}}
<div class="field">
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq $.CurrentVisibility 0}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label>
</div>
</div>
{{else if $mode.IsLimited}}
<div class="field">
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq $.CurrentVisibility 1}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label>
</div>
</div>
{{else if $mode.IsPrivate}}
<div class="field">
<div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq $.CurrentVisibility 2}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label>
</div>
</div>
{{end}}
{{end}}
</div>

<div class="field" id="permission_box">
Expand Down