mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -1485,6 +1485,11 @@ LEVEL = Info | |||||||
| ;; - manage_ssh_keys: a user cannot configure ssh keys | ;; - manage_ssh_keys: a user cannot configure ssh keys | ||||||
| ;; - manage_gpg_keys: a user cannot configure gpg keys | ;; - manage_gpg_keys: a user cannot configure gpg keys | ||||||
| ;USER_DISABLED_FEATURES = | ;USER_DISABLED_FEATURES = | ||||||
|  | ;; 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. | ||||||
|  | ;; - deletion: a user cannot delete their own account | ||||||
|  | ;; - manage_ssh_keys: a user cannot configure ssh keys | ||||||
|  | ;; - manage_gpg_keys: a user cannot configure gpg keys | ||||||
|  | ;;EXTERNAL_USER_DISABLE_FEATURES = | ||||||
|  |  | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
|   | |||||||
| @@ -522,6 +522,10 @@ And the following unique queues: | |||||||
|   - `deletion`: User cannot delete their own account. |   - `deletion`: User cannot delete their own account. | ||||||
|   - `manage_ssh_keys`: User cannot configure ssh keys. |   - `manage_ssh_keys`: User cannot configure ssh keys. | ||||||
|   - `manage_gpg_keys`: User cannot configure gpg keys. |   - `manage_gpg_keys`: User cannot configure gpg keys. | ||||||
|  | - `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. | ||||||
|  |   - `deletion`: User cannot delete their own account. | ||||||
|  |   - `manage_ssh_keys`: User cannot configure ssh keys. | ||||||
|  |   - `manage_gpg_keys`: User cannot configure gpg keys. | ||||||
|  |  | ||||||
| ## Security (`security`) | ## Security (`security`) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1232,3 +1232,21 @@ func GetOrderByName() string { | |||||||
| 	} | 	} | ||||||
| 	return "name" | 	return "name" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // IsFeatureDisabledWithLoginType checks if a user feature is disabled, taking into account the login type of the | ||||||
|  | // user if applicable | ||||||
|  | func IsFeatureDisabledWithLoginType(user *User, feature string) bool { | ||||||
|  | 	// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType | ||||||
|  | 	return (user != nil && user.LoginType > auth.Plain && setting.Admin.ExternalUserDisableFeatures.Contains(feature)) || | ||||||
|  | 		setting.Admin.UserDisabledFeatures.Contains(feature) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type | ||||||
|  | // of the user if applicable | ||||||
|  | func DisabledFeaturesWithLoginType(user *User) *container.Set[string] { | ||||||
|  | 	// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType | ||||||
|  | 	if user != nil && user.LoginType > auth.Plain { | ||||||
|  | 		return &setting.Admin.ExternalUserDisableFeatures | ||||||
|  | 	} | ||||||
|  | 	return &setting.Admin.UserDisabledFeatures | ||||||
|  | } | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/auth/password/hash" | 	"code.gitea.io/gitea/modules/auth/password/hash" | ||||||
|  | 	"code.gitea.io/gitea/modules/container" | ||||||
| 	"code.gitea.io/gitea/modules/optional" | 	"code.gitea.io/gitea/modules/optional" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/structs" | 	"code.gitea.io/gitea/modules/structs" | ||||||
| @@ -526,3 +527,37 @@ func Test_NormalizeUserFromEmail(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestDisabledUserFeatures(t *testing.T) { | ||||||
|  | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
|  | 	testValues := container.SetOf(setting.UserFeatureDeletion, | ||||||
|  | 		setting.UserFeatureManageSSHKeys, | ||||||
|  | 		setting.UserFeatureManageGPGKeys) | ||||||
|  |  | ||||||
|  | 	oldSetting := setting.Admin.ExternalUserDisableFeatures | ||||||
|  | 	defer func() { | ||||||
|  | 		setting.Admin.ExternalUserDisableFeatures = oldSetting | ||||||
|  | 	}() | ||||||
|  | 	setting.Admin.ExternalUserDisableFeatures = testValues | ||||||
|  |  | ||||||
|  | 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) | ||||||
|  |  | ||||||
|  | 	assert.Len(t, setting.Admin.UserDisabledFeatures.Values(), 0) | ||||||
|  |  | ||||||
|  | 	// no features should be disabled with a plain login type | ||||||
|  | 	assert.LessOrEqual(t, user.LoginType, auth.Plain) | ||||||
|  | 	assert.Len(t, user_model.DisabledFeaturesWithLoginType(user).Values(), 0) | ||||||
|  | 	for _, f := range testValues.Values() { | ||||||
|  | 		assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check disabled features with external login type | ||||||
|  | 	user.LoginType = auth.OAuth2 | ||||||
|  |  | ||||||
|  | 	// all features should be disabled | ||||||
|  | 	assert.NotEmpty(t, user_model.DisabledFeaturesWithLoginType(user).Values()) | ||||||
|  | 	for _, f := range testValues.Values() { | ||||||
|  | 		assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -3,13 +3,16 @@ | |||||||
|  |  | ||||||
| package setting | package setting | ||||||
|  |  | ||||||
| import "code.gitea.io/gitea/modules/container" | import ( | ||||||
|  | 	"code.gitea.io/gitea/modules/container" | ||||||
|  | ) | ||||||
|  |  | ||||||
| // Admin settings | // Admin settings | ||||||
| var Admin struct { | var Admin struct { | ||||||
| 	DisableRegularOrgCreation bool | 	DisableRegularOrgCreation   bool | ||||||
| 	DefaultEmailNotification  string | 	DefaultEmailNotification    string | ||||||
| 	UserDisabledFeatures      container.Set[string] | 	UserDisabledFeatures        container.Set[string] | ||||||
|  | 	ExternalUserDisableFeatures container.Set[string] | ||||||
| } | } | ||||||
|  |  | ||||||
| func loadAdminFrom(rootCfg ConfigProvider) { | func loadAdminFrom(rootCfg ConfigProvider) { | ||||||
| @@ -17,6 +20,7 @@ func loadAdminFrom(rootCfg ConfigProvider) { | |||||||
| 	Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false) | 	Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false) | ||||||
| 	Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled") | 	Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled") | ||||||
| 	Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...) | 	Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...) | ||||||
|  | 	Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...) | ||||||
| } | } | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ( | |||||||
|  |  | ||||||
| 	asymkey_model "code.gitea.io/gitea/models/asymkey" | 	asymkey_model "code.gitea.io/gitea/models/asymkey" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| @@ -133,7 +134,7 @@ func GetGPGKey(ctx *context.APIContext) { | |||||||
|  |  | ||||||
| // CreateUserGPGKey creates new GPG key to given user by ID. | // CreateUserGPGKey creates new GPG key to given user by ID. | ||||||
| func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { | func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { | ||||||
| 	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) { | 	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { | ||||||
| 		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) | 		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -274,7 +275,7 @@ func DeleteGPGKey(ctx *context.APIContext) { | |||||||
| 	//   "404": | 	//   "404": | ||||||
| 	//     "$ref": "#/responses/notFound" | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
| 	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) { | 	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { | ||||||
| 		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) | 		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -199,7 +199,7 @@ func GetPublicKey(ctx *context.APIContext) { | |||||||
|  |  | ||||||
| // CreateUserPublicKey creates new public key to given user by ID. | // CreateUserPublicKey creates new public key to given user by ID. | ||||||
| func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) { | func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) { | ||||||
| 	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { | 	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { | ||||||
| 		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | 		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -269,7 +269,7 @@ func DeletePublicKey(ctx *context.APIContext) { | |||||||
| 	//   "404": | 	//   "404": | ||||||
| 	//     "$ref": "#/responses/notFound" | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
| 	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { | 	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { | ||||||
| 		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | 		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -235,7 +235,7 @@ func DeleteEmail(ctx *context.Context) { | |||||||
|  |  | ||||||
| // DeleteAccount render user suicide page and response for delete user himself | // DeleteAccount render user suicide page and response for delete user himself | ||||||
| func DeleteAccount(ctx *context.Context) { | func DeleteAccount(ctx *context.Context) { | ||||||
| 	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureDeletion) { | 	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureDeletion) { | ||||||
| 		ctx.Error(http.StatusNotFound) | 		ctx.Error(http.StatusNotFound) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -319,7 +319,7 @@ func loadAccountData(ctx *context.Context) { | |||||||
| 	ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference | 	ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference | ||||||
| 	ctx.Data["ActivationsPending"] = pendingActivation | 	ctx.Data["ActivationsPending"] = pendingActivation | ||||||
| 	ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm | 	ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm | ||||||
| 	ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures | 	ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) | ||||||
|  |  | ||||||
| 	if setting.Service.UserDeleteWithCommentsMaxTime != 0 { | 	if setting.Service.UserDeleteWithCommentsMaxTime != 0 { | ||||||
| 		ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String() | 		ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String() | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ( | |||||||
|  |  | ||||||
| 	asymkey_model "code.gitea.io/gitea/models/asymkey" | 	asymkey_model "code.gitea.io/gitea/models/asymkey" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| @@ -78,7 +79,7 @@ func KeysPost(ctx *context.Context) { | |||||||
| 		ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content)) | 		ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content)) | ||||||
| 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys") | 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys") | ||||||
| 	case "gpg": | 	case "gpg": | ||||||
| 		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) { | 		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { | ||||||
| 			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) | 			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| @@ -159,7 +160,7 @@ func KeysPost(ctx *context.Context) { | |||||||
| 		ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID)) | 		ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID)) | ||||||
| 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys") | 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys") | ||||||
| 	case "ssh": | 	case "ssh": | ||||||
| 		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { | 		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { | ||||||
| 			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | 			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| @@ -203,7 +204,7 @@ func KeysPost(ctx *context.Context) { | |||||||
| 		ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title)) | 		ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title)) | ||||||
| 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys") | 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys") | ||||||
| 	case "verify_ssh": | 	case "verify_ssh": | ||||||
| 		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { | 		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { | ||||||
| 			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | 			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| @@ -240,7 +241,7 @@ func KeysPost(ctx *context.Context) { | |||||||
| func DeleteKey(ctx *context.Context) { | func DeleteKey(ctx *context.Context) { | ||||||
| 	switch ctx.FormString("type") { | 	switch ctx.FormString("type") { | ||||||
| 	case "gpg": | 	case "gpg": | ||||||
| 		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) { | 		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { | ||||||
| 			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) | 			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| @@ -250,7 +251,7 @@ func DeleteKey(ctx *context.Context) { | |||||||
| 			ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success")) | 			ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success")) | ||||||
| 		} | 		} | ||||||
| 	case "ssh": | 	case "ssh": | ||||||
| 		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { | 		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { | ||||||
| 			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | 			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| @@ -333,5 +334,5 @@ func loadKeysData(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg") | 	ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg") | ||||||
| 	ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh") | 	ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh") | ||||||
| 	ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures | 	ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user