From bb885d6036e75017e6cb479b28a4d86ebe8ee4b4 Mon Sep 17 00:00:00 2001 From: Rowan Bohde Date: Mon, 1 Jul 2024 12:19:31 -0500 Subject: [PATCH 1/3] Allow disabling authentication related user features 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 --- custom/conf/app.example.ini | 8 +++++-- .../config-cheat-sheet.en-us.md | 8 +++++-- models/user/user.go | 10 ++++---- modules/container/set.go | 23 +++++++++++++++---- modules/setting/admin.go | 10 ++++---- routers/web/repo/setting/secrets.go | 2 ++ routers/web/user/setting/account.go | 15 ++++++++++++ routers/web/user/setting/applications.go | 3 +++ routers/web/user/setting/keys.go | 7 ++++++ routers/web/user/setting/packages.go | 6 +++++ routers/web/user/setting/profile.go | 6 +++++ routers/web/user/setting/security/2fa.go | 21 +++++++++++++++++ routers/web/user/setting/security/openid.go | 15 ++++++++++++ routers/web/user/setting/security/security.go | 12 ++++++++++ routers/web/user/setting/security/webauthn.go | 16 +++++++++++++ routers/web/user/setting/webhooks.go | 2 ++ templates/user/settings/account.tmpl | 9 +++++++- templates/user/settings/navbar.tmpl | 6 +++++ .../user/settings/security/security.tmpl | 6 +++++ 19 files changed, 168 insertions(+), 17 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 9196180d81e3c..f522b9da28abc 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1488,15 +1488,19 @@ LEVEL = Info ;; ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled -;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future +;; Disabled features for users could be "deletion", "manage_ssh_keys", "manage_gpg_keys", "manage_mfa", "manage_credentials" more features can be disabled in future ;; - 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 +;; - manage_mfa: a user cannot configure mfa devices +;; - manage_credentials: a user cannot configure emails, passwords, or openid ;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. +;; 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. ;; - 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 +;; - manage_mfa: a user cannot configure mfa devices +;; - manage_credentials: a user cannot configure emails, passwords, or openid ;;EXTERNAL_USER_DISABLE_FEATURES = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 0c15a866b6e88..b38e53d4ab213 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -517,14 +517,18 @@ And the following unique queues: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations. -- `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. +- `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. - `deletion`: User cannot delete their own account. - `manage_ssh_keys`: User cannot configure ssh 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. + - `manage_mfa`: a User cannot configure mfa devices. + - `manage_credentials`: a user cannot configure emails, passwords, or openid +- `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. - `deletion`: User cannot delete their own account. - `manage_ssh_keys`: User cannot configure ssh keys. - `manage_gpg_keys`: User cannot configure gpg keys. + - `manage_mfa`: a User cannot configure mfa devices. + - `manage_credentials`: a user cannot configure emails, passwords, or openid ## Security (`security`) diff --git a/models/user/user.go b/models/user/user.go index 23637f4616288..5e89e7a3e443a 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1233,12 +1233,14 @@ func GetOrderByName() string { return "name" } -// IsFeatureDisabledWithLoginType checks if a user feature is disabled, taking into account the login type of the +// IsFeatureDisabledWithLoginType checks if a user features are disabled, taking into account the login type of the // user if applicable -func IsFeatureDisabledWithLoginType(user *User, feature string) bool { +func IsFeatureDisabledWithLoginType(user *User, features ...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) + if user != nil && user.LoginType > auth.Plain { + return setting.Admin.ExternalUserDisableFeatures.Contains(features...) + } + return setting.Admin.UserDisabledFeatures.Contains(features...) } // DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type diff --git a/modules/container/set.go b/modules/container/set.go index 15779983fd1ea..adb77dcac7a7a 100644 --- a/modules/container/set.go +++ b/modules/container/set.go @@ -3,6 +3,8 @@ package container +import "maps" + type Set[T comparable] map[T]struct{} // SetOf creates a set and adds the specified elements to it. @@ -29,11 +31,15 @@ func (s Set[T]) AddMultiple(values ...T) { } } -// Contains determines whether a set contains the specified element. +// Contains determines whether a set contains the specified elements. // Returns true if the set contains the specified element; otherwise, false. -func (s Set[T]) Contains(value T) bool { - _, has := s[value] - return has +func (s Set[T]) Contains(values ...T) bool { + ret := true + for _, value := range values { + _, has := s[value] + ret = ret && has + } + return ret } // Remove removes the specified element. @@ -54,3 +60,12 @@ func (s Set[T]) Values() []T { } return keys } + +// Union constructs a new set that is the union of the provided sets +func (s Set[T]) Union(sets ...Set[T]) Set[T] { + newSet := maps.Clone(s) + for i := range sets { + maps.Copy(newSet, sets[i]) + } + return newSet +} diff --git a/modules/setting/admin.go b/modules/setting/admin.go index 8aebc76154d30..ca4e9b1d5835b 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -20,11 +20,13 @@ func loadAdminFrom(rootCfg ConfigProvider) { Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false) Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled") Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...) - Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...) + Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...).Union(Admin.UserDisabledFeatures) } const ( - UserFeatureDeletion = "deletion" - UserFeatureManageSSHKeys = "manage_ssh_keys" - UserFeatureManageGPGKeys = "manage_gpg_keys" + UserFeatureDeletion = "deletion" + UserFeatureManageSSHKeys = "manage_ssh_keys" + UserFeatureManageGPGKeys = "manage_gpg_keys" + UserFeatureManageMFA = "manage_mfa" + UserFeatureManageCredentials = "manage_credentials" ) diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index d4d56bfc57e31..df1172934437d 100644 --- a/routers/web/repo/setting/secrets.go +++ b/routers/web/repo/setting/secrets.go @@ -7,6 +7,7 @@ import ( "errors" "net/http" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" shared "code.gitea.io/gitea/routers/web/shared/secrets" @@ -74,6 +75,7 @@ func Secrets(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("actions.actions") ctx.Data["PageType"] = "secrets" ctx.Data["PageIsSharedSettingsSecrets"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) sCtx, err := getSecretsCtx(ctx) if err != nil { diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 8ea7548e51dae..9a685f0d632c2 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -6,6 +6,7 @@ package setting import ( "errors" + "fmt" "net/http" "time" @@ -45,6 +46,11 @@ func Account(ctx *context.Context) { // AccountPost response for change user's password func AccountPost(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) { + ctx.NotFound("Not Found", fmt.Errorf("password setting is not allowed to be changed")) + return + } + form := web.GetForm(ctx).(*forms.ChangePasswordForm) ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true @@ -89,6 +95,11 @@ func AccountPost(ctx *context.Context) { // EmailPost response for change user's email func EmailPost(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) { + ctx.NotFound("Not Found", fmt.Errorf("emails are not allowed to be changed")) + return + } + form := web.GetForm(ctx).(*forms.AddEmailForm) ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true @@ -216,6 +227,10 @@ func EmailPost(ctx *context.Context) { // DeleteEmail response for delete user's email func DeleteEmail(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) { + ctx.NotFound("Not Found", fmt.Errorf("emails are not allowed to be changed")) + return + } email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, ctx.FormInt64("id")) if err != nil || email == nil { ctx.ServerError("GetEmailAddressByID", err) diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index e3822ca988c52..356c2ea5de37f 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -9,6 +9,7 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "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/setting" "code.gitea.io/gitea/modules/web" @@ -24,6 +25,7 @@ const ( func Applications(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.applications") ctx.Data["PageIsSettingsApplications"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) loadApplicationsData(ctx) @@ -35,6 +37,7 @@ func ApplicationsPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.NewAccessTokenForm) ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsApplications"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) if ctx.HasError() { loadApplicationsData(ctx) diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index 9e969e045dd1d..c492715fb5f71 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -25,11 +25,17 @@ const ( // Keys render user's SSH/GPG public keys page func Keys(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) { + ctx.NotFound("Not Found", fmt.Errorf("keys setting is not allowed to be changed")) + return + } + ctx.Data["Title"] = ctx.Tr("settings.ssh_gpg_keys") ctx.Data["PageIsSettingsKeys"] = true ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) loadKeysData(ctx) @@ -44,6 +50,7 @@ func KeysPost(ctx *context.Context) { ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) if ctx.HasError() { loadKeysData(ctx) diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go index 4132659495394..50521c11c0e7e 100644 --- a/routers/web/user/setting/packages.go +++ b/routers/web/user/setting/packages.go @@ -25,6 +25,7 @@ const ( func Packages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetPackagesContext(ctx, ctx.Doer) @@ -34,6 +35,7 @@ func Packages(ctx *context.Context) { func PackagesRuleAdd(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetRuleAddContext(ctx) @@ -43,6 +45,7 @@ func PackagesRuleAdd(ctx *context.Context) { func PackagesRuleEdit(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetRuleEditContext(ctx, ctx.Doer) @@ -52,6 +55,7 @@ func PackagesRuleEdit(ctx *context.Context) { func PackagesRuleAddPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsPackages"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.PerformRuleAddPost( ctx, @@ -64,6 +68,7 @@ func PackagesRuleAddPost(ctx *context.Context) { func PackagesRuleEditPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.PerformRuleEditPost( ctx, @@ -76,6 +81,7 @@ func PackagesRuleEditPost(ctx *context.Context) { func PackagesRulePreview(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetRulePreviewContext(ctx, ctx.Doer) diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index e5ff8570cf955..554f6cd6ce3f9 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -48,6 +48,8 @@ func Profile(ctx *context.Context) { ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) + ctx.HTML(http.StatusOK, tplSettingsProfile) } @@ -57,6 +59,7 @@ func ProfilePost(ctx *context.Context) { ctx.Data["PageIsSettingsProfile"] = true ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) if ctx.HasError() { ctx.HTML(http.StatusOK, tplSettingsProfile) @@ -182,6 +185,7 @@ func DeleteAvatar(ctx *context.Context) { func Organization(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.organization") ctx.Data["PageIsSettingsOrganization"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) opts := organization.FindOrgOptions{ ListOptions: db.ListOptions{ @@ -213,6 +217,7 @@ func Organization(ctx *context.Context) { func Repos(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.repos") ctx.Data["PageIsSettingsRepos"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories @@ -326,6 +331,7 @@ func Appearance(ctx *context.Context) { allThemes = append([]string{setting.UI.DefaultTheme}, allThemes...) // move the default theme to the top } ctx.Data["AllThemes"] = allThemes + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) var hiddenCommentTypes *big.Int val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go index cd09102369868..74ffe169dbde2 100644 --- a/routers/web/user/setting/security/2fa.go +++ b/routers/web/user/setting/security/2fa.go @@ -13,6 +13,7 @@ import ( "strings" "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" @@ -25,6 +26,11 @@ import ( // RegenerateScratchTwoFactor regenerates the user's 2FA scratch code. func RegenerateScratchTwoFactor(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) { + ctx.Error(http.StatusNotFound) + return + } + ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsSecurity"] = true @@ -55,6 +61,11 @@ func RegenerateScratchTwoFactor(ctx *context.Context) { // DisableTwoFactor deletes the user's 2FA settings. func DisableTwoFactor(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) { + ctx.Error(http.StatusNotFound) + return + } + ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsSecurity"] = true @@ -142,6 +153,11 @@ func twofaGenerateSecretAndQr(ctx *context.Context) bool { // EnrollTwoFactor shows the page where the user can enroll into 2FA. func EnrollTwoFactor(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) { + ctx.Error(http.StatusNotFound) + return + } + ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsSecurity"] = true @@ -167,6 +183,11 @@ func EnrollTwoFactor(ctx *context.Context) { // EnrollTwoFactorPost handles enrolling the user into 2FA. func EnrollTwoFactorPost(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) { + ctx.Error(http.StatusNotFound) + return + } + form := web.GetForm(ctx).(*forms.TwoFactorAuthForm) ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsSecurity"] = true diff --git a/routers/web/user/setting/security/openid.go b/routers/web/user/setting/security/openid.go index 8f788e17356ca..30eb6f63f89a0 100644 --- a/routers/web/user/setting/security/openid.go +++ b/routers/web/user/setting/security/openid.go @@ -17,6 +17,11 @@ import ( // OpenIDPost response for change user's openid func OpenIDPost(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) { + ctx.Error(http.StatusNotFound) + return + } + form := web.GetForm(ctx).(*forms.AddOpenIDForm) ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsSecurity"] = true @@ -105,6 +110,11 @@ func settingsOpenIDVerify(ctx *context.Context) { // DeleteOpenID response for delete user's openid func DeleteOpenID(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) { + ctx.Error(http.StatusNotFound) + return + } + if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil { ctx.ServerError("DeleteUserOpenID", err) return @@ -117,6 +127,11 @@ func DeleteOpenID(ctx *context.Context) { // ToggleOpenIDVisibility response for toggle visibility of user's openid func ToggleOpenIDVisibility(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) { + ctx.Error(http.StatusNotFound) + return + } + if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id")); err != nil { ctx.ServerError("ToggleUserOpenIDVisibility", err) return diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go index 8d6859ab87da9..b44cb4dd499fa 100644 --- a/routers/web/user/setting/security/security.go +++ b/routers/web/user/setting/security/security.go @@ -25,6 +25,12 @@ const ( // Security render change user's password page and 2FA func Security(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, + setting.UserFeatureManageMFA, setting.UserFeatureManageCredentials) { + ctx.Error(http.StatusNotFound) + return + } + ctx.Data["Title"] = ctx.Tr("settings.security") ctx.Data["PageIsSettingsSecurity"] = true @@ -40,6 +46,11 @@ func Security(ctx *context.Context) { // DeleteAccountLink delete a single account link func DeleteAccountLink(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) { + ctx.Error(http.StatusNotFound) + return + } + id := ctx.FormInt64("id") if id <= 0 { ctx.Flash.Error("Account link id is not given") @@ -145,4 +156,5 @@ func loadSecurityData(ctx *context.Context) { return } ctx.Data["OpenIDs"] = openid + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) } diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go index 1b8d0171f5633..aafc2f2a648ca 100644 --- a/routers/web/user/setting/security/webauthn.go +++ b/routers/web/user/setting/security/webauthn.go @@ -10,6 +10,7 @@ import ( "time" "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" wa "code.gitea.io/gitea/modules/auth/webauthn" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -23,6 +24,11 @@ import ( // WebAuthnRegister initializes the webauthn registration procedure func WebAuthnRegister(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) { + ctx.Error(http.StatusNotFound) + return + } + form := web.GetForm(ctx).(*forms.WebauthnRegistrationForm) if form.Name == "" { // Set name to the hexadecimal of the current time @@ -64,6 +70,11 @@ func WebAuthnRegister(ctx *context.Context) { // WebauthnRegisterPost receives the response of the security key func WebauthnRegisterPost(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) { + ctx.Error(http.StatusNotFound) + return + } + name, ok := ctx.Session.Get("webauthnName").(string) if !ok || name == "" { ctx.ServerError("Get webauthnName", errors.New("no webauthnName")) @@ -113,6 +124,11 @@ func WebauthnRegisterPost(ctx *context.Context) { // WebauthnDelete deletes an security key by id func WebauthnDelete(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) { + ctx.Error(http.StatusNotFound) + return + } + form := web.GetForm(ctx).(*forms.WebauthnDeleteForm) if _, err := auth.DeleteCredential(ctx, form.ID, ctx.Doer.ID); err != nil { ctx.ServerError("GetWebAuthnCredentialByID", err) diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go index 4423b627814ab..3732ca27c06f1 100644 --- a/routers/web/user/setting/webhooks.go +++ b/routers/web/user/setting/webhooks.go @@ -7,6 +7,7 @@ import ( "net/http" "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" @@ -24,6 +25,7 @@ func Webhooks(ctx *context.Context) { ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/hooks" ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks" ctx.Data["Description"] = ctx.Tr("settings.hooks.desc") + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID}) if err != nil { diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index 2aaf8535d1200..a7b24640693c6 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -4,7 +4,7 @@ {{ctx.Locale.Tr "settings.password"}}
- {{if or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2)}} + {{if and (not ($.UserDisabledFeatures.Contains "manage_credentials")) (or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2))}}
{{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} @@ -35,6 +35,7 @@ {{end}}
+ {{if not (and ($.UserDisabledFeatures.Contains "manage_credentials") (not $.EnableNotifyMail))}}

{{ctx.Locale.Tr "settings.manage_emails"}}

@@ -63,6 +64,7 @@ {{end}} + {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}} {{range .Emails}}
{{if not .IsPrimary}} @@ -109,8 +111,12 @@
{{end}} + {{end}} + {{end}} + + {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}
{{.CsrfTokenHtml}} @@ -127,6 +133,7 @@
{{ctx.Locale.Tr "settings.can_not_add_email_activations_pending"}}
{{end}}
+ {{end}} {{if not ($.UserDisabledFeatures.Contains "deletion")}}

diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index c360944814a45..0cf8c5bab544b 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -4,24 +4,30 @@ {{ctx.Locale.Tr "settings.profile"}} + {{if not (and ($.UserDisabledFeatures.Contains "manage_credentials") (not $.EnablenNotifyMail))}} {{ctx.Locale.Tr "settings.account"}} + {{end}} {{ctx.Locale.Tr "settings.appearance"}} + {{if not ($.UserDisabledFeatures.Contains "manage_mfa" "manage_credentials")}} {{ctx.Locale.Tr "settings.security"}} + {{end}} {{ctx.Locale.Tr "user.block.list"}} {{ctx.Locale.Tr "settings.applications"}} + {{if not ($.UserDisabledFeatures.Contains "manage_ssh_keys" "manage_gpg_keys")}} {{ctx.Locale.Tr "settings.ssh_gpg_keys"}} + {{end}} {{if .EnableActions}}
{{ctx.Locale.Tr "actions.actions"}} diff --git a/templates/user/settings/security/security.tmpl b/templates/user/settings/security/security.tmpl index aee0456b8f556..d9403cfc26147 100644 --- a/templates/user/settings/security/security.tmpl +++ b/templates/user/settings/security/security.tmpl @@ -1,11 +1,17 @@ {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings security")}} + {{if not ($.UserDisabledFeatures.Contains "manage_mfa" "manage_credentials")}}
+ {{if not ($.UserDisabledFeatures.Contains "manage_mfa")}} {{template "user/settings/security/twofa" .}} {{template "user/settings/security/webauthn" .}} + {{end}} + {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}} {{template "user/settings/security/accountlinks" .}} {{if .EnableOpenIDSignIn}} {{template "user/settings/security/openid" .}} {{end}} + {{end}}
+ {{end}} {{template "user/settings/layout_footer" .}} From 3df7358bdb6b4b9c5271bbfe0c43ce8934cab190 Mon Sep 17 00:00:00 2001 From: Rowan Bohde Date: Mon, 8 Jul 2024 10:58:10 -0500 Subject: [PATCH 2/3] ensure we return a 404 on account page when it's not usable add integration tests for navbar and all disabled user settings --- routers/web/user/setting/account.go | 11 + templates/user/settings/navbar.tmpl | 2 +- tests/integration/user_settings_test.go | 394 ++++++++++++++++++++++++ 3 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 tests/integration/user_settings_test.go diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 9a685f0d632c2..563f39f0c8ac9 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -34,6 +34,11 @@ const ( // Account renders change user's password, user's email and user suicide page func Account(ctx *context.Context) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials, setting.UserFeatureDeletion) && !setting.Service.EnableNotifyMail { + ctx.NotFound("Not Found", fmt.Errorf("account setting are not allowed to be changed")) + return + } + ctx.Data["Title"] = ctx.Tr("settings.account") ctx.Data["PageIsSettingsAccount"] = true ctx.Data["Email"] = ctx.Doer.Email @@ -54,6 +59,8 @@ func AccountPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ChangePasswordForm) ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true + ctx.Data["Email"] = ctx.Doer.Email + ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail if ctx.HasError() { loadAccountData(ctx) @@ -103,6 +110,8 @@ func EmailPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.AddEmailForm) ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true + ctx.Data["Email"] = ctx.Doer.Email + ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail // Make email address primary. if ctx.FormString("_method") == "PRIMARY" { @@ -256,6 +265,8 @@ func DeleteAccount(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true + ctx.Data["Email"] = ctx.Doer.Email + ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil { switch { diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index 0cf8c5bab544b..c6c15512abdab 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -4,7 +4,7 @@ {{ctx.Locale.Tr "settings.profile"}} - {{if not (and ($.UserDisabledFeatures.Contains "manage_credentials") (not $.EnablenNotifyMail))}} + {{if not (and ($.UserDisabledFeatures.Contains "manage_credentials" "deletion") (not $.EnableNotifyMail))}} {{ctx.Locale.Tr "settings.account"}} diff --git a/tests/integration/user_settings_test.go b/tests/integration/user_settings_test.go new file mode 100644 index 0000000000000..d4b7b075df30d --- /dev/null +++ b/tests/integration/user_settings_test.go @@ -0,0 +1,394 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/tests" +) + +// Validate that each navbar setting is correct. This checks that the +// appropriate context is passed everywhere the navbar is rendered +func assertNavbar(t *testing.T, doc *HTMLDoc) { + // Only show the account page if users can change their email notifications, delete themselves, or manage credentials + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureDeletion, setting.UserFeatureManageCredentials) && !setting.Service.EnableNotifyMail { + doc.AssertElement(t, ".menu a[href='/user/settings/account']", false) + } else { + doc.AssertElement(t, ".menu a[href='/user/settings/account']", true) + } + + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageMFA, setting.UserFeatureManageCredentials) { + doc.AssertElement(t, ".menu a[href='/user/settings/security']", false) + } else { + doc.AssertElement(t, ".menu a[href='/user/settings/security']", true) + } + + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) { + doc.AssertElement(t, ".menu a[href='/user/settings/keys']", false) + } else { + doc.AssertElement(t, ".menu a[href='/user/settings/keys']", true) + } +} + +func WithDisabledFeatures(t *testing.T, features ...string) { + t.Helper() + + global := setting.Admin.UserDisabledFeatures + user := setting.Admin.ExternalUserDisableFeatures + + setting.Admin.UserDisabledFeatures = container.SetOf(features...) + setting.Admin.ExternalUserDisableFeatures = setting.Admin.UserDisabledFeatures + + t.Cleanup(func() { + setting.Admin.UserDisabledFeatures = global + setting.Admin.ExternalUserDisableFeatures = user + }) +} + +func TestUserSettingsAccount(t *testing.T) { + t.Run("all features enabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/account") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + // account navbar should display + doc.AssertElement(t, ".menu a[href='/user/settings/account']", true) + + doc.AssertElement(t, "#password", true) + doc.AssertElement(t, "#email", true) + doc.AssertElement(t, "#delete-form", true) + }) + + t.Run("credentials disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageCredentials) + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/account") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) + + doc.AssertElement(t, "#password", false) + doc.AssertElement(t, "#email", false) + doc.AssertElement(t, "#delete-form", true) + }) + + t.Run("deletion disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureDeletion) + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/account") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) + + doc.AssertElement(t, "#password", true) + doc.AssertElement(t, "#email", true) + doc.AssertElement(t, "#delete-form", false) + }) + + t.Run("deletion, credentials and email notifications are disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + mail := setting.Service.EnableNotifyMail + setting.Service.EnableNotifyMail = false + defer func() { + setting.Service.EnableNotifyMail = mail + }() + + WithDisabledFeatures(t, setting.UserFeatureDeletion, setting.UserFeatureManageCredentials) + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/account") + session.MakeRequest(t, req, http.StatusNotFound) + }) +} + +func TestUserSettingsUpdatePassword(t *testing.T) { + t.Run("enabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user/settings/account") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + req = NewRequestWithValues(t, "POST", "/user/settings/account", map[string]string{ + "_csrf": doc.GetCSRF(), + "old_password": "password", + "password": "password", + "retype": "password", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + }) + + t.Run("credentials disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageCredentials) + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user/settings/account") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + req = NewRequestWithValues(t, "POST", "/user/settings/account", map[string]string{ + "_csrf": doc.GetCSRF(), + }) + session.MakeRequest(t, req, http.StatusNotFound) + }) +} + +func TestUserSettingsUpdateEmail(t *testing.T) { + t.Run("credentials disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageCredentials) + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user/settings/account") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + req = NewRequestWithValues(t, "POST", "/user/settings/account/email", map[string]string{ + "_csrf": doc.GetCSRF(), + }) + session.MakeRequest(t, req, http.StatusNotFound) + }) +} + +func TestUserSettingsDeleteEmail(t *testing.T) { + t.Run("credentials disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageCredentials) + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user/settings/account") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + req = NewRequestWithValues(t, "POST", "/user/settings/account/email/delete", map[string]string{ + "_csrf": doc.GetCSRF(), + }) + session.MakeRequest(t, req, http.StatusNotFound) + }) +} + +func TestUserSettingsDelete(t *testing.T) { + t.Run("deletion disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureDeletion) + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user/settings/account") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + req = NewRequestWithValues(t, "POST", "/user/settings/account/delete", map[string]string{ + "_csrf": doc.GetCSRF(), + }) + session.MakeRequest(t, req, http.StatusNotFound) + }) +} + +func TestUserSettingsAppearance(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/appearance") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) +} + +func TestUserSettingsSecurity(t *testing.T) { + t.Run("credentials disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageCredentials) + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/security") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) + + doc.AssertElement(t, "#register-webauthn", true) + }) + + t.Run("mfa disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageMFA) + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/security") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) + + doc.AssertElement(t, "#register-webauthn", false) + }) + + t.Run("credentials and mfa disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageCredentials, setting.UserFeatureManageMFA) + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/security") + session.MakeRequest(t, req, http.StatusNotFound) + }) +} + +func TestUserSettingsApplications(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/applications") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) +} + +func TestUserSettingsKeys(t *testing.T) { + t.Run("all enabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/keys") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) + + doc.AssertElement(t, "#add-ssh-button", true) + doc.AssertElement(t, "#add-gpg-key-panel", true) + }) + + t.Run("ssh keys disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageSSHKeys) + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/keys") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) + + doc.AssertElement(t, "#add-ssh-button", false) + doc.AssertElement(t, "#add-gpg-key-panel", true) + }) + + t.Run("gpg keys disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageGPGKeys) + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/keys") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) + + doc.AssertElement(t, "#add-ssh-button", true) + doc.AssertElement(t, "#add-gpg-key-panel", false) + }) + + t.Run("ssh & gpg keys disabled", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + WithDisabledFeatures(t, setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/keys") + _ = session.MakeRequest(t, req, http.StatusNotFound) + }) +} + +func TestUserSettingsSecrets(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/actions/secrets") + if setting.Actions.Enabled { + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) + } else { + session.MakeRequest(t, req, http.StatusNotFound) + } +} + +func TestUserSettingsPackages(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/packages") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) +} + +func TestUserSettingsPackagesRulesAdd(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/packages/rules/add") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) +} + +func TestUserSettingsOrganization(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/organization") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) +} + +func TestUserSettingsRepos(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/repos") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) +} From c9487b41eecf4a25d85a862e24b5d1aab58b8920 Mon Sep 17 00:00:00 2001 From: Rowan Bohde Date: Tue, 9 Jul 2024 10:27:19 -0500 Subject: [PATCH 3/3] Ensure that /user/settings/blocked_users has the list of disabled features --- routers/web/user/setting/block.go | 2 ++ tests/integration/user_settings_test.go | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go index 94fc380cee848..d419fb321bc00 100644 --- a/routers/web/user/setting/block.go +++ b/routers/web/user/setting/block.go @@ -6,6 +6,7 @@ package setting import ( "net/http" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" shared_user "code.gitea.io/gitea/routers/web/shared/user" @@ -19,6 +20,7 @@ const ( func BlockedUsers(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("user.block.list") ctx.Data["PageIsSettingsBlockedUsers"] = true + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared_user.BlockedUsers(ctx, ctx.Doer) if ctx.Written() { diff --git a/tests/integration/user_settings_test.go b/tests/integration/user_settings_test.go index d4b7b075df30d..2103c92d58372 100644 --- a/tests/integration/user_settings_test.go +++ b/tests/integration/user_settings_test.go @@ -392,3 +392,14 @@ func TestUserSettingsRepos(t *testing.T) { assertNavbar(t, doc) } + +func TestUserSettingsBlockedUsers(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/settings/blocked_users") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + assertNavbar(t, doc) +}