From 0b27b93728fd3cf2ecc82ac6a2b5859270543ef2 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sun, 27 Jun 2021 20:47:35 +0200 Subject: [PATCH] Make allowed Visiblity modes configurable for Users (#16271) 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 --- custom/conf/app.example.ini | 3 + .../doc/advanced/config-cheat-sheet.en-us.md | 1 + models/user.go | 64 ++++++++++++------- models/user_test.go | 22 +++++++ modules/setting/service.go | 34 +++++++++- routers/web/admin/users.go | 2 + routers/web/admin/users_test.go | 4 -- routers/web/user/setting/profile.go | 1 + templates/admin/user/edit.tmpl | 30 ++++----- templates/admin/user/new.tmpl | 18 ++++-- templates/user/settings/profile.tmpl | 30 ++++----- 11 files changed, 146 insertions(+), 63 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index e7fe9206e..33ff7a62c 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -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 diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 21359dcab..d1d47bc89 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -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. diff --git a/models/user.go b/models/user.go index 221c840a7..47d24aefd 100644 --- a/models/user.go +++ b/models/user.go @@ -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 } diff --git a/models/user_test.go b/models/user_test.go index 39a1b3c98..34c465c58 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -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)) +} diff --git a/modules/setting/service.go b/modules/setting/service.go index 3f689212f..dbabfb840 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -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() diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index dc2a97e52..e1903ab1d 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -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() { diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go index 17c5a309b..5ce20d8fa 100644 --- a/routers/web/admin/users_test.go +++ b/routers/web/admin/users_test.go @@ -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") diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 463c4ec20..682f92057 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -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) } diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index dba24d983..5e5bc75c9 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -32,25 +32,25 @@
diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl index 2e3917253..a433c5a7c 100644 --- a/templates/admin/user/new.tmpl +++ b/templates/admin/user/new.tmpl @@ -30,15 +30,21 @@ diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 4b860049d..1f1585a78 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -71,25 +71,25 @@