Skip to content

Commit 1ee59f0

Browse files
bohdekdumontnu
andauthored
Allow disabling authentication related user features (#31535)
We have some instances that only allow using an external authentication source for authentication. In this case, users changing their email, password, or linked OpenID connections will not have any effect, and we'd like to prevent showing that to them to prevent confusion. Included in this are several changes to support this: * A new setting to disable user managed authentication credentials (email, password & OpenID connections) * A new setting to disable user managed MFA (2FA codes & WebAuthn) * Fix an issue where some templates had separate logic for determining if a feature was disabled since it didn't check the globally disabled features * Hide more user setting pages in the navbar when their settings aren't enabled --------- Co-authored-by: Kyle D <[email protected]>
1 parent 13015bb commit 1ee59f0

File tree

21 files changed

+586
-17
lines changed

21 files changed

+586
-17
lines changed

custom/conf/app.example.ini

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,15 +1488,19 @@ LEVEL = Info
14881488
;;
14891489
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
14901490
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
1491-
;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future
1491+
;; Disabled features for users could be "deletion", "manage_ssh_keys", "manage_gpg_keys", "manage_mfa", "manage_credentials" more features can be disabled in future
14921492
;; - deletion: a user cannot delete their own account
14931493
;; - manage_ssh_keys: a user cannot configure ssh keys
14941494
;; - manage_gpg_keys: a user cannot configure gpg keys
1495+
;; - manage_mfa: a user cannot configure mfa devices
1496+
;; - manage_credentials: a user cannot configure emails, passwords, or openid
14951497
;USER_DISABLED_FEATURES =
1496-
;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
1498+
;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be "deletion", "manage_ssh_keys", "manage_gpg_keys", "manage_mfa", "manage_credentials". This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
14971499
;; - deletion: a user cannot delete their own account
14981500
;; - manage_ssh_keys: a user cannot configure ssh keys
14991501
;; - manage_gpg_keys: a user cannot configure gpg keys
1502+
;; - manage_mfa: a user cannot configure mfa devices
1503+
;; - manage_credentials: a user cannot configure emails, passwords, or openid
15001504
;;EXTERNAL_USER_DISABLE_FEATURES =
15011505

15021506
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

docs/content/administration/config-cheat-sheet.en-us.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,14 +517,18 @@ And the following unique queues:
517517

518518
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
519519
- `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
520-
- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future.
520+
- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`, `manage_mfa`, `manage_credentials` and more features can be added in future.
521521
- `deletion`: User cannot delete their own account.
522522
- `manage_ssh_keys`: User cannot configure ssh keys.
523523
- `manage_gpg_keys`: User cannot configure gpg keys.
524-
- `EXTERNAL_USER_DISABLE_FEATURES`: **_empty_**: Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
524+
- `manage_mfa`: a User cannot configure mfa devices.
525+
- `manage_credentials`: a user cannot configure emails, passwords, or openid
526+
- `EXTERNAL_USER_DISABLE_FEATURES`: **_empty_**: Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`, `manage_mfa`, `manage_credentials`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
525527
- `deletion`: User cannot delete their own account.
526528
- `manage_ssh_keys`: User cannot configure ssh keys.
527529
- `manage_gpg_keys`: User cannot configure gpg keys.
530+
- `manage_mfa`: a User cannot configure mfa devices.
531+
- `manage_credentials`: a user cannot configure emails, passwords, or openid
528532

529533
## Security (`security`)
530534

models/user/user.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,12 +1263,14 @@ func GetOrderByName() string {
12631263
return "name"
12641264
}
12651265

1266-
// IsFeatureDisabledWithLoginType checks if a user feature is disabled, taking into account the login type of the
1266+
// IsFeatureDisabledWithLoginType checks if a user features are disabled, taking into account the login type of the
12671267
// user if applicable
1268-
func IsFeatureDisabledWithLoginType(user *User, feature string) bool {
1268+
func IsFeatureDisabledWithLoginType(user *User, features ...string) bool {
12691269
// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
1270-
return (user != nil && user.LoginType > auth.Plain && setting.Admin.ExternalUserDisableFeatures.Contains(feature)) ||
1271-
setting.Admin.UserDisabledFeatures.Contains(feature)
1270+
if user != nil && user.LoginType > auth.Plain {
1271+
return setting.Admin.ExternalUserDisableFeatures.Contains(features...)
1272+
}
1273+
return setting.Admin.UserDisabledFeatures.Contains(features...)
12721274
}
12731275

12741276
// DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type

modules/container/set.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
package container
55

6+
import "maps"
7+
68
type Set[T comparable] map[T]struct{}
79

810
// SetOf creates a set and adds the specified elements to it.
@@ -29,11 +31,15 @@ func (s Set[T]) AddMultiple(values ...T) {
2931
}
3032
}
3133

32-
// Contains determines whether a set contains the specified element.
34+
// Contains determines whether a set contains the specified elements.
3335
// Returns true if the set contains the specified element; otherwise, false.
34-
func (s Set[T]) Contains(value T) bool {
35-
_, has := s[value]
36-
return has
36+
func (s Set[T]) Contains(values ...T) bool {
37+
ret := true
38+
for _, value := range values {
39+
_, has := s[value]
40+
ret = ret && has
41+
}
42+
return ret
3743
}
3844

3945
// Remove removes the specified element.
@@ -54,3 +60,12 @@ func (s Set[T]) Values() []T {
5460
}
5561
return keys
5662
}
63+
64+
// Union constructs a new set that is the union of the provided sets
65+
func (s Set[T]) Union(sets ...Set[T]) Set[T] {
66+
newSet := maps.Clone(s)
67+
for i := range sets {
68+
maps.Copy(newSet, sets[i])
69+
}
70+
return newSet
71+
}

modules/setting/admin.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ func loadAdminFrom(rootCfg ConfigProvider) {
2020
Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
2121
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
2222
Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
23-
Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...)
23+
Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...).Union(Admin.UserDisabledFeatures)
2424
}
2525

2626
const (
27-
UserFeatureDeletion = "deletion"
28-
UserFeatureManageSSHKeys = "manage_ssh_keys"
29-
UserFeatureManageGPGKeys = "manage_gpg_keys"
27+
UserFeatureDeletion = "deletion"
28+
UserFeatureManageSSHKeys = "manage_ssh_keys"
29+
UserFeatureManageGPGKeys = "manage_gpg_keys"
30+
UserFeatureManageMFA = "manage_mfa"
31+
UserFeatureManageCredentials = "manage_credentials"
3032
)

routers/web/repo/setting/secrets.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"net/http"
99

10+
user_model "code.gitea.io/gitea/models/user"
1011
"code.gitea.io/gitea/modules/base"
1112
"code.gitea.io/gitea/modules/setting"
1213
shared "code.gitea.io/gitea/routers/web/shared/secrets"
@@ -74,6 +75,7 @@ func Secrets(ctx *context.Context) {
7475
ctx.Data["Title"] = ctx.Tr("actions.actions")
7576
ctx.Data["PageType"] = "secrets"
7677
ctx.Data["PageIsSharedSettingsSecrets"] = true
78+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
7779

7880
sCtx, err := getSecretsCtx(ctx)
7981
if err != nil {

routers/web/user/setting/account.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package setting
66

77
import (
88
"errors"
9+
"fmt"
910
"net/http"
1011
"time"
1112

@@ -33,6 +34,11 @@ const (
3334

3435
// Account renders change user's password, user's email and user suicide page
3536
func Account(ctx *context.Context) {
37+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials, setting.UserFeatureDeletion) && !setting.Service.EnableNotifyMail {
38+
ctx.NotFound("Not Found", fmt.Errorf("account setting are not allowed to be changed"))
39+
return
40+
}
41+
3642
ctx.Data["Title"] = ctx.Tr("settings.account")
3743
ctx.Data["PageIsSettingsAccount"] = true
3844
ctx.Data["Email"] = ctx.Doer.Email
@@ -45,9 +51,16 @@ func Account(ctx *context.Context) {
4551

4652
// AccountPost response for change user's password
4753
func AccountPost(ctx *context.Context) {
54+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
55+
ctx.NotFound("Not Found", fmt.Errorf("password setting is not allowed to be changed"))
56+
return
57+
}
58+
4859
form := web.GetForm(ctx).(*forms.ChangePasswordForm)
4960
ctx.Data["Title"] = ctx.Tr("settings")
5061
ctx.Data["PageIsSettingsAccount"] = true
62+
ctx.Data["Email"] = ctx.Doer.Email
63+
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
5164

5265
if ctx.HasError() {
5366
loadAccountData(ctx)
@@ -89,9 +102,16 @@ func AccountPost(ctx *context.Context) {
89102

90103
// EmailPost response for change user's email
91104
func EmailPost(ctx *context.Context) {
105+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
106+
ctx.NotFound("Not Found", fmt.Errorf("emails are not allowed to be changed"))
107+
return
108+
}
109+
92110
form := web.GetForm(ctx).(*forms.AddEmailForm)
93111
ctx.Data["Title"] = ctx.Tr("settings")
94112
ctx.Data["PageIsSettingsAccount"] = true
113+
ctx.Data["Email"] = ctx.Doer.Email
114+
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
95115

96116
// Make email address primary.
97117
if ctx.FormString("_method") == "PRIMARY" {
@@ -216,6 +236,10 @@ func EmailPost(ctx *context.Context) {
216236

217237
// DeleteEmail response for delete user's email
218238
func DeleteEmail(ctx *context.Context) {
239+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
240+
ctx.NotFound("Not Found", fmt.Errorf("emails are not allowed to be changed"))
241+
return
242+
}
219243
email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
220244
if err != nil || email == nil {
221245
ctx.ServerError("GetEmailAddressByID", err)
@@ -241,6 +265,8 @@ func DeleteAccount(ctx *context.Context) {
241265

242266
ctx.Data["Title"] = ctx.Tr("settings")
243267
ctx.Data["PageIsSettingsAccount"] = true
268+
ctx.Data["Email"] = ctx.Doer.Email
269+
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
244270

245271
if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil {
246272
switch {

routers/web/user/setting/applications.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
auth_model "code.gitea.io/gitea/models/auth"
1111
"code.gitea.io/gitea/models/db"
12+
user_model "code.gitea.io/gitea/models/user"
1213
"code.gitea.io/gitea/modules/base"
1314
"code.gitea.io/gitea/modules/setting"
1415
"code.gitea.io/gitea/modules/web"
@@ -24,6 +25,7 @@ const (
2425
func Applications(ctx *context.Context) {
2526
ctx.Data["Title"] = ctx.Tr("settings.applications")
2627
ctx.Data["PageIsSettingsApplications"] = true
28+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
2729

2830
loadApplicationsData(ctx)
2931

@@ -35,6 +37,7 @@ func ApplicationsPost(ctx *context.Context) {
3537
form := web.GetForm(ctx).(*forms.NewAccessTokenForm)
3638
ctx.Data["Title"] = ctx.Tr("settings")
3739
ctx.Data["PageIsSettingsApplications"] = true
40+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
3841

3942
if ctx.HasError() {
4043
loadApplicationsData(ctx)

routers/web/user/setting/block.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package setting
66
import (
77
"net/http"
88

9+
user_model "code.gitea.io/gitea/models/user"
910
"code.gitea.io/gitea/modules/base"
1011
"code.gitea.io/gitea/modules/setting"
1112
shared_user "code.gitea.io/gitea/routers/web/shared/user"
@@ -19,6 +20,7 @@ const (
1920
func BlockedUsers(ctx *context.Context) {
2021
ctx.Data["Title"] = ctx.Tr("user.block.list")
2122
ctx.Data["PageIsSettingsBlockedUsers"] = true
23+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
2224

2325
shared_user.BlockedUsers(ctx, ctx.Doer)
2426
if ctx.Written() {

routers/web/user/setting/keys.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ const (
2525

2626
// Keys render user's SSH/GPG public keys page
2727
func Keys(ctx *context.Context) {
28+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) {
29+
ctx.NotFound("Not Found", fmt.Errorf("keys setting is not allowed to be changed"))
30+
return
31+
}
32+
2833
ctx.Data["Title"] = ctx.Tr("settings.ssh_gpg_keys")
2934
ctx.Data["PageIsSettingsKeys"] = true
3035
ctx.Data["DisableSSH"] = setting.SSH.Disabled
3136
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
3237
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
38+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
3339

3440
loadKeysData(ctx)
3541

@@ -44,6 +50,7 @@ func KeysPost(ctx *context.Context) {
4450
ctx.Data["DisableSSH"] = setting.SSH.Disabled
4551
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
4652
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
53+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
4754

4855
if ctx.HasError() {
4956
loadKeysData(ctx)

routers/web/user/setting/packages.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525
func Packages(ctx *context.Context) {
2626
ctx.Data["Title"] = ctx.Tr("packages.title")
2727
ctx.Data["PageIsSettingsPackages"] = true
28+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
2829

2930
shared.SetPackagesContext(ctx, ctx.Doer)
3031

@@ -34,6 +35,7 @@ func Packages(ctx *context.Context) {
3435
func PackagesRuleAdd(ctx *context.Context) {
3536
ctx.Data["Title"] = ctx.Tr("packages.title")
3637
ctx.Data["PageIsSettingsPackages"] = true
38+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
3739

3840
shared.SetRuleAddContext(ctx)
3941

@@ -43,6 +45,7 @@ func PackagesRuleAdd(ctx *context.Context) {
4345
func PackagesRuleEdit(ctx *context.Context) {
4446
ctx.Data["Title"] = ctx.Tr("packages.title")
4547
ctx.Data["PageIsSettingsPackages"] = true
48+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
4649

4750
shared.SetRuleEditContext(ctx, ctx.Doer)
4851

@@ -52,6 +55,7 @@ func PackagesRuleEdit(ctx *context.Context) {
5255
func PackagesRuleAddPost(ctx *context.Context) {
5356
ctx.Data["Title"] = ctx.Tr("settings")
5457
ctx.Data["PageIsSettingsPackages"] = true
58+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
5559

5660
shared.PerformRuleAddPost(
5761
ctx,
@@ -64,6 +68,7 @@ func PackagesRuleAddPost(ctx *context.Context) {
6468
func PackagesRuleEditPost(ctx *context.Context) {
6569
ctx.Data["Title"] = ctx.Tr("packages.title")
6670
ctx.Data["PageIsSettingsPackages"] = true
71+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
6772

6873
shared.PerformRuleEditPost(
6974
ctx,
@@ -76,6 +81,7 @@ func PackagesRuleEditPost(ctx *context.Context) {
7681
func PackagesRulePreview(ctx *context.Context) {
7782
ctx.Data["Title"] = ctx.Tr("packages.title")
7883
ctx.Data["PageIsSettingsPackages"] = true
84+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
7985

8086
shared.SetRulePreviewContext(ctx, ctx.Doer)
8187

routers/web/user/setting/profile.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ func Profile(ctx *context.Context) {
4848
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
4949
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
5050

51+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
52+
5153
ctx.HTML(http.StatusOK, tplSettingsProfile)
5254
}
5355

@@ -57,6 +59,7 @@ func ProfilePost(ctx *context.Context) {
5759
ctx.Data["PageIsSettingsProfile"] = true
5860
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
5961
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
62+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
6063

6164
if ctx.HasError() {
6265
ctx.HTML(http.StatusOK, tplSettingsProfile)
@@ -182,6 +185,7 @@ func DeleteAvatar(ctx *context.Context) {
182185
func Organization(ctx *context.Context) {
183186
ctx.Data["Title"] = ctx.Tr("settings.organization")
184187
ctx.Data["PageIsSettingsOrganization"] = true
188+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
185189

186190
opts := organization.FindOrgOptions{
187191
ListOptions: db.ListOptions{
@@ -213,6 +217,7 @@ func Organization(ctx *context.Context) {
213217
func Repos(ctx *context.Context) {
214218
ctx.Data["Title"] = ctx.Tr("settings.repos")
215219
ctx.Data["PageIsSettingsRepos"] = true
220+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
216221
ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
217222
ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
218223

@@ -326,6 +331,7 @@ func Appearance(ctx *context.Context) {
326331
allThemes = append([]string{setting.UI.DefaultTheme}, allThemes...) // move the default theme to the top
327332
}
328333
ctx.Data["AllThemes"] = allThemes
334+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
329335

330336
var hiddenCommentTypes *big.Int
331337
val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)

0 commit comments

Comments
 (0)