Skip to content

Commit 59d4aad

Browse files
authored
Add setting to disable user features when user login type is not plain (#29615)
## Changes - Adds setting `EXTERNAL_USER_DISABLE_FEATURES` to disable any supported user features when login type is not plain - In general, this is necessary for SSO implementations to avoid inconsistencies between the external account management and the linked account - Adds helper functions to encourage correct use
1 parent 849eee8 commit 59d4aad

File tree

9 files changed

+84
-16
lines changed

9 files changed

+84
-16
lines changed

custom/conf/app.example.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,11 @@ LEVEL = Info
14851485
;; - manage_ssh_keys: a user cannot configure ssh keys
14861486
;; - manage_gpg_keys: a user cannot configure gpg keys
14871487
;USER_DISABLED_FEATURES =
1488+
;; 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.
1489+
;; - deletion: a user cannot delete their own account
1490+
;; - manage_ssh_keys: a user cannot configure ssh keys
1491+
;; - manage_gpg_keys: a user cannot configure gpg keys
1492+
;;EXTERNAL_USER_DISABLE_FEATURES =
14881493

14891494
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
14901495
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@ And the following unique queues:
522522
- `deletion`: User cannot delete their own account.
523523
- `manage_ssh_keys`: User cannot configure ssh keys.
524524
- `manage_gpg_keys`: User cannot configure gpg keys.
525+
- `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.
526+
- `deletion`: User cannot delete their own account.
527+
- `manage_ssh_keys`: User cannot configure ssh keys.
528+
- `manage_gpg_keys`: User cannot configure gpg keys.
525529

526530
## Security (`security`)
527531

models/user/user.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,3 +1232,21 @@ func GetOrderByName() string {
12321232
}
12331233
return "name"
12341234
}
1235+
1236+
// IsFeatureDisabledWithLoginType checks if a user feature is disabled, taking into account the login type of the
1237+
// user if applicable
1238+
func IsFeatureDisabledWithLoginType(user *User, feature string) bool {
1239+
// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
1240+
return (user != nil && user.LoginType > auth.Plain && setting.Admin.ExternalUserDisableFeatures.Contains(feature)) ||
1241+
setting.Admin.UserDisabledFeatures.Contains(feature)
1242+
}
1243+
1244+
// DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type
1245+
// of the user if applicable
1246+
func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
1247+
// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
1248+
if user != nil && user.LoginType > auth.Plain {
1249+
return &setting.Admin.ExternalUserDisableFeatures
1250+
}
1251+
return &setting.Admin.UserDisabledFeatures
1252+
}

models/user/user_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"code.gitea.io/gitea/models/unittest"
1717
user_model "code.gitea.io/gitea/models/user"
1818
"code.gitea.io/gitea/modules/auth/password/hash"
19+
"code.gitea.io/gitea/modules/container"
1920
"code.gitea.io/gitea/modules/optional"
2021
"code.gitea.io/gitea/modules/setting"
2122
"code.gitea.io/gitea/modules/structs"
@@ -526,3 +527,37 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
526527
}
527528
}
528529
}
530+
531+
func TestDisabledUserFeatures(t *testing.T) {
532+
assert.NoError(t, unittest.PrepareTestDatabase())
533+
534+
testValues := container.SetOf(setting.UserFeatureDeletion,
535+
setting.UserFeatureManageSSHKeys,
536+
setting.UserFeatureManageGPGKeys)
537+
538+
oldSetting := setting.Admin.ExternalUserDisableFeatures
539+
defer func() {
540+
setting.Admin.ExternalUserDisableFeatures = oldSetting
541+
}()
542+
setting.Admin.ExternalUserDisableFeatures = testValues
543+
544+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
545+
546+
assert.Len(t, setting.Admin.UserDisabledFeatures.Values(), 0)
547+
548+
// no features should be disabled with a plain login type
549+
assert.LessOrEqual(t, user.LoginType, auth.Plain)
550+
assert.Len(t, user_model.DisabledFeaturesWithLoginType(user).Values(), 0)
551+
for _, f := range testValues.Values() {
552+
assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f))
553+
}
554+
555+
// check disabled features with external login type
556+
user.LoginType = auth.OAuth2
557+
558+
// all features should be disabled
559+
assert.NotEmpty(t, user_model.DisabledFeaturesWithLoginType(user).Values())
560+
for _, f := range testValues.Values() {
561+
assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f))
562+
}
563+
}

modules/setting/admin.go

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

44
package setting
55

6-
import "code.gitea.io/gitea/modules/container"
6+
import (
7+
"code.gitea.io/gitea/modules/container"
8+
)
79

810
// Admin settings
911
var Admin struct {
10-
DisableRegularOrgCreation bool
11-
DefaultEmailNotification string
12-
UserDisabledFeatures container.Set[string]
12+
DisableRegularOrgCreation bool
13+
DefaultEmailNotification string
14+
UserDisabledFeatures container.Set[string]
15+
ExternalUserDisableFeatures container.Set[string]
1316
}
1417

1518
func loadAdminFrom(rootCfg ConfigProvider) {
1619
sec := rootCfg.Section("admin")
1720
Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
1821
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
1922
Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
23+
Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...)
2024
}
2125

2226
const (

routers/api/v1/user/gpg_key.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
asymkey_model "code.gitea.io/gitea/models/asymkey"
1212
"code.gitea.io/gitea/models/db"
13+
user_model "code.gitea.io/gitea/models/user"
1314
"code.gitea.io/gitea/modules/setting"
1415
api "code.gitea.io/gitea/modules/structs"
1516
"code.gitea.io/gitea/modules/web"
@@ -133,7 +134,7 @@ func GetGPGKey(ctx *context.APIContext) {
133134

134135
// CreateUserGPGKey creates new GPG key to given user by ID.
135136
func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
136-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
137+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
137138
ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
138139
return
139140
}
@@ -274,7 +275,7 @@ func DeleteGPGKey(ctx *context.APIContext) {
274275
// "404":
275276
// "$ref": "#/responses/notFound"
276277

277-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
278+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
278279
ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
279280
return
280281
}

routers/api/v1/user/key.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func GetPublicKey(ctx *context.APIContext) {
199199

200200
// CreateUserPublicKey creates new public key to given user by ID.
201201
func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) {
202-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
202+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
203203
ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
204204
return
205205
}
@@ -269,7 +269,7 @@ func DeletePublicKey(ctx *context.APIContext) {
269269
// "404":
270270
// "$ref": "#/responses/notFound"
271271

272-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
272+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
273273
ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
274274
return
275275
}

routers/web/user/setting/account.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ func DeleteEmail(ctx *context.Context) {
235235

236236
// DeleteAccount render user suicide page and response for delete user himself
237237
func DeleteAccount(ctx *context.Context) {
238-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureDeletion) {
238+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureDeletion) {
239239
ctx.Error(http.StatusNotFound)
240240
return
241241
}
@@ -319,7 +319,7 @@ func loadAccountData(ctx *context.Context) {
319319
ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
320320
ctx.Data["ActivationsPending"] = pendingActivation
321321
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
322-
ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures
322+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
323323

324324
if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
325325
ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()

routers/web/user/setting/keys.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
asymkey_model "code.gitea.io/gitea/models/asymkey"
1212
"code.gitea.io/gitea/models/db"
13+
user_model "code.gitea.io/gitea/models/user"
1314
"code.gitea.io/gitea/modules/base"
1415
"code.gitea.io/gitea/modules/setting"
1516
"code.gitea.io/gitea/modules/web"
@@ -78,7 +79,7 @@ func KeysPost(ctx *context.Context) {
7879
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
7980
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
8081
case "gpg":
81-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
82+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
8283
ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
8384
return
8485
}
@@ -159,7 +160,7 @@ func KeysPost(ctx *context.Context) {
159160
ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
160161
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
161162
case "ssh":
162-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
163+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
163164
ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
164165
return
165166
}
@@ -203,7 +204,7 @@ func KeysPost(ctx *context.Context) {
203204
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
204205
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
205206
case "verify_ssh":
206-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
207+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
207208
ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
208209
return
209210
}
@@ -240,7 +241,7 @@ func KeysPost(ctx *context.Context) {
240241
func DeleteKey(ctx *context.Context) {
241242
switch ctx.FormString("type") {
242243
case "gpg":
243-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
244+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
244245
ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
245246
return
246247
}
@@ -250,7 +251,7 @@ func DeleteKey(ctx *context.Context) {
250251
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
251252
}
252253
case "ssh":
253-
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
254+
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
254255
ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
255256
return
256257
}
@@ -333,5 +334,5 @@ func loadKeysData(ctx *context.Context) {
333334

334335
ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
335336
ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
336-
ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures
337+
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
337338
}

0 commit comments

Comments
 (0)