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 <kdumontnu@gmail.com>
This commit is contained in:
Rowan Bohde 2024-07-09 12:36:31 -05:00 committed by GitHub
parent 13015bba5a
commit 1ee59f0fa3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 586 additions and 17 deletions

View File

@ -1488,15 +1488,19 @@ LEVEL = Info
;; ;;
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
;DEFAULT_EMAIL_NOTIFICATIONS = enabled ;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 ;; - deletion: a user cannot delete their own account
;; - 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
;; - manage_mfa: a user cannot configure mfa devices
;; - manage_credentials: a user cannot configure emails, passwords, or openid
;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. ;; 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 ;; - deletion: a user cannot delete their own account
;; - 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
;; - manage_mfa: a user cannot configure mfa devices
;; - manage_credentials: a user cannot configure emails, passwords, or openid
;;EXTERNAL_USER_DISABLE_FEATURES = ;;EXTERNAL_USER_DISABLE_FEATURES =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -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 - `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. - `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. - `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. - `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. - `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.
- `manage_mfa`: a User cannot configure mfa devices.
- `manage_credentials`: a user cannot configure emails, passwords, or openid
## Security (`security`) ## Security (`security`)

View File

@ -1263,12 +1263,14 @@ func GetOrderByName() string {
return "name" 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 // 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 // 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)) || if user != nil && user.LoginType > auth.Plain {
setting.Admin.UserDisabledFeatures.Contains(feature) 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 // DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type

View File

@ -3,6 +3,8 @@
package container package container
import "maps"
type Set[T comparable] map[T]struct{} type Set[T comparable] map[T]struct{}
// SetOf creates a set and adds the specified elements to it. // 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. // Returns true if the set contains the specified element; otherwise, false.
func (s Set[T]) Contains(value T) bool { func (s Set[T]) Contains(values ...T) bool {
_, has := s[value] ret := true
return has for _, value := range values {
_, has := s[value]
ret = ret && has
}
return ret
} }
// Remove removes the specified element. // Remove removes the specified element.
@ -54,3 +60,12 @@ func (s Set[T]) Values() []T {
} }
return keys 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
}

View File

@ -20,11 +20,13 @@ 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(",")...) Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...).Union(Admin.UserDisabledFeatures)
} }
const ( const (
UserFeatureDeletion = "deletion" UserFeatureDeletion = "deletion"
UserFeatureManageSSHKeys = "manage_ssh_keys" UserFeatureManageSSHKeys = "manage_ssh_keys"
UserFeatureManageGPGKeys = "manage_gpg_keys" UserFeatureManageGPGKeys = "manage_gpg_keys"
UserFeatureManageMFA = "manage_mfa"
UserFeatureManageCredentials = "manage_credentials"
) )

View File

@ -7,6 +7,7 @@ import (
"errors" "errors"
"net/http" "net/http"
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"
shared "code.gitea.io/gitea/routers/web/shared/secrets" 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["Title"] = ctx.Tr("actions.actions")
ctx.Data["PageType"] = "secrets" ctx.Data["PageType"] = "secrets"
ctx.Data["PageIsSharedSettingsSecrets"] = true ctx.Data["PageIsSharedSettingsSecrets"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
sCtx, err := getSecretsCtx(ctx) sCtx, err := getSecretsCtx(ctx)
if err != nil { if err != nil {

View File

@ -6,6 +6,7 @@ package setting
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"time" "time"
@ -33,6 +34,11 @@ const (
// Account renders change user's password, user's email and user suicide page // Account renders change user's password, user's email and user suicide page
func Account(ctx *context.Context) { 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["Title"] = ctx.Tr("settings.account")
ctx.Data["PageIsSettingsAccount"] = true ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.Doer.Email ctx.Data["Email"] = ctx.Doer.Email
@ -45,9 +51,16 @@ func Account(ctx *context.Context) {
// AccountPost response for change user's password // AccountPost response for change user's password
func AccountPost(ctx *context.Context) { 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) form := web.GetForm(ctx).(*forms.ChangePasswordForm)
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.Doer.Email
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
if ctx.HasError() { if ctx.HasError() {
loadAccountData(ctx) loadAccountData(ctx)
@ -89,9 +102,16 @@ func AccountPost(ctx *context.Context) {
// EmailPost response for change user's email // EmailPost response for change user's email
func EmailPost(ctx *context.Context) { 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) form := web.GetForm(ctx).(*forms.AddEmailForm)
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.Doer.Email
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
// Make email address primary. // Make email address primary.
if ctx.FormString("_method") == "PRIMARY" { if ctx.FormString("_method") == "PRIMARY" {
@ -216,6 +236,10 @@ func EmailPost(ctx *context.Context) {
// DeleteEmail response for delete user's email // DeleteEmail response for delete user's email
func DeleteEmail(ctx *context.Context) { 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")) email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
if err != nil || email == nil { if err != nil || email == nil {
ctx.ServerError("GetEmailAddressByID", err) ctx.ServerError("GetEmailAddressByID", err)
@ -241,6 +265,8 @@ func DeleteAccount(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true 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 { if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil {
switch { switch {

View File

@ -9,6 +9,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"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"
@ -24,6 +25,7 @@ const (
func Applications(ctx *context.Context) { func Applications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.applications") ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsSettingsApplications"] = true ctx.Data["PageIsSettingsApplications"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
loadApplicationsData(ctx) loadApplicationsData(ctx)
@ -35,6 +37,7 @@ func ApplicationsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewAccessTokenForm) form := web.GetForm(ctx).(*forms.NewAccessTokenForm)
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true ctx.Data["PageIsSettingsApplications"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
if ctx.HasError() { if ctx.HasError() {
loadApplicationsData(ctx) loadApplicationsData(ctx)

View File

@ -6,6 +6,7 @@ package setting
import ( import (
"net/http" "net/http"
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"
shared_user "code.gitea.io/gitea/routers/web/shared/user" shared_user "code.gitea.io/gitea/routers/web/shared/user"
@ -19,6 +20,7 @@ const (
func BlockedUsers(ctx *context.Context) { func BlockedUsers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("user.block.list") ctx.Data["Title"] = ctx.Tr("user.block.list")
ctx.Data["PageIsSettingsBlockedUsers"] = true ctx.Data["PageIsSettingsBlockedUsers"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared_user.BlockedUsers(ctx, ctx.Doer) shared_user.BlockedUsers(ctx, ctx.Doer)
if ctx.Written() { if ctx.Written() {

View File

@ -25,11 +25,17 @@ const (
// Keys render user's SSH/GPG public keys page // Keys render user's SSH/GPG public keys page
func Keys(ctx *context.Context) { 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["Title"] = ctx.Tr("settings.ssh_gpg_keys")
ctx.Data["PageIsSettingsKeys"] = true ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["DisableSSH"] = setting.SSH.Disabled
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
loadKeysData(ctx) loadKeysData(ctx)
@ -44,6 +50,7 @@ func KeysPost(ctx *context.Context) {
ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["DisableSSH"] = setting.SSH.Disabled
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
if ctx.HasError() { if ctx.HasError() {
loadKeysData(ctx) loadKeysData(ctx)

View File

@ -25,6 +25,7 @@ const (
func Packages(ctx *context.Context) { func Packages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.SetPackagesContext(ctx, ctx.Doer) shared.SetPackagesContext(ctx, ctx.Doer)
@ -34,6 +35,7 @@ func Packages(ctx *context.Context) {
func PackagesRuleAdd(ctx *context.Context) { func PackagesRuleAdd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.SetRuleAddContext(ctx) shared.SetRuleAddContext(ctx)
@ -43,6 +45,7 @@ func PackagesRuleAdd(ctx *context.Context) {
func PackagesRuleEdit(ctx *context.Context) { func PackagesRuleEdit(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.SetRuleEditContext(ctx, ctx.Doer) shared.SetRuleEditContext(ctx, ctx.Doer)
@ -52,6 +55,7 @@ func PackagesRuleEdit(ctx *context.Context) {
func PackagesRuleAddPost(ctx *context.Context) { func PackagesRuleAddPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsPackages"] = true ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.PerformRuleAddPost( shared.PerformRuleAddPost(
ctx, ctx,
@ -64,6 +68,7 @@ func PackagesRuleAddPost(ctx *context.Context) {
func PackagesRuleEditPost(ctx *context.Context) { func PackagesRuleEditPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.PerformRuleEditPost( shared.PerformRuleEditPost(
ctx, ctx,
@ -76,6 +81,7 @@ func PackagesRuleEditPost(ctx *context.Context) {
func PackagesRulePreview(ctx *context.Context) { func PackagesRulePreview(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.SetRulePreviewContext(ctx, ctx.Doer) shared.SetRulePreviewContext(ctx, ctx.Doer)

View File

@ -48,6 +48,8 @@ func Profile(ctx *context.Context) {
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsProfile) ctx.HTML(http.StatusOK, tplSettingsProfile)
} }
@ -57,6 +59,7 @@ func ProfilePost(ctx *context.Context) {
ctx.Data["PageIsSettingsProfile"] = true ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
if ctx.HasError() { if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsProfile) ctx.HTML(http.StatusOK, tplSettingsProfile)
@ -182,6 +185,7 @@ func DeleteAvatar(ctx *context.Context) {
func Organization(ctx *context.Context) { func Organization(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.organization") ctx.Data["Title"] = ctx.Tr("settings.organization")
ctx.Data["PageIsSettingsOrganization"] = true ctx.Data["PageIsSettingsOrganization"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
opts := organization.FindOrgOptions{ opts := organization.FindOrgOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
@ -213,6 +217,7 @@ func Organization(ctx *context.Context) {
func Repos(ctx *context.Context) { func Repos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.repos") ctx.Data["Title"] = ctx.Tr("settings.repos")
ctx.Data["PageIsSettingsRepos"] = true ctx.Data["PageIsSettingsRepos"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories 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 allThemes = append([]string{setting.UI.DefaultTheme}, allThemes...) // move the default theme to the top
} }
ctx.Data["AllThemes"] = allThemes ctx.Data["AllThemes"] = allThemes
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
var hiddenCommentTypes *big.Int var hiddenCommentTypes *big.Int
val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)

View File

@ -13,6 +13,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models/auth" "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/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
@ -25,6 +26,11 @@ import (
// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code. // RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
func RegenerateScratchTwoFactor(ctx *context.Context) { 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["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true ctx.Data["PageIsSettingsSecurity"] = true
@ -55,6 +61,11 @@ func RegenerateScratchTwoFactor(ctx *context.Context) {
// DisableTwoFactor deletes the user's 2FA settings. // DisableTwoFactor deletes the user's 2FA settings.
func DisableTwoFactor(ctx *context.Context) { 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["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true 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. // EnrollTwoFactor shows the page where the user can enroll into 2FA.
func EnrollTwoFactor(ctx *context.Context) { 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["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true ctx.Data["PageIsSettingsSecurity"] = true
@ -167,6 +183,11 @@ func EnrollTwoFactor(ctx *context.Context) {
// EnrollTwoFactorPost handles enrolling the user into 2FA. // EnrollTwoFactorPost handles enrolling the user into 2FA.
func EnrollTwoFactorPost(ctx *context.Context) { func EnrollTwoFactorPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.Error(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm) form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true ctx.Data["PageIsSettingsSecurity"] = true

View File

@ -17,6 +17,11 @@ import (
// OpenIDPost response for change user's openid // OpenIDPost response for change user's openid
func OpenIDPost(ctx *context.Context) { func OpenIDPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.Error(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.AddOpenIDForm) form := web.GetForm(ctx).(*forms.AddOpenIDForm)
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true ctx.Data["PageIsSettingsSecurity"] = true
@ -105,6 +110,11 @@ func settingsOpenIDVerify(ctx *context.Context) {
// DeleteOpenID response for delete user's openid // DeleteOpenID response for delete user's openid
func DeleteOpenID(ctx *context.Context) { 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 { if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
ctx.ServerError("DeleteUserOpenID", err) ctx.ServerError("DeleteUserOpenID", err)
return return
@ -117,6 +127,11 @@ func DeleteOpenID(ctx *context.Context) {
// ToggleOpenIDVisibility response for toggle visibility of user's openid // ToggleOpenIDVisibility response for toggle visibility of user's openid
func ToggleOpenIDVisibility(ctx *context.Context) { 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 { if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id")); err != nil {
ctx.ServerError("ToggleUserOpenIDVisibility", err) ctx.ServerError("ToggleUserOpenIDVisibility", err)
return return

View File

@ -25,6 +25,12 @@ const (
// Security render change user's password page and 2FA // Security render change user's password page and 2FA
func Security(ctx *context.Context) { 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["Title"] = ctx.Tr("settings.security")
ctx.Data["PageIsSettingsSecurity"] = true ctx.Data["PageIsSettingsSecurity"] = true
@ -40,6 +46,11 @@ func Security(ctx *context.Context) {
// DeleteAccountLink delete a single account link // DeleteAccountLink delete a single account link
func DeleteAccountLink(ctx *context.Context) { func DeleteAccountLink(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.Error(http.StatusNotFound)
return
}
id := ctx.FormInt64("id") id := ctx.FormInt64("id")
if id <= 0 { if id <= 0 {
ctx.Flash.Error("Account link id is not given") ctx.Flash.Error("Account link id is not given")
@ -145,4 +156,5 @@ func loadSecurityData(ctx *context.Context) {
return return
} }
ctx.Data["OpenIDs"] = openid ctx.Data["OpenIDs"] = openid
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
} }

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
wa "code.gitea.io/gitea/modules/auth/webauthn" wa "code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -23,6 +24,11 @@ import (
// WebAuthnRegister initializes the webauthn registration procedure // WebAuthnRegister initializes the webauthn registration procedure
func WebAuthnRegister(ctx *context.Context) { func WebAuthnRegister(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.Error(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.WebauthnRegistrationForm) form := web.GetForm(ctx).(*forms.WebauthnRegistrationForm)
if form.Name == "" { if form.Name == "" {
// Set name to the hexadecimal of the current time // 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 // WebauthnRegisterPost receives the response of the security key
func WebauthnRegisterPost(ctx *context.Context) { 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) name, ok := ctx.Session.Get("webauthnName").(string)
if !ok || name == "" { if !ok || name == "" {
ctx.ServerError("Get webauthnName", errors.New("no webauthnName")) ctx.ServerError("Get webauthnName", errors.New("no webauthnName"))
@ -113,6 +124,11 @@ func WebauthnRegisterPost(ctx *context.Context) {
// WebauthnDelete deletes an security key by id // WebauthnDelete deletes an security key by id
func WebauthnDelete(ctx *context.Context) { func WebauthnDelete(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.Error(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.WebauthnDeleteForm) form := web.GetForm(ctx).(*forms.WebauthnDeleteForm)
if _, err := auth.DeleteCredential(ctx, form.ID, ctx.Doer.ID); err != nil { if _, err := auth.DeleteCredential(ctx, form.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("GetWebAuthnCredentialByID", err) ctx.ServerError("GetWebAuthnCredentialByID", err)

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting" "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["BaseLink"] = setting.AppSubURL + "/user/settings/hooks"
ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks" ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks"
ctx.Data["Description"] = ctx.Tr("settings.hooks.desc") 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}) ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID})
if err != nil { if err != nil {

View File

@ -4,7 +4,7 @@
{{ctx.Locale.Tr "settings.password"}} {{ctx.Locale.Tr "settings.password"}}
</h4> </h4>
<div class="ui attached segment"> <div class="ui attached segment">
{{if or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2)}} {{if and (not ($.UserDisabledFeatures.Contains "manage_credentials")) (or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2))}}
<form class="ui form ignore-dirty" action="{{AppSubUrl}}/user/settings/account" method="post"> <form class="ui form ignore-dirty" action="{{AppSubUrl}}/user/settings/account" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
@ -35,6 +35,7 @@
{{end}} {{end}}
</div> </div>
{{if not (and ($.UserDisabledFeatures.Contains "manage_credentials") (not $.EnableNotifyMail))}}
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.manage_emails"}} {{ctx.Locale.Tr "settings.manage_emails"}}
</h4> </h4>
@ -63,6 +64,7 @@
</form> </form>
</div> </div>
{{end}} {{end}}
{{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}
{{range .Emails}} {{range .Emails}}
<div class="item"> <div class="item">
{{if not .IsPrimary}} {{if not .IsPrimary}}
@ -109,8 +111,12 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{end}}
</div> </div>
</div> </div>
{{end}}
{{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}
<div class="ui bottom attached segment"> <div class="ui bottom attached segment">
<form class="ui form" action="{{AppSubUrl}}/user/settings/account/email" method="post"> <form class="ui form" action="{{AppSubUrl}}/user/settings/account/email" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
@ -127,6 +133,7 @@
<div class="ui warning message">{{ctx.Locale.Tr "settings.can_not_add_email_activations_pending"}}</div> <div class="ui warning message">{{ctx.Locale.Tr "settings.can_not_add_email_activations_pending"}}</div>
{{end}} {{end}}
</div> </div>
{{end}}
{{if not ($.UserDisabledFeatures.Contains "deletion")}} {{if not ($.UserDisabledFeatures.Contains "deletion")}}
<h4 class="ui top attached error header"> <h4 class="ui top attached error header">

View File

@ -4,24 +4,30 @@
<a class="{{if .PageIsSettingsProfile}}active {{end}}item" href="{{AppSubUrl}}/user/settings"> <a class="{{if .PageIsSettingsProfile}}active {{end}}item" href="{{AppSubUrl}}/user/settings">
{{ctx.Locale.Tr "settings.profile"}} {{ctx.Locale.Tr "settings.profile"}}
</a> </a>
{{if not (and ($.UserDisabledFeatures.Contains "manage_credentials" "deletion") (not $.EnableNotifyMail))}}
<a class="{{if .PageIsSettingsAccount}}active {{end}}item" href="{{AppSubUrl}}/user/settings/account"> <a class="{{if .PageIsSettingsAccount}}active {{end}}item" href="{{AppSubUrl}}/user/settings/account">
{{ctx.Locale.Tr "settings.account"}} {{ctx.Locale.Tr "settings.account"}}
</a> </a>
{{end}}
<a class="{{if .PageIsSettingsAppearance}}active {{end}}item" href="{{AppSubUrl}}/user/settings/appearance"> <a class="{{if .PageIsSettingsAppearance}}active {{end}}item" href="{{AppSubUrl}}/user/settings/appearance">
{{ctx.Locale.Tr "settings.appearance"}} {{ctx.Locale.Tr "settings.appearance"}}
</a> </a>
{{if not ($.UserDisabledFeatures.Contains "manage_mfa" "manage_credentials")}}
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security"> <a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security">
{{ctx.Locale.Tr "settings.security"}} {{ctx.Locale.Tr "settings.security"}}
</a> </a>
{{end}}
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users"> <a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
{{ctx.Locale.Tr "user.block.list"}} {{ctx.Locale.Tr "user.block.list"}}
</a> </a>
<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications"> <a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications">
{{ctx.Locale.Tr "settings.applications"}} {{ctx.Locale.Tr "settings.applications"}}
</a> </a>
{{if not ($.UserDisabledFeatures.Contains "manage_ssh_keys" "manage_gpg_keys")}}
<a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{AppSubUrl}}/user/settings/keys"> <a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{AppSubUrl}}/user/settings/keys">
{{ctx.Locale.Tr "settings.ssh_gpg_keys"}} {{ctx.Locale.Tr "settings.ssh_gpg_keys"}}
</a> </a>
{{end}}
{{if .EnableActions}} {{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}> <details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary> <summary>{{ctx.Locale.Tr "actions.actions"}}</summary>

View File

@ -1,11 +1,17 @@
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings security")}} {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings security")}}
{{if not ($.UserDisabledFeatures.Contains "manage_mfa" "manage_credentials")}}
<div class="user-setting-content"> <div class="user-setting-content">
{{if not ($.UserDisabledFeatures.Contains "manage_mfa")}}
{{template "user/settings/security/twofa" .}} {{template "user/settings/security/twofa" .}}
{{template "user/settings/security/webauthn" .}} {{template "user/settings/security/webauthn" .}}
{{end}}
{{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}
{{template "user/settings/security/accountlinks" .}} {{template "user/settings/security/accountlinks" .}}
{{if .EnableOpenIDSignIn}} {{if .EnableOpenIDSignIn}}
{{template "user/settings/security/openid" .}} {{template "user/settings/security/openid" .}}
{{end}} {{end}}
{{end}}
</div> </div>
{{end}}
{{template "user/settings/layout_footer" .}} {{template "user/settings/layout_footer" .}}

View File

@ -0,0 +1,405 @@
// 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)
}
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)
}