mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-10 20:59:31 +01:00
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 <art27@cantab.net>
This commit is contained in:
parent
2a98ec1c3c
commit
0b27b93728
11 changed files with 146 additions and 63 deletions
|
@ -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,12 +863,31 @@ 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
|
||||
}
|
||||
|
||||
// validate data
|
||||
|
||||
if err := validateUser(u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isExist, err := isUserExist(sess, 0, u.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -876,15 +895,6 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
|
|||
return ErrUserAlreadyExist{u.Name}
|
||||
}
|
||||
|
||||
if err = deleteUserRedirect(sess, u.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Email = strings.ToLower(u.Email)
|
||||
if err = ValidateEmail(u.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isExist, err = isEmailUsed(sess, u.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -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,15 +32,9 @@
|
|||
<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}}
|
||||
|
@ -48,9 +42,15 @@
|
|||
</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>
|
||||
|
|
|
@ -36,9 +36,15 @@
|
|||
</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,15 +71,9 @@
|
|||
<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}}
|
||||
|
@ -87,9 +81,15 @@
|
|||
</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>
|
||||
|
|
Loading…
Reference in a new issue