diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 051b65dd55..6a30600a5d 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1561,15 +1561,17 @@ LEVEL = Info ;DEFAULT_EMAIL_NOTIFICATIONS = enabled ;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false ;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false -;; 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_password" 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_password: a user cannot configure their password ;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_password`. 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_password: a user cannot configure their password ;;EXTERNAL_USER_DISABLE_FEATURES = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/setting/admin.go b/modules/setting/admin.go index 7a1e071bac..f383a5e382 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -26,7 +26,8 @@ func loadAdminFrom(rootCfg ConfigProvider) { } const ( - UserFeatureDeletion = "deletion" - UserFeatureManageSSHKeys = "manage_ssh_keys" - UserFeatureManageGPGKeys = "manage_gpg_keys" + UserFeatureDeletion = "deletion" + UserFeatureManageSSHKeys = "manage_ssh_keys" + UserFeatureManageGPGKeys = "manage_gpg_keys" + UserFeatureManagePassword = "manage_password" ) diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 1dfcc90e35..5b0e8b4970 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -46,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.UserFeatureManagePassword) { + ctx.NotFound("Not Found", errors.New("password is not allowed to be changed")) + return + } + form := web.GetForm(ctx).(*forms.ChangePasswordForm) ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index 35f1d07b60..6f81b255f1 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -4,7 +4,7 @@ {{ctx.Locale.Tr "settings.change_password"}}
- {{if or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2)}} + {{if and (not ($.UserDisabledFeatures.Contains "manage_password")) (or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2))}}
{{template "base/disable_form_autofill"}} {{if .SignedUser.IsPasswordSet}} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index ad0b09305f..9434bffa9a 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -708,16 +708,23 @@ func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile func GetHTMLTitle(t testing.TB, session *TestSession, urlStr string) string { t.Helper() + doc := getHTMLDoc(t, session, urlStr, http.StatusOK) + return doc.Find("head title").Text() +} + +// getHTMLDoc gets HTMLDoc from url with expected status. Use status +// NoExpectedStatus to ignore status. +func getHTMLDoc(t testing.TB, session *TestSession, urlStr string, expectedStatus int) *HTMLDoc { + t.Helper() + req := NewRequest(t, "GET", urlStr) var resp *httptest.ResponseRecorder if session == nil { - resp = MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, expectedStatus) } else { - resp = session.MakeRequest(t, req, http.StatusOK) + resp = session.MakeRequest(t, req, expectedStatus) } - - doc := NewHTMLParser(t, resp.Body) - return doc.Find("head title").Text() + return NewHTMLParser(t, resp.Body) } func SortMailerMessages(msgs []*mailer.Message) { diff --git a/tests/integration/user_settings_test.go b/tests/integration/user_settings_test.go new file mode 100644 index 0000000000..9a9e58da49 --- /dev/null +++ b/tests/integration/user_settings_test.go @@ -0,0 +1,126 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package integration + +import ( + "net/http" + "testing" + + "forgejo.org/modules/container" + "forgejo.org/modules/setting" + "forgejo.org/modules/test" + "forgejo.org/tests" +) + +// TestUserSettingsAccount tests the contents of a user's account settings +// with(out) disabled user features. +func TestUserSettingsAccount(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("all features enabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + doc := getHTMLDoc(t, loginUser(t, "user2"), "/user/settings/account", http.StatusOK) + doc.AssertElement(t, "#password", true) + doc.AssertElement(t, "#email", true) + doc.AssertElement(t, "#delete-form", true) + }) + + t.Run("password disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + disabled := container.SetOf(setting.UserFeatureManagePassword) + defer test.MockVariableValue(&setting.Admin.UserDisabledFeatures, disabled)() + defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, disabled)() + + doc := getHTMLDoc(t, loginUser(t, "user2"), "/user/settings/account", http.StatusOK) + doc.AssertElement(t, "#password", false) + doc.AssertElement(t, "#email", true) + doc.AssertElement(t, "#delete-form", true) + }) + + t.Run("deletion disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + disabled := container.SetOf(setting.UserFeatureDeletion) + defer test.MockVariableValue(&setting.Admin.UserDisabledFeatures, disabled)() + defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, disabled)() + + doc := getHTMLDoc(t, loginUser(t, "user2"), "/user/settings/account", http.StatusOK) + doc.AssertElement(t, "#password", true) + doc.AssertElement(t, "#email", true) + doc.AssertElement(t, "#delete-form", false) + }) + + t.Run("deletion, password disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + disabled := container.SetOf( + setting.UserFeatureDeletion, + setting.UserFeatureManagePassword, + ) + defer test.MockVariableValue(&setting.Admin.UserDisabledFeatures, disabled)() + defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, disabled)() + + doc := getHTMLDoc(t, loginUser(t, "user2"), "/user/settings/account", http.StatusOK) + doc.AssertElement(t, "#password", false) + doc.AssertElement(t, "#email", true) + doc.AssertElement(t, "#delete-form", false) + }) +} + +// TestUserSettingsUpdatePassword tests updating a user's password with(out) +// disabled user features. +func TestUserSettingsUpdatePassword(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("password enabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // changing password should work + session := loginUser(t, "user2") + req := NewRequestWithValues(t, "POST", "/user/settings/account", map[string]string{ + "old_password": "password", + "password": "password", + "retype": "password", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + }) + + t.Run("password disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + disabled := container.SetOf(setting.UserFeatureManagePassword) + defer test.MockVariableValue(&setting.Admin.UserDisabledFeatures, disabled)() + defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, disabled)() + + // changing password should not work + session := loginUser(t, "user2") + req := NewRequestWithValues(t, "POST", "/user/settings/account", map[string]string{ + "old_password": "password", + "password": "password", + "retype": "password", + }) + session.MakeRequest(t, req, http.StatusNotFound) + }) +} + +// TestUserSettingsDelete tests deleting a user with(out) disabled user +// features. +func TestUserSettingsDelete(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("deletion disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + disabled := container.SetOf(setting.UserFeatureDeletion) + defer test.MockVariableValue(&setting.Admin.UserDisabledFeatures, disabled)() + defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, disabled)() + + // deleting user should not work + session := loginUser(t, "user2") + req := NewRequest(t, "POST", "/user/settings/account/delete") + session.MakeRequest(t, req, http.StatusNotFound) + }) +}