Skip to content
8 changes: 6 additions & 2 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 =

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
8 changes: 6 additions & 2 deletions docs/content/administration/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
10 changes: 6 additions & 4 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -1263,12 +1263,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
Expand Down
23 changes: 19 additions & 4 deletions modules/container/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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
}
10 changes: 6 additions & 4 deletions modules/setting/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
2 changes: 2 additions & 0 deletions routers/web/repo/setting/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions routers/web/user/setting/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package setting

import (
"errors"
"fmt"
"net/http"
"time"

Expand Down Expand Up @@ -33,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
Expand All @@ -45,9 +51,16 @@ 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
ctx.Data["Email"] = ctx.Doer.Email
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail

if ctx.HasError() {
loadAccountData(ctx)
Expand Down Expand Up @@ -89,9 +102,16 @@ 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
ctx.Data["Email"] = ctx.Doer.Email
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail

// Make email address primary.
if ctx.FormString("_method") == "PRIMARY" {
Expand Down Expand Up @@ -216,6 +236,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)
Expand All @@ -241,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 {
Expand Down
3 changes: 3 additions & 0 deletions routers/web/user/setting/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)

Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions routers/web/user/setting/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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() {
Expand Down
7 changes: 7 additions & 0 deletions routers/web/user/setting/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions routers/web/user/setting/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions routers/web/user/setting/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
Loading