From ce8baf75c63f3aa9eb25b938670a038f2f85df95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 26 Jun 2025 19:21:30 +0200 Subject: [PATCH 1/2] fix: Throw specific LoginException when the user is disabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/User/DisabledUserException.php | 10 ++++++++++ lib/private/User/Session.php | 2 +- lib/private/legacy/OC_User.php | 4 ++-- 5 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 lib/private/User/DisabledUserException.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index fc0694124f8c9..5b7821a74387b 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -2090,6 +2090,7 @@ 'OC\\User\\Backend' => $baseDir . '/lib/private/User/Backend.php', 'OC\\User\\BackgroundJobs\\CleanupDeletedUsers' => $baseDir . '/lib/private/User/BackgroundJobs/CleanupDeletedUsers.php', 'OC\\User\\Database' => $baseDir . '/lib/private/User/Database.php', + 'OC\\User\\DisabledUserException' => $baseDir . '/lib/private/User/DisabledUserException.php', 'OC\\User\\DisplayNameCache' => $baseDir . '/lib/private/User/DisplayNameCache.php', 'OC\\User\\LazyUser' => $baseDir . '/lib/private/User/LazyUser.php', 'OC\\User\\Listeners\\BeforeUserDeletedListener' => $baseDir . '/lib/private/User/Listeners/BeforeUserDeletedListener.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 98d74bd5dce94..1c22c0957f2f2 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -2139,6 +2139,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\User\\Backend' => __DIR__ . '/../../..' . '/lib/private/User/Backend.php', 'OC\\User\\BackgroundJobs\\CleanupDeletedUsers' => __DIR__ . '/../../..' . '/lib/private/User/BackgroundJobs/CleanupDeletedUsers.php', 'OC\\User\\Database' => __DIR__ . '/../../..' . '/lib/private/User/Database.php', + 'OC\\User\\DisabledUserException' => __DIR__ . '/../../..' . '/lib/private/User/DisabledUserException.php', 'OC\\User\\DisplayNameCache' => __DIR__ . '/../../..' . '/lib/private/User/DisplayNameCache.php', 'OC\\User\\LazyUser' => __DIR__ . '/../../..' . '/lib/private/User/LazyUser.php', 'OC\\User\\Listeners\\BeforeUserDeletedListener' => __DIR__ . '/../../..' . '/lib/private/User/Listeners/BeforeUserDeletedListener.php', diff --git a/lib/private/User/DisabledUserException.php b/lib/private/User/DisabledUserException.php new file mode 100644 index 0000000000000..db8a23d202752 --- /dev/null +++ b/lib/private/User/DisabledUserException.php @@ -0,0 +1,10 @@ +t('Account disabled'); - throw new LoginException($message); + throw new DisabledUserException($message); } if ($regenerateSessionId) { diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php index 4cc102d6672b4..6fcf740397881 100644 --- a/lib/private/legacy/OC_User.php +++ b/lib/private/legacy/OC_User.php @@ -6,7 +6,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ use OC\Authentication\Token\IProvider; -use OC\User\LoginException; +use OC\User\DisabledUserException; use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\Exceptions\WipeTokenException; use OCP\Authentication\Token\IToken; @@ -157,7 +157,7 @@ public static function loginWithApache(\OCP\Authentication\IApacheBackend $backe if ($userSession->getUser() && !$userSession->getUser()->isEnabled()) { $message = \OC::$server->getL10N('lib')->t('Account disabled'); - throw new LoginException($message); + throw new DisabledUserException($message); } $userSession->setLoginName($uid); $request = OC::$server->getRequest(); From ac6b07356d305bb773b057fb28a323106e50e8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 5 Jun 2025 04:25:34 +0200 Subject: [PATCH 2/2] fix: Fix theming for disabled accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Theming app injects the stylesheets for the different themes in the "
" element of the page, and those stylesheets are then loaded by the browser from a "Controller" (a plain "Controller", not an "OCSController"). The stylesheets, in turn, may also get some images (like the background) also from the "Controller". When handling a request to "index.php" it is checked whether the user is logged in and, if not, a login is tried. A disabled user is explicitly seen as not logged in, so a login is always tried in that case, but disabled users are also explicitly prevented to log in, so the login also fails. Due to that trying to get any of the themed stylesheets or images with a disabled account (to be able to show the "Account disabled" error page) fails with an HTTP status 401. To solve that, and to avoid touching this basic logic as much as possible, the login exception is now ignored (if the user is disabled) for some specific requests to the Theming app. Signed-off-by: Daniel Calviño Sánchez --- .github/workflows/integration-sqlite.yml | 1 + build/integration/config/behat.yml | 12 +- .../features/bootstrap/BasicStructure.php | 1 + .../features/bootstrap/Theming.php | 49 +++++++ .../theming_features/theming.feature | 131 ++++++++++++++++++ lib/base.php | 23 ++- 6 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 build/integration/features/bootstrap/Theming.php create mode 100644 build/integration/theming_features/theming.feature diff --git a/.github/workflows/integration-sqlite.yml b/.github/workflows/integration-sqlite.yml index 5eb89c23761f4..c0d3f701a498e 100644 --- a/.github/workflows/integration-sqlite.yml +++ b/.github/workflows/integration-sqlite.yml @@ -69,6 +69,7 @@ jobs: - 'setup_features' - 'sharees_features' - 'sharing_features' + - 'theming_features' - 'videoverification_features' php-versions: ['8.1'] diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index a3018c3966c0a..f27183e920c48 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -255,4 +255,14 @@ default: admin: - admin - admin - regular_user_password: 123456 \ No newline at end of file + regular_user_password: 123456 + theming: + paths: + - "%paths.base%/../theming_features" + contexts: + - FeatureContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index a8c232d6fe744..0290297dd884f 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -19,6 +19,7 @@ trait BasicStructure { use Avatar; use Download; use Mail; + use Theming; /** @var string */ private $currentUser = ''; diff --git a/build/integration/features/bootstrap/Theming.php b/build/integration/features/bootstrap/Theming.php new file mode 100644 index 0000000000000..f44a6533a1bda --- /dev/null +++ b/build/integration/features/bootstrap/Theming.php @@ -0,0 +1,49 @@ +undoAllThemingChangesAfterScenario) { + return; + } + + $this->loggingInUsingWebAs('admin'); + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/undoAllChanges'); + + $this->undoAllThemingChangesAfterScenario = false; + } + + /** + * @When logged in admin uploads theming image for :key from file :source + * + * @param string $key + * @param string $source + */ + public function loggedInAdminUploadsThemingImageForFromFile(string $key, string $source) { + $this->undoAllThemingChangesAfterScenario = true; + + $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); + + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/uploadImage?key=' . $key, + [ + 'multipart' => [ + [ + 'name' => 'image', + 'contents' => $file + ] + ] + ]); + $this->theHTTPStatusCodeShouldBe('200'); + } +} diff --git a/build/integration/theming_features/theming.feature b/build/integration/theming_features/theming.feature new file mode 100644 index 0000000000000..2ae5d4f75c377 --- /dev/null +++ b/build/integration/theming_features/theming.feature @@ -0,0 +1,131 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +Feature: theming + + Background: + Given user "user0" exists + + Scenario: themed stylesheets are available for users + Given As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed stylesheets are available for guests + Given As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + # Themes that can not be explicitly set by a guest could have been + # globally set too through "enforce_theme". + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed stylesheets are available for disabled users + Given As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed images are available for users + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed images are available for guests + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed images are available for disabled users + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for users + Given As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for guests + Given As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for disabled users + Given As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" diff --git a/lib/base.php b/lib/base.php index f8a90bd63f269..63f2e758428f7 100644 --- a/lib/base.php +++ b/lib/base.php @@ -11,6 +11,7 @@ use OC\Share20\Hooks; use OC\Share20\UserDeletedListener; use OC\Share20\UserRemovedListener; +use OC\User\DisabledUserException; use OCP\EventDispatcher\IEventDispatcher; use OCP\Group\Events\GroupDeletedEvent; use OCP\Group\Events\UserRemovedEvent; @@ -1026,7 +1027,27 @@ public static function handleRequest(): void { // OAuth needs to support basic auth too, so the login is not valid // inside Nextcloud and the Login exception would ruin it. if ($request->getRawPathInfo() !== '/apps/oauth2/api/v1/token') { - self::handleLogin($request); + try { + self::handleLogin($request); + } catch (DisabledUserException $e) { + // Disabled users would not be seen as logged in and + // trying to log them in would fail, so the login + // exception is ignored for the themed stylesheets and + // images. + if ($request->getRawPathInfo() !== '/apps/theming/theme/default.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/light.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/dark.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/light-highcontrast.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/dark-highcontrast.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/opendyslexic.css' + && $request->getRawPathInfo() !== '/apps/theming/image/background' + && $request->getRawPathInfo() !== '/apps/theming/image/logo' + && $request->getRawPathInfo() !== '/apps/theming/image/logoheader' + && !str_starts_with($request->getRawPathInfo(), '/apps/theming/favicon') + && !str_starts_with($request->getRawPathInfo(), '/apps/theming/icon')) { + throw $e; + } + } } } }