Now that #16069 is merged, some sites may wish to enforce that users are all public, limited or private, and/or disallow users from becoming private. This PR adds functionality and settings to constrain a user's ability to change their visibility. Co-authored-by: zeripath <art27@cantab.net>tags/v1.15.0-rc1
@@ -656,6 +656,9 @@ PATH = | |||
;; Public is for users visible for everyone | |||
;DEFAULT_USER_VISIBILITY = public | |||
;; | |||
;; Set whitch visibibilty modes a user can have | |||
;ALLOWED_USER_VISIBILITY_MODES = public,limited,private | |||
;; | |||
;; Either "public", "limited" or "private", default is "public" | |||
;; Limited is for organizations visible only to signed users | |||
;; Private is for organizations visible only to members of the organization |
@@ -513,6 +513,7 @@ relation to port exhaustion. | |||
- `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created | |||
- `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it | |||
- `DEFAULT_USER_VISIBILITY`: **public**: Set default visibility mode for users, either "public", "limited" or "private". | |||
- `ALLOWED_USER_VISIBILITY_MODES`: **public,limited,private**: Set whitch visibibilty modes a user can have | |||
- `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private". | |||
- `DEFAULT_ORG_MEMBER_VISIBLE`: **false** True will make the membership of the users visible when added to the organisation. | |||
- `ALLOW_ONLY_INTERNAL_REGISTRATION`: **false** Set to true to force registration only via gitea. |
@@ -863,26 +863,36 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e | |||
return err | |||
} | |||
// set system defaults | |||
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate | |||
u.Visibility = setting.Service.DefaultUserVisibilityMode | |||
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation | |||
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification | |||
u.MaxRepoCreation = -1 | |||
u.Theme = setting.UI.DefaultTheme | |||
// overwrite defaults if set | |||
if len(overwriteDefault) != 0 && overwriteDefault[0] != nil { | |||
u.Visibility = overwriteDefault[0].Visibility | |||
} | |||
sess := x.NewSession() | |||
defer sess.Close() | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
isExist, err := isUserExist(sess, 0, u.Name) | |||
if err != nil { | |||
return err | |||
} else if isExist { | |||
return ErrUserAlreadyExist{u.Name} | |||
} | |||
// validate data | |||
if err = deleteUserRedirect(sess, u.Name); err != nil { | |||
if err := validateUser(u); err != nil { | |||
return err | |||
} | |||
u.Email = strings.ToLower(u.Email) | |||
if err = ValidateEmail(u.Email); err != nil { | |||
isExist, err := isUserExist(sess, 0, u.Name) | |||
if err != nil { | |||
return err | |||
} else if isExist { | |||
return ErrUserAlreadyExist{u.Name} | |||
} | |||
isExist, err = isEmailUsed(sess, u.Email) | |||
@@ -892,6 +902,8 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e | |||
return ErrEmailAlreadyUsed{u.Email} | |||
} | |||
// prepare for database | |||
u.LowerName = strings.ToLower(u.Name) | |||
u.AvatarEmail = u.Email | |||
if u.Rands, err = GetUserSalt(); err != nil { | |||
@@ -901,16 +913,10 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e | |||
return err | |||
} | |||
// set system defaults | |||
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate | |||
u.Visibility = setting.Service.DefaultUserVisibilityMode | |||
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation | |||
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification | |||
u.MaxRepoCreation = -1 | |||
u.Theme = setting.UI.DefaultTheme | |||
// overwrite defaults if set | |||
if len(overwriteDefault) != 0 && overwriteDefault[0] != nil { | |||
u.Visibility = overwriteDefault[0].Visibility | |||
// save changes to database | |||
if err = deleteUserRedirect(sess, u.Name); err != nil { | |||
return err | |||
} | |||
if _, err = sess.Insert(u); err != nil { | |||
@@ -1056,12 +1062,22 @@ func checkDupEmail(e Engine, u *User) error { | |||
return nil | |||
} | |||
func updateUser(e Engine, u *User) (err error) { | |||
// validateUser check if user is valide to insert / update into database | |||
func validateUser(u *User) error { | |||
if !setting.Service.AllowedUserVisibilityModesSlice.IsAllowedVisibility(u.Visibility) { | |||
return fmt.Errorf("visibility Mode not allowed: %s", u.Visibility.String()) | |||
} | |||
u.Email = strings.ToLower(u.Email) | |||
if err = ValidateEmail(u.Email); err != nil { | |||
return ValidateEmail(u.Email) | |||
} | |||
func updateUser(e Engine, u *User) error { | |||
if err := validateUser(u); err != nil { | |||
return err | |||
} | |||
_, err = e.ID(u.ID).AllCols().Update(u) | |||
_, err := e.ID(u.ID).AllCols().Update(u) | |||
return err | |||
} | |||
@@ -1076,6 +1092,10 @@ func UpdateUserCols(u *User, cols ...string) error { | |||
} | |||
func updateUserCols(e Engine, u *User, cols ...string) error { | |||
if err := validateUser(u); err != nil { | |||
return err | |||
} | |||
_, err := e.ID(u.ID).Cols(cols...).Update(u) | |||
return err | |||
} |
@@ -11,6 +11,7 @@ import ( | |||
"testing" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/structs" | |||
"code.gitea.io/gitea/modules/util" | |||
"github.com/stretchr/testify/assert" | |||
@@ -189,6 +190,7 @@ func TestDeleteUser(t *testing.T) { | |||
func TestEmailNotificationPreferences(t *testing.T) { | |||
assert.NoError(t, PrepareTestDatabase()) | |||
for _, test := range []struct { | |||
expected string | |||
userID int64 | |||
@@ -467,3 +469,23 @@ ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ib | |||
} | |||
} | |||
} | |||
func TestUpdateUser(t *testing.T) { | |||
assert.NoError(t, PrepareTestDatabase()) | |||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | |||
user.KeepActivityPrivate = true | |||
assert.NoError(t, UpdateUser(user)) | |||
user = AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | |||
assert.True(t, user.KeepActivityPrivate) | |||
setting.Service.AllowedUserVisibilityModesSlice = []bool{true, false, false} | |||
user.KeepActivityPrivate = false | |||
user.Visibility = structs.VisibleTypePrivate | |||
assert.Error(t, UpdateUser(user)) | |||
user = AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | |||
assert.True(t, user.KeepActivityPrivate) | |||
user.Email = "no mail@mail.org" | |||
assert.Error(t, UpdateUser(user)) | |||
} |
@@ -14,9 +14,11 @@ import ( | |||
) | |||
// Service settings | |||
var Service struct { | |||
var Service = struct { | |||
DefaultUserVisibility string | |||
DefaultUserVisibilityMode structs.VisibleType | |||
AllowedUserVisibilityModes []string | |||
AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"` | |||
DefaultOrgVisibility string | |||
DefaultOrgVisibilityMode structs.VisibleType | |||
ActiveCodeLives int | |||
@@ -71,6 +73,29 @@ var Service struct { | |||
RequireSigninView bool `ini:"REQUIRE_SIGNIN_VIEW"` | |||
DisableUsersPage bool `ini:"DISABLE_USERS_PAGE"` | |||
} `ini:"service.explore"` | |||
}{ | |||
AllowedUserVisibilityModesSlice: []bool{true, true, true}, | |||
} | |||
// AllowedVisibility store in a 3 item bool array what is allowed | |||
type AllowedVisibility []bool | |||
// IsAllowedVisibility check if a AllowedVisibility allow a specific VisibleType | |||
func (a AllowedVisibility) IsAllowedVisibility(t structs.VisibleType) bool { | |||
if int(t) >= len(a) { | |||
return false | |||
} | |||
return a[t] | |||
} | |||
// ToVisibleTypeSlice convert a AllowedVisibility into a VisibleType slice | |||
func (a AllowedVisibility) ToVisibleTypeSlice() (result []structs.VisibleType) { | |||
for i, v := range a { | |||
if v { | |||
result = append(result, structs.VisibleType(i)) | |||
} | |||
} | |||
return | |||
} | |||
func newService() { | |||
@@ -122,6 +147,13 @@ func newService() { | |||
Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false) | |||
Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) | |||
Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility] | |||
Service.AllowedUserVisibilityModes = sec.Key("ALLOWED_USER_VISIBILITY_MODES").Strings(",") | |||
if len(Service.AllowedUserVisibilityModes) != 0 { | |||
Service.AllowedUserVisibilityModesSlice = []bool{false, false, false} | |||
for _, sMode := range Service.AllowedUserVisibilityModes { | |||
Service.AllowedUserVisibilityModesSlice[structs.VisibilityModes[sMode]] = true | |||
} | |||
} | |||
Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) | |||
Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] | |||
Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() |
@@ -52,6 +52,7 @@ func NewUser(ctx *context.Context) { | |||
ctx.Data["PageIsAdmin"] = true | |||
ctx.Data["PageIsAdminUsers"] = true | |||
ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode | |||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() | |||
ctx.Data["login_type"] = "0-0" | |||
@@ -211,6 +212,7 @@ func EditUser(ctx *context.Context) { | |||
ctx.Data["PageIsAdminUsers"] = true | |||
ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation | |||
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations | |||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() | |||
prepareUserInfo(ctx) | |||
if ctx.Written() { |
@@ -56,7 +56,6 @@ func TestNewUserPost_MustChangePassword(t *testing.T) { | |||
} | |||
func TestNewUserPost_MustChangePasswordFalse(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
ctx := test.MockContext(t, "admin/users/new") | |||
@@ -94,7 +93,6 @@ func TestNewUserPost_MustChangePasswordFalse(t *testing.T) { | |||
} | |||
func TestNewUserPost_InvalidEmail(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
ctx := test.MockContext(t, "admin/users/new") | |||
@@ -125,7 +123,6 @@ func TestNewUserPost_InvalidEmail(t *testing.T) { | |||
} | |||
func TestNewUserPost_VisiblityDefaultPublic(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
ctx := test.MockContext(t, "admin/users/new") | |||
@@ -164,7 +161,6 @@ func TestNewUserPost_VisiblityDefaultPublic(t *testing.T) { | |||
} | |||
func TestNewUserPost_VisibilityPrivate(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
ctx := test.MockContext(t, "admin/users/new") | |||
@@ -38,6 +38,7 @@ const ( | |||
func Profile(ctx *context.Context) { | |||
ctx.Data["Title"] = ctx.Tr("settings") | |||
ctx.Data["PageIsSettingsProfile"] = true | |||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() | |||
ctx.HTML(http.StatusOK, tplSettingsProfile) | |||
} |
@@ -32,25 +32,25 @@ | |||
<div class="inline field {{if .Err_Visibility}}error{{end}}"> | |||
<span class="inline required field"><label for="visibility">{{.i18n.Tr "settings.visibility"}}</label></span> | |||
<div class="ui selection type dropdown"> | |||
{{if .User.Visibility.IsPublic}} | |||
<input type="hidden" id="visibility" name="visibility" value="0"> | |||
{{end}} | |||
{{if .User.Visibility.IsLimited}} | |||
<input type="hidden" id="visibility" name="visibility" value="1"> | |||
{{end}} | |||
{{if .User.Visibility.IsPrivate}} | |||
<input type="hidden" id="visibility" name="visibility" value="2"> | |||
{{end}} | |||
{{if .User.Visibility.IsPublic}}<input type="hidden" id="visibility" name="visibility" value="0">{{end}} | |||
{{if .User.Visibility.IsLimited}}<input type="hidden" id="visibility" name="visibility" value="1">{{end}} | |||
{{if .User.Visibility.IsPrivate}}<input type="hidden" id="visibility" name="visibility" value="2">{{end}} | |||
<div class="text"> | |||
{{if .User.Visibility.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}} | |||
{{if .User.Visibility.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}} | |||
{{if .User.Visibility.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}} | |||
{{if .User.Visibility.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}} | |||
{{if .User.Visibility.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}} | |||
{{if .User.Visibility.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}} | |||
</div> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
<div class="menu"> | |||
<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{.i18n.Tr "settings.visibility.public"}}</div> | |||
<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{.i18n.Tr "settings.visibility.limited"}}</div> | |||
<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{.i18n.Tr "settings.visibility.private"}}</div> | |||
{{range $mode := .AllowedUserVisibilityModes}} | |||
{{if $mode.IsPublic}} | |||
<div class="item poping up" data-content="{{$.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{$.i18n.Tr "settings.visibility.public"}}</div> | |||
{{else if $mode.IsLimited}} | |||
<div class="item poping up" data-content="{{$.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{$.i18n.Tr "settings.visibility.limited"}}</div> | |||
{{else if $mode.IsPrivate}} | |||
<div class="item poping up" data-content="{{$.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{$.i18n.Tr "settings.visibility.private"}}</div> | |||
{{end}} | |||
{{end}} | |||
</div> | |||
</div> | |||
</div> |
@@ -30,15 +30,21 @@ | |||
<div class="ui selection type dropdown"> | |||
<input type="hidden" id="visibility" name="visibility" value="{{.visibility}}"> | |||
<div class="text"> | |||
{{if .DefaultUserVisibilityMode.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}} | |||
{{if .DefaultUserVisibilityMode.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}} | |||
{{if .DefaultUserVisibilityMode.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}} | |||
{{if .DefaultUserVisibilityMode.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}} | |||
{{if .DefaultUserVisibilityMode.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}} | |||
{{if .DefaultUserVisibilityMode.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}} | |||
</div> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
<div class="menu"> | |||
<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{.i18n.Tr "settings.visibility.public"}}</div> | |||
<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{.i18n.Tr "settings.visibility.limited"}}</div> | |||
<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{.i18n.Tr "settings.visibility.private"}}</div> | |||
{{range $mode := .AllowedUserVisibilityModes}} | |||
{{if $mode.IsPublic}} | |||
<div class="item poping up" data-content="{{$.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{$.i18n.Tr "settings.visibility.public"}}</div> | |||
{{else if $mode.IsLimited}} | |||
<div class="item poping up" data-content="{{$.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{$.i18n.Tr "settings.visibility.limited"}}</div> | |||
{{else if $mode.IsPrivate}} | |||
<div class="item poping up" data-content="{{$.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{$.i18n.Tr "settings.visibility.private"}}</div> | |||
{{end}} | |||
{{end}} | |||
</div> | |||
</div> | |||
</div> |
@@ -71,25 +71,25 @@ | |||
<div class="inline field {{if .Err_Visibility}}error{{end}}"> | |||
<span class="inline required field"><label for="visibility">{{.i18n.Tr "settings.visibility"}}</label></span> | |||
<div class="ui selection type dropdown"> | |||
{{if .SignedUser.Visibility.IsPublic}} | |||
<input type="hidden" id="visibility" name="visibility" value="0"> | |||
{{end}} | |||
{{if .SignedUser.Visibility.IsLimited}} | |||
<input type="hidden" id="visibility" name="visibility" value="1"> | |||
{{end}} | |||
{{if .SignedUser.Visibility.IsPrivate}} | |||
<input type="hidden" id="visibility" name="visibility" value="2"> | |||
{{end}} | |||
{{if .SignedUser.Visibility.IsPublic}}<input type="hidden" id="visibility" name="visibility" value="0">{{end}} | |||
{{if .SignedUser.Visibility.IsLimited}}<input type="hidden" id="visibility" name="visibility" value="1">{{end}} | |||
{{if .SignedUser.Visibility.IsPrivate}}<input type="hidden" id="visibility" name="visibility" value="2">{{end}} | |||
<div class="text"> | |||
{{if .SignedUser.Visibility.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}} | |||
{{if .SignedUser.Visibility.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}} | |||
{{if .SignedUser.Visibility.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}} | |||
{{if .SignedUser.Visibility.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}} | |||
{{if .SignedUser.Visibility.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}} | |||
{{if .SignedUser.Visibility.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}} | |||
</div> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
<div class="menu"> | |||
<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{.i18n.Tr "settings.visibility.public"}}</div> | |||
<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{.i18n.Tr "settings.visibility.limited"}}</div> | |||
<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{.i18n.Tr "settings.visibility.private"}}</div> | |||
{{range $mode := .AllowedUserVisibilityModes}} | |||
{{if $mode.IsPublic}} | |||
<div class="item poping up" data-content="{{$.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{$.i18n.Tr "settings.visibility.public"}}</div> | |||
{{else if $mode.IsLimited}} | |||
<div class="item poping up" data-content="{{$.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{$.i18n.Tr "settings.visibility.limited"}}</div> | |||
{{else if $mode.IsPrivate}} | |||
<div class="item poping up" data-content="{{$.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{$.i18n.Tr "settings.visibility.private"}}</div> | |||
{{end}} | |||
{{end}} | |||
</div> | |||
</div> | |||
</div> |