Compare commits

...

6 Commits

Author SHA1 Message Date
Mark Wylde
3e99cadfa8
Merge a8efed8c627f876c8e6112f4978844763dccd4bc into 1d4ad5aa2b3a321a8d759bb91fc78e0aa6a89ed9 2025-07-01 14:00:44 -03:00
Mark Wylde
a8efed8c62
Merge upstream/main and resolve conflicts 2025-06-21 23:13:51 +01:00
Mark Wylde
4211016c85
refactor for dangerzone 2025-06-21 23:12:09 +01:00
Mark Wylde
b711e5fd91
Merge branch 'main' into add-allowed-org-visibility-modes 2025-04-26 18:10:19 +01:00
Mark Wylde
93dd2042ab
Add ALLOWED_ORG_VISIBILITY_MODES setting
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 <noreply@anthropic.com>
2025-04-26 12:43:01 +01:00
Mark Wylde
a1420340dd
Add ALLOWED_ORG_VISIBILITY_MODES setting
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 <noreply@anthropic.com>
2025-04-26 12:18:24 +01:00
10 changed files with 273 additions and 36 deletions

View File

@ -32,6 +32,8 @@ var Service = struct {
AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"` AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"`
DefaultOrgVisibility string DefaultOrgVisibility string
DefaultOrgVisibilityMode structs.VisibleType DefaultOrgVisibilityMode structs.VisibleType
AllowedOrgVisibilityModes []string
AllowedOrgVisibilityModesSlice AllowedVisibility `ini:"-"`
ActiveCodeLives int ActiveCodeLives int
ResetPwdCodeLives int ResetPwdCodeLives int
RegisterEmailConfirm bool RegisterEmailConfirm bool
@ -108,6 +110,7 @@ var Service = struct {
} }
}{ }{
AllowedUserVisibilityModesSlice: []bool{true, true, true}, AllowedUserVisibilityModesSlice: []bool{true, true, true},
AllowedOrgVisibilityModesSlice: []bool{true, true, true},
} }
// AllowedVisibility store in a 3 item bool array what is allowed // 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.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0]
} }
Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility] 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.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility]
Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool()
Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0) Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0)

View File

@ -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) { func TestLoadServiceRequireSignInView(t *testing.T) {
defer test.MockVariableValue(&Service)() defer test.MockVariableValue(&Service)()

View File

@ -2829,6 +2829,7 @@ form.name_been_taken = The organisation name "%s" has already been taken.
form.name_reserved = The organization name "%s" is reserved. 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.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.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 = Settings
settings.options = Organization settings.options = Organization

View File

@ -33,6 +33,7 @@ func Create(ctx *context.Context) {
} }
ctx.Data["visibility"] = setting.Service.DefaultOrgVisibilityMode ctx.Data["visibility"] = setting.Service.DefaultOrgVisibilityMode
ctx.Data["AllowedOrgVisibilityModes"] = setting.Service.AllowedOrgVisibilityModesSlice.ToVisibleTypeSlice()
ctx.Data["repo_admin_change_team_access"] = true ctx.Data["repo_admin_change_team_access"] = true
ctx.HTML(http.StatusOK, tplCreateOrg) ctx.HTML(http.StatusOK, tplCreateOrg)
@ -48,6 +49,13 @@ func CreatePost(ctx *context.Context) {
return 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() { if ctx.HasError() {
ctx.HTML(http.StatusOK, tplCreateOrg) ctx.HTML(http.StatusOK, tplCreateOrg)
return return

View File

@ -46,6 +46,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["AllowedOrgVisibilityModes"] = setting.Service.AllowedOrgVisibilityModesSlice.ToVisibleTypeSlice()
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err) ctx.ServerError("RenderUserOrgHeader", err)
@ -62,6 +63,14 @@ 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["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() { if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsOptions) ctx.HTML(http.StatusOK, tplSettingsOptions)

View File

@ -149,6 +149,9 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
if !u.IsOrganization() && !setting.Service.AllowedUserVisibilityModesSlice.IsAllowedVisibility(opts.Visibility.Value()) { if !u.IsOrganization() && !setting.Service.AllowedUserVisibilityModesSlice.IsAllowedVisibility(opts.Visibility.Value()) {
return fmt.Errorf("visibility mode not allowed: %s", opts.Visibility.Value().String()) 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() u.Visibility = opts.Visibility.Value()
cols = append(cols, "visibility") cols = append(cols, "visibility")

View File

@ -11,7 +11,9 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
password_module "code.gitea.io/gitea/modules/auth/password" password_module "code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -122,3 +124,68 @@ func TestUpdateAuth(t *testing.T) {
Password: optional.Some("aaaa"), Password: optional.Some("aaaa"),
}), password_module.ErrMinLength) }), 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"}
}

View File

@ -17,18 +17,24 @@
<div class="inline field required {{if .Err_OrgVisibility}}error{{end}}"> <div class="inline field required {{if .Err_OrgVisibility}}error{{end}}">
<label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label> <label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label>
<div class="inline-right"> <div class="inline-right">
{{range $mode := .AllowedOrgVisibilityModes}}
{{if $mode.IsPublic}}
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if .visibility.IsPublic}}checked{{end}}> <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> <label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label>
</div> </div>
{{else if $mode.IsLimited}}
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if .visibility.IsLimited}}checked{{end}}> <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> <label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label>
</div> </div>
{{else if $mode.IsPrivate}}
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if .visibility.IsPrivate}}checked{{end}}> <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> <label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label>
</div> </div>
{{end}}
{{end}}
</div> </div>
</div> </div>

View File

@ -30,27 +30,6 @@
</div> </div>
<div class="divider"></div> <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>
</div>
<div class="field" id="permission_box"> <div class="field" id="permission_box">
<label>{{ctx.Locale.Tr "org.settings.permission"}}</label> <label>{{ctx.Locale.Tr "org.settings.permission"}}</label>

View File

@ -3,6 +3,16 @@
</h4> </h4>
<div class="ui attached error danger segment"> <div class="ui attached error danger segment">
<div class="flex-list"> <div class="flex-list">
<div class="flex-item">
<div class="flex-item-main">
<div class="flex-item-title">{{ctx.Locale.Tr "org.settings.visibility"}}</div>
<div class="flex-item-body">{{ctx.Locale.Tr "org.settings.visibility_helper"}}</div>
</div>
<div class="flex-item-trailing">
<button class="ui basic red show-modal button" data-modal="#change-visibility-modal">{{ctx.Locale.Tr "org.settings.change_visibility"}}</button>
</div>
</div>
<div class="flex-item tw-items-center"> <div class="flex-item tw-items-center">
<div class="flex-item-main"> <div class="flex-item-main">
<div class="flex-item-title">{{ctx.Locale.Tr "org.settings.rename"}}</div> <div class="flex-item-title">{{ctx.Locale.Tr "org.settings.rename"}}</div>
@ -25,6 +35,49 @@
</div> </div>
</div> </div>
<div class="ui small modal" id="change-visibility-modal">
<div class="header">
{{ctx.Locale.Tr "org.settings.change_visibility"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "org.settings.visibility_change_warning"}}</p>
<form class="ui form form-fetch-action" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field" id="visibility_box">
<label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label>
{{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="actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "org.settings.update_settings"}}</button>
</div>
</form>
</div>
</div>
<div class="ui small modal" id="rename-org-modal"> <div class="ui small modal" id="rename-org-modal">
<div class="header"> <div class="header">
{{ctx.Locale.Tr "org.settings.rename"}} {{ctx.Locale.Tr "org.settings.rename"}}