diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 131fb3401eec2..4abd3128d9514 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2611,3 +2611,7 @@ ROUTER = console ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; storage type ;STORAGE_TYPE = local + +;[user] +; Disabled modules from user settings, could be password, deletion, security, applications, gpg_keys, organizations +;SETTING_DISABLED_MODULES = diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index aca591787e91b..a9aba13a43b30 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -1386,6 +1386,16 @@ steps: although Github don't support this form. +## User (`user`) + +- `USER_SETTING_DISABLED_MODULES`:**** Disabled modules from user settings, could be a copmosite of `password`, `deletion`, `security`, `applications`, `gpg keys`, `organizations` with a comma. + - `password`: User cannot change his password from the website. + - `deletion`: User cannot remove himself from the website. + - `security`: User cannot update his security settings from the website. + - `applications`: User cannot create application himself. + - `gpg_keys`: User cannot manage gpg keys himself. + - `organizations`: User cannot manage his organizations himself. + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer. diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 4d7a7caab8de3..e38795aa48632 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -278,6 +278,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) { loadMirrorFrom(cfg) loadMarkupFrom(cfg) loadOtherFrom(cfg) + loadUserFrom(cfg) } func loadRunModeFrom(rootCfg ConfigProvider) { diff --git a/modules/setting/user.go b/modules/setting/user.go new file mode 100644 index 0000000000000..ccb7a2b070739 --- /dev/null +++ b/modules/setting/user.go @@ -0,0 +1,27 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "strings" + + "code.gitea.io/gitea/modules/container" +) + +// userSetting represents user settings +type userSetting struct { + content container.Set[string] +} + +func (s *userSetting) Enabled(module string) bool { + return !s.content.Contains(strings.ToLower(module)) +} + +var User userSetting + +func loadUserFrom(rootCfg ConfigProvider) { + sec := rootCfg.Section("user") + values := sec.Key("SETTING_DISABLED_MODULES").Strings(",") + User.content = container.SetOf(values...) +} diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index 6debf95bbce06..6f092e32110a7 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -5,6 +5,7 @@ package setting import ( + "fmt" "net/http" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -19,6 +20,13 @@ import ( const ( tplSettingsKeys base.TplName = "user/settings/keys" + + UserPasswordKey = "password" + UserGPGKeysKey = "gpg_keys" + UserDeletionKey = "deletion" + UserSecurityKey = "security" + UserApplicationKey = "applications" + UserOrganizations = "organizations" ) // Keys render user's SSH/GPG public keys page @@ -77,6 +85,11 @@ func KeysPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "gpg": + if !setting.User.Enabled(UserGPGKeysKey) { + ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting are not allowed")) + return + } + token := asymkey_model.VerificationToken(ctx.Doer, 1) lastToken := asymkey_model.VerificationToken(ctx.Doer, 0) @@ -224,6 +237,10 @@ func KeysPost(ctx *context.Context) { func DeleteKey(ctx *context.Context) { switch ctx.FormString("type") { case "gpg": + if !setting.User.Enabled(UserGPGKeysKey) { + ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting are not allowed")) + return + } if err := asymkey_model.DeleteGPGKey(ctx.Doer, ctx.FormInt64("id")); err != nil { ctx.Flash.Error("DeleteGPGKey: " + err.Error()) } else { diff --git a/routers/web/web.go b/routers/web/web.go index 292268dc8055a..f782af59978af 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -344,6 +344,14 @@ func RegisterRoutes(m *web.Route) { m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) } + userSettingModuleEnabled := func(module string) func(ctx *context.Context) { + return func(ctx *context.Context) { + if !setting.User.Enabled(module) { + ctx.Error(http.StatusNotFound) + } + } + } + // FIXME: not all routes need go through same middleware. // Especially some AJAX requests, we can reduce middleware number to improve performance. // Routers. @@ -434,15 +442,15 @@ func RegisterRoutes(m *web.Route) { m.Group("/user/settings", func() { m.Get("", user_setting.Profile) m.Post("", web.Bind(forms.UpdateProfileForm{}), user_setting.ProfilePost) - m.Get("/change_password", auth.MustChangePassword) - m.Post("/change_password", web.Bind(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost) + m.Get("/change_password", userSettingModuleEnabled(user_setting.UserPasswordKey), auth.MustChangePassword) + m.Post("/change_password", userSettingModuleEnabled(user_setting.UserPasswordKey), web.Bind(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost) m.Post("/avatar", web.Bind(forms.AvatarForm{}), user_setting.AvatarPost) m.Post("/avatar/delete", user_setting.DeleteAvatar) m.Group("/account", func() { m.Combo("").Get(user_setting.Account).Post(web.Bind(forms.ChangePasswordForm{}), user_setting.AccountPost) m.Post("/email", web.Bind(forms.AddEmailForm{}), user_setting.EmailPost) m.Post("/email/delete", user_setting.DeleteEmail) - m.Post("/delete", user_setting.DeleteAccount) + m.Post("/delete", userSettingModuleEnabled(user_setting.UserDeletionKey), user_setting.DeleteAccount) }) m.Group("/appearance", func() { m.Get("", user_setting.Appearance) @@ -469,7 +477,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/toggle_visibility", security.ToggleOpenIDVisibility) }, openIDSignInEnabled) m.Post("/account_link", linkAccountEnabled, security.DeleteAccountLink) - }) + }, userSettingModuleEnabled(user_setting.UserSecurityKey)) m.Group("/applications/oauth2", func() { m.Get("/{id}", user_setting.OAuth2ApplicationShow) m.Post("/{id}", web.Bind(forms.EditOAuth2ApplicationForm{}), user_setting.OAuthApplicationsEdit) @@ -477,10 +485,10 @@ func RegisterRoutes(m *web.Route) { m.Post("", web.Bind(forms.EditOAuth2ApplicationForm{}), user_setting.OAuthApplicationsPost) m.Post("/{id}/delete", user_setting.DeleteOAuth2Application) m.Post("/{id}/revoke/{grantId}", user_setting.RevokeOAuth2Grant) - }) - m.Combo("/applications").Get(user_setting.Applications). - Post(web.Bind(forms.NewAccessTokenForm{}), user_setting.ApplicationsPost) - m.Post("/applications/delete", user_setting.DeleteApplication) + }, userSettingModuleEnabled(user_setting.UserApplicationKey)) + m.Combo("/applications").Get(userSettingModuleEnabled(user_setting.UserApplicationKey), user_setting.Applications). + Post(userSettingModuleEnabled(user_setting.UserApplicationKey), web.Bind(forms.NewAccessTokenForm{}), user_setting.ApplicationsPost) + m.Post("/applications/delete", userSettingModuleEnabled(user_setting.UserApplicationKey), user_setting.DeleteApplication) m.Combo("/keys").Get(user_setting.Keys). Post(web.Bind(forms.AddKeyForm{}), user_setting.KeysPost) m.Post("/keys/delete", user_setting.DeleteKey) @@ -508,7 +516,7 @@ func RegisterRoutes(m *web.Route) { m.Post("", web.Bind(forms.AddSecretForm{}), user_setting.SecretsPost) m.Post("/delete", user_setting.SecretsDelete) }) - m.Get("/organization", user_setting.Organization) + m.Get("/organization", userSettingModuleEnabled(user_setting.UserOrganizations), user_setting.Organization) m.Get("/repos", user_setting.Repos) m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository) @@ -528,6 +536,7 @@ func RegisterRoutes(m *web.Route) { ctx.Data["PageIsUserSettings"] = true ctx.Data["AllThemes"] = setting.UI.Themes ctx.Data["EnablePackages"] = setting.Packages.Enabled + ctx.Data["UserModules"] = &setting.User }) m.Group("/user", func() { diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 24b4e18fbff2e..4ac47dfacdfbe 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -142,7 +142,7 @@ <span class="fitted">{{svg "octicon-repo-push"}}</span> {{.locale.Tr "new_migrate"}} </a> {{end}} - {{if .SignedUser.CanCreateOrganization}} + {{if and (.SignedUser.CanCreateOrganization) ($.UserModules.Enabled "organizations")}} <a class="item" href="{{AppSubUrl}}/org/create"> <span class="fitted">{{svg "octicon-organization"}}</span> {{.locale.Tr "new_org"}} </a> diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index 0eae60fbfc490..d95b4aa8cb6fe 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -57,20 +57,22 @@ {{end}} <div class="ui three wide column"> <div class="text right"> - {{if eq $.SignedUser.ID .ID}} - <form> - <button class="ui red small button delete-button" data-modal-id="leave-organization" - data-url="{{$.OrgLink}}/members/action/leave" data-datauid="{{.ID}}" - data-name="{{.DisplayName}}" - data-data-organization-name="{{$.Org.DisplayName}}">{{$.locale.Tr "org.members.leave"}}</button> - </form> - {{else if $.IsOrganizationOwner}} - <form> - <button class="ui red small button delete-button" data-modal-id="remove-organization-member" - data-url="{{$.OrgLink}}/members/action/remove" data-datauid="{{.ID}}" - data-name="{{.DisplayName}}" - data-data-organization-name="{{$.Org.DisplayName}}">{{$.locale.Tr "org.members.remove"}}</button> - </form> + {{if ($.UserModules.Enabled "organizations")}} + {{if eq $.SignedUser.ID .ID}} + <form> + <button class="ui red small button delete-button" data-modal-id="leave-organization" + data-url="{{$.OrgLink}}/members/action/leave" data-datauid="{{.ID}}" + data-name="{{.DisplayName}}" + data-data-organization-name="{{$.Org.DisplayName}}">{{$.locale.Tr "org.members.leave"}}</button> + </form> + {{else if $.IsOrganizationOwner}} + <form> + <button class="ui red small button delete-button" data-modal-id="remove-organization-member" + data-url="{{$.OrgLink}}/members/action/remove" data-datauid="{{.ID}}" + data-name="{{.DisplayName}}" + data-data-organization-name="{{$.Org.DisplayName}}">{{$.locale.Tr "org.members.remove"}}</button> + </form> + {{end}} {{end}} </div> </div> diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 507173e51fcff..385d437fc47f4 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -2,18 +2,20 @@ <h4 class="ui top attached header"> <strong>{{.Team.Name}}</strong> <div class="ui right"> - {{if .Team.IsMember $.SignedUser.ID}} - <form> - <button class="ui red tiny button delete-button" data-modal-id="leave-team-sidebar" - data-url="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}" - data-name="{{.Team.Name}}">{{$.locale.Tr "org.teams.leave"}}</button> - </form> - {{else if .IsOrganizationOwner}} - <form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/join"> - {{$.CsrfTokenHtml}} - <input type="hidden" name="page" value="team"/> - <button type="submit" class="ui primary tiny button" name="uid" value="{{$.SignedUser.ID}}">{{$.locale.Tr "org.teams.join"}}</button> - </form> + {{if ($.UserModules.Enabled "organizations")}} + {{if .Team.IsMember $.SignedUser.ID}} + <form> + <button class="ui red tiny button delete-button" data-modal-id="leave-team-sidebar" + data-url="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}" + data-name="{{.Team.Name}}">{{$.locale.Tr "org.teams.leave"}}</button> + </form> + {{else if .IsOrganizationOwner}} + <form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/join"> + {{$.CsrfTokenHtml}} + <input type="hidden" name="page" value="team"/> + <button type="submit" class="ui primary tiny button" name="uid" value="{{$.SignedUser.ID}}">{{$.locale.Tr "org.teams.join"}}</button> + </form> + {{end}} {{end}} </div> </h4> diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index df0620af49032..ded51dec226b8 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -16,17 +16,19 @@ <div class="ui top attached header"> <a class="text black" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.Name}}</strong></a> <div class="ui right"> - {{if .IsMember $.SignedUser.ID}} - <form> - <button class="ui red tiny button delete-button" data-modal-id="leave-team" - data-url="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}" - data-name="{{.Name}}">{{$.locale.Tr "org.teams.leave"}}</button> - </form> - {{else if $.IsOrganizationOwner}} - <form method="post" action="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/join"> - {{$.CsrfTokenHtml}} - <button type="submit" class="ui primary small button" name="uid" value="{{$.SignedUser.ID}}">{{$.locale.Tr "org.teams.join"}}</button> - </form> + {{if ($.UserModules.Enabled "organizations")}} + {{if .IsMember $.SignedUser.ID}} + <form> + <button class="ui red tiny button delete-button" data-modal-id="leave-team" + data-url="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}" + data-name="{{.Name}}">{{$.locale.Tr "org.teams.leave"}}</button> + </form> + {{else if $.IsOrganizationOwner}} + <form method="post" action="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/join"> + {{$.CsrfTokenHtml}} + <button type="submit" class="ui primary small button" name="uid" value="{{$.SignedUser.ID}}">{{$.locale.Tr "org.teams.join"}}</button> + </form> + {{end}} {{end}} </div> </div> diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index 53f7d021e0b6a..358e4ada37478 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -3,40 +3,42 @@ {{template "user/settings/navbar" .}} <div class="ui container"> {{template "base/alert" .}} - <h4 class="ui top attached header"> - {{.locale.Tr "settings.password"}} - </h4> - <div class="ui attached segment"> - {{if or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2)}} - <form class="ui form ignore-dirty" action="{{AppSubUrl}}/user/settings/account" method="post"> - {{template "base/disable_form_autofill"}} - {{.CsrfTokenHtml}} - {{if .SignedUser.IsPasswordSet}} - <div class="required field {{if .Err_OldPassword}}error{{end}}"> - <label for="old_password">{{.locale.Tr "settings.old_password"}}</label> - <input id="old_password" name="old_password" type="password" autocomplete="current-password" autofocus required> - </div> - {{end}} - <div class="required field {{if .Err_Password}}error{{end}}"> - <label for="password">{{.locale.Tr "settings.new_password"}}</label> - <input id="password" name="password" type="password" autocomplete="new-password" required> - </div> - <div class="required field {{if .Err_Password}}error{{end}}"> - <label for="retype">{{.locale.Tr "settings.retype_new_password"}}</label> - <input id="retype" name="retype" type="password" autocomplete="new-password" required> - </div> + {{if $.UserModules.Enabled "password"}} + <h4 class="ui top attached header"> + {{.locale.Tr "settings.password"}} + </h4> + <div class="ui attached segment"> + {{if or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2)}} + <form class="ui form ignore-dirty" action="{{AppSubUrl}}/user/settings/account" method="post"> + {{template "base/disable_form_autofill"}} + {{.CsrfTokenHtml}} + {{if .SignedUser.IsPasswordSet}} + <div class="required field {{if .Err_OldPassword}}error{{end}}"> + <label for="old_password">{{.locale.Tr "settings.old_password"}}</label> + <input id="old_password" name="old_password" type="password" autocomplete="current-password" autofocus required> + </div> + {{end}} + <div class="required field {{if .Err_Password}}error{{end}}"> + <label for="password">{{.locale.Tr "settings.new_password"}}</label> + <input id="password" name="password" type="password" autocomplete="new-password" required> + </div> + <div class="required field {{if .Err_Password}}error{{end}}"> + <label for="retype">{{.locale.Tr "settings.retype_new_password"}}</label> + <input id="retype" name="retype" type="password" autocomplete="new-password" required> + </div> - <div class="field"> - <button class="ui green button">{{$.locale.Tr "settings.change_password"}}</button> - <a href="{{AppSubUrl}}/user/forgot_password?email={{.Email}}">{{.locale.Tr "auth.forgot_password"}}</a> + <div class="field"> + <button class="ui green button">{{$.locale.Tr "settings.change_password"}}</button> + <a href="{{AppSubUrl}}/user/forgot_password?email={{.Email}}">{{.locale.Tr "auth.forgot_password"}}</a> + </div> + </form> + {{else}} + <div class="ui info message"> + <p class="text left">{{$.locale.Tr "settings.password_change_disabled"}}</p> </div> - </form> - {{else}} - <div class="ui info message"> - <p class="text left">{{$.locale.Tr "settings.password_change_disabled"}}</p> + {{end}} </div> - {{end}} - </div> + {{end}} <h4 class="ui top attached header"> {{.locale.Tr "settings.manage_emails"}} @@ -133,22 +135,16 @@ </form> </div> - <h4 class="ui top attached error header"> - {{.locale.Tr "settings.delete_account"}} - </h4> - <div class="ui attached error segment"> - <div class="ui red message"> - <p class="text left">{{svg "octicon-alert"}} {{.locale.Tr "settings.delete_prompt" | Str2html}}</p> - {{if .UserDeleteWithComments}} - <p class="text left" style="font-weight: bold;">{{.locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime | Str2html}}</p> - {{end}} - </div> - <form class="ui form ignore-dirty" id="delete-form" action="{{AppSubUrl}}/user/settings/account/delete" method="post"> - {{template "base/disable_form_autofill"}} - {{.CsrfTokenHtml}} - <div class="required field {{if .Err_Password}}error{{end}}"> - <label for="password-confirmation">{{.locale.Tr "password"}}</label> - <input id="password-confirmation" name="password" type="password" autocomplete="off" required> + {{if $.UserModules.Enabled "deletion"}} + <h4 class="ui top attached error header"> + {{.locale.Tr "settings.delete_account"}} + </h4> + <div class="ui attached error segment"> + <div class="ui red message"> + <p class="text left">{{svg "octicon-alert"}} {{.locale.Tr "settings.delete_prompt" | Str2html}}</p> + {{if .UserDeleteWithComments}} + <p class="text left" style="font-weight: bold;">{{.locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime | Str2html}}</p> + {{end}} </div> <div class="field"> <button class="ui red button delete-button" data-modal-id="delete-account" data-type="form" data-form="#delete-form"> diff --git a/templates/user/settings/keys.tmpl b/templates/user/settings/keys.tmpl index 9a4be2b2c2c05..13de052a9ca98 100644 --- a/templates/user/settings/keys.tmpl +++ b/templates/user/settings/keys.tmpl @@ -5,7 +5,9 @@ {{template "base/alert" .}} {{template "user/settings/keys_ssh" .}} {{template "user/settings/keys_principal" .}} - {{template "user/settings/keys_gpg" .}} + {{if $.UserModules.Enabled "gpg keys"}} + {{template "user/settings/keys_gpg" .}} + {{end}} </div> </div> diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index 4afe2173c2bda..2447d60bc3f86 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -9,12 +9,16 @@ <a class="{{if .PageIsSettingsAppearance}}active {{end}}item" href="{{AppSubUrl}}/user/settings/appearance"> {{.locale.Tr "settings.appearance"}} </a> - <a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security"> - {{.locale.Tr "settings.security"}} - </a> - <a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications"> - {{.locale.Tr "settings.applications"}} - </a> + {{if $.UserModules.Enabled "security"}} + <a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security"> + {{.locale.Tr "settings.security"}} + </a> + {{end}} + {{if $.UserModules.Enabled "applications"}} + <a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications"> + {{.locale.Tr "settings.applications"}} + </a> + {{end}} <a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{AppSubUrl}}/user/settings/keys"> {{.locale.Tr "settings.ssh_gpg_keys"}} </a> @@ -31,9 +35,11 @@ {{.locale.Tr "repo.settings.hooks"}} </a> {{end}} - <a class="{{if .PageIsSettingsOrganization}}active {{end}}item" href="{{AppSubUrl}}/user/settings/organization"> - {{.locale.Tr "settings.organization"}} - </a> + {{if $.UserModules.Enabled "organizations"}} + <a class="{{if .PageIsSettingsOrganization}}active {{end}}item" href="{{AppSubUrl}}/user/settings/organization"> + {{.locale.Tr "settings.organization"}} + </a> + {{end}} <a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos"> {{.locale.Tr "settings.repos"}} </a>