diff options
Diffstat (limited to 'lib/private/AppFramework/Middleware/Security')
11 files changed, 240 insertions, 179 deletions
diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php index 7b617b22e3c..4453f5a7d4b 100644 --- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -21,6 +21,7 @@ use OCP\AppFramework\Middleware; use OCP\IRequest; use OCP\ISession; use OCP\Security\Bruteforce\IThrottler; +use Psr\Log\LoggerInterface; use ReflectionMethod; /** @@ -30,7 +31,7 @@ use ReflectionMethod; * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS */ class CORSMiddleware extends Middleware { - /** @var IRequest */ + /** @var IRequest */ private $request; /** @var ControllerMethodReflector */ private $reflector; @@ -39,10 +40,13 @@ class CORSMiddleware extends Middleware { /** @var IThrottler */ private $throttler; - public function __construct(IRequest $request, + public function __construct( + IRequest $request, ControllerMethodReflector $reflector, Session $session, - IThrottler $throttler) { + IThrottler $throttler, + private readonly LoggerInterface $logger, + ) { $this->request = $request; $this->reflector = $reflector; $this->session = $session; @@ -64,8 +68,8 @@ class CORSMiddleware extends Middleware { // ensure that @CORS annotated API routes are not used in conjunction // with session authentication since this enables CSRF attack vectors - if ($this->hasAnnotationOrAttribute($reflectionMethod, 'CORS', CORS::class) && - (!$this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class) || $this->session->isLoggedIn())) { + if ($this->hasAnnotationOrAttribute($reflectionMethod, 'CORS', CORS::class) + && (!$this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class) || $this->session->isLoggedIn())) { $user = array_key_exists('PHP_AUTH_USER', $this->request->server) ? $this->request->server['PHP_AUTH_USER'] : null; $pass = array_key_exists('PHP_AUTH_PW', $this->request->server) ? $this->request->server['PHP_AUTH_PW'] : null; @@ -98,6 +102,7 @@ class CORSMiddleware extends Middleware { */ protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool { if ($this->reflector->hasAnnotation($annotationName)) { + $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead'); return true; } @@ -129,10 +134,10 @@ class CORSMiddleware extends Middleware { // allow credentials headers must not be true or CSRF is possible // otherwise foreach ($response->getHeaders() as $header => $value) { - if (strtolower($header) === 'access-control-allow-credentials' && - strtolower(trim($value)) === 'true') { - $msg = 'Access-Control-Allow-Credentials must not be '. - 'set to true in order to prevent CSRF'; + if (strtolower($header) === 'access-control-allow-credentials' + && strtolower(trim($value)) === 'true') { + $msg = 'Access-Control-Allow-Credentials must not be ' + . 'set to true in order to prevent CSRF'; throw new SecurityException($msg); } } diff --git a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php index 2115c07c0fc..e88c9563c00 100644 --- a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php @@ -10,7 +10,6 @@ namespace OC\AppFramework\Middleware\Security; use OC\Security\CSP\ContentSecurityPolicyManager; use OC\Security\CSP\ContentSecurityPolicyNonceManager; -use OC\Security\CSRF\CsrfTokenManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\EmptyContentSecurityPolicy; @@ -18,19 +17,11 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; class CSPMiddleware extends Middleware { - /** @var ContentSecurityPolicyManager */ - private $contentSecurityPolicyManager; - /** @var ContentSecurityPolicyNonceManager */ - private $cspNonceManager; - /** @var CsrfTokenManager */ - private $csrfTokenManager; - - public function __construct(ContentSecurityPolicyManager $policyManager, - ContentSecurityPolicyNonceManager $cspNonceManager, - CsrfTokenManager $csrfTokenManager) { - $this->contentSecurityPolicyManager = $policyManager; - $this->cspNonceManager = $cspNonceManager; - $this->csrfTokenManager = $csrfTokenManager; + + public function __construct( + private ContentSecurityPolicyManager $policyManager, + private ContentSecurityPolicyNonceManager $cspNonceManager, + ) { } /** @@ -49,11 +40,11 @@ class CSPMiddleware extends Middleware { return $response; } - $defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy(); - $defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy); + $defaultPolicy = $this->policyManager->getDefaultPolicy(); + $defaultPolicy = $this->policyManager->mergePolicies($defaultPolicy, $policy); if ($this->cspNonceManager->browserSupportsCspV3()) { - $defaultPolicy->useJsNonce($this->csrfTokenManager->getToken()->getEncryptedValue()); + $defaultPolicy->useJsNonce($this->cspNonceManager->getNonce()); } $response->setContentSecurityPolicy($defaultPolicy); diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/AdminIpNotAllowedException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/AdminIpNotAllowedException.php new file mode 100644 index 00000000000..36eb8f18928 --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/AdminIpNotAllowedException.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\AppFramework\Middleware\Security\Exceptions; + +use OCP\AppFramework\Http; + +/** + * Class AdminIpNotAllowed is thrown when a resource has been requested by a + * an admin user connecting from an unauthorized IP address + * See configuration `allowed_admin_ranges` + * + * @package OC\AppFramework\Middleware\Security\Exceptions + */ +class AdminIpNotAllowedException extends SecurityException { + public function __construct(string $message) { + parent::__construct($message, Http::STATUS_FORBIDDEN); + } +} diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php index 646a5240bfc..53fbaaf5ed2 100644 --- a/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/ExAppRequiredException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/ExAppRequiredException.php new file mode 100644 index 00000000000..77bc7efebac --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/ExAppRequiredException.php @@ -0,0 +1,18 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\AppFramework\Middleware\Security\Exceptions; + +use OCP\AppFramework\Http; + +/** + * Class ExAppRequiredException is thrown when an endpoint can only be called by an ExApp but the caller is not an ExApp. + */ +class ExAppRequiredException extends SecurityException { + public function __construct() { + parent::__construct('ExApp required', Http::STATUS_PRECONDITION_FAILED); + } +} diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php index 91f1dba874d..0380c6781aa 100644 --- a/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php index 7e950f2c976..ca30f736fbc 100644 --- a/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -14,7 +15,7 @@ use OCP\AppFramework\Http; * @package OC\AppFramework\Middleware\Security\Exceptions */ class NotConfirmedException extends SecurityException { - public function __construct() { - parent::__construct('Password confirmation is required', Http::STATUS_FORBIDDEN); + public function __construct(string $message = 'Password confirmation is required') { + parent::__construct($message, Http::STATUS_FORBIDDEN); } } diff --git a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php index 5ff9d7386da..0facbffe504 100644 --- a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -8,6 +9,7 @@ namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Authentication\Token\IProvider; +use OC\User\Manager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Middleware; @@ -16,82 +18,81 @@ use OCP\Authentication\Exceptions\ExpiredTokenException; use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\Exceptions\WipeTokenException; use OCP\Authentication\Token\IToken; +use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; use OCP\Session\Exceptions\SessionNotAvailableException; use OCP\User\Backend\IPasswordConfirmationBackend; +use Psr\Log\LoggerInterface; use ReflectionMethod; class PasswordConfirmationMiddleware extends Middleware { - /** @var ControllerMethodReflector */ - private $reflector; - /** @var ISession */ - private $session; - /** @var IUserSession */ - private $userSession; - /** @var ITimeFactory */ - private $timeFactory; - /** @var array */ - private $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true]; - private IProvider $tokenProvider; + private array $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true]; - /** - * PasswordConfirmationMiddleware constructor. - * - * @param ControllerMethodReflector $reflector - * @param ISession $session - * @param IUserSession $userSession - * @param ITimeFactory $timeFactory - */ - public function __construct(ControllerMethodReflector $reflector, - ISession $session, - IUserSession $userSession, - ITimeFactory $timeFactory, - IProvider $tokenProvider, + public function __construct( + private ControllerMethodReflector $reflector, + private ISession $session, + private IUserSession $userSession, + private ITimeFactory $timeFactory, + private IProvider $tokenProvider, + private readonly LoggerInterface $logger, + private readonly IRequest $request, + private readonly Manager $userManager, ) { - $this->reflector = $reflector; - $this->session = $session; - $this->userSession = $userSession; - $this->timeFactory = $timeFactory; - $this->tokenProvider = $tokenProvider; } /** - * @param Controller $controller - * @param string $methodName * @throws NotConfirmedException */ - public function beforeController($controller, $methodName) { + public function beforeController(Controller $controller, string $methodName) { $reflectionMethod = new ReflectionMethod($controller, $methodName); - if ($this->hasAnnotationOrAttribute($reflectionMethod, 'PasswordConfirmationRequired', PasswordConfirmationRequired::class)) { - $user = $this->userSession->getUser(); - $backendClassName = ''; - if ($user !== null) { - $backend = $user->getBackend(); - if ($backend instanceof IPasswordConfirmationBackend) { - if (!$backend->canConfirmPassword($user->getUID())) { - return; - } - } + if (!$this->needsPasswordConfirmation($reflectionMethod)) { + return; + } - $backendClassName = $user->getBackendClassName(); + $user = $this->userSession->getUser(); + $backendClassName = ''; + if ($user !== null) { + $backend = $user->getBackend(); + if ($backend instanceof IPasswordConfirmationBackend) { + if (!$backend->canConfirmPassword($user->getUID())) { + return; + } } - try { - $sessionId = $this->session->getId(); - $token = $this->tokenProvider->getToken($sessionId); - } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) { - // States we do not deal with here. - return; + $backendClassName = $user->getBackendClassName(); + } + + try { + $sessionId = $this->session->getId(); + $token = $this->tokenProvider->getToken($sessionId); + } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) { + // States we do not deal with here. + return; + } + + $scope = $token->getScopeAsArray(); + if (isset($scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION]) && $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === true) { + // Users logging in from SSO backends cannot confirm their password by design + return; + } + + if ($this->isPasswordConfirmationStrict($reflectionMethod)) { + $authHeader = $this->request->getHeader('Authorization'); + if (!str_starts_with(strtolower($authHeader), 'basic ')) { + throw new NotConfirmedException('Required authorization header missing'); } - $scope = $token->getScopeAsArray(); - if (isset($scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION]) && $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === true) { - // Users logging in from SSO backends cannot confirm their password by design - return; + [, $password] = explode(':', base64_decode(substr($authHeader, 6)), 2); + $loginName = $this->session->get('loginname'); + $loginResult = $this->userManager->checkPassword($loginName, $password); + if ($loginResult === false) { + throw new NotConfirmedException(); } - $lastConfirm = (int) $this->session->get('last-password-confirm'); + $this->session->set('last-password-confirm', $this->timeFactory->getTime()); + } else { + $lastConfirm = (int)$this->session->get('last-password-confirm'); // TODO: confirm excludedUserBackEnds can go away and remove it if (!isset($this->excludedUserBackEnds[$backendClassName]) && $lastConfirm < ($this->timeFactory->getTime() - (30 * 60 + 15))) { // allow 15 seconds delay throw new NotConfirmedException(); @@ -99,23 +100,22 @@ class PasswordConfirmationMiddleware extends Middleware { } } - /** - * @template T - * - * @param ReflectionMethod $reflectionMethod - * @param string $annotationName - * @param class-string<T> $attributeClass - * @return boolean - */ - protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool { - if (!empty($reflectionMethod->getAttributes($attributeClass))) { + private function needsPasswordConfirmation(ReflectionMethod $reflectionMethod): bool { + $attributes = $reflectionMethod->getAttributes(PasswordConfirmationRequired::class); + if (!empty($attributes)) { return true; } - if ($this->reflector->hasAnnotation($annotationName)) { + if ($this->reflector->hasAnnotation('PasswordConfirmationRequired')) { + $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . 'PasswordConfirmationRequired' . ' annotation and should use the #[PasswordConfirmationRequired] attribute instead'); return true; } return false; } + + private function isPasswordConfirmationStrict(ReflectionMethod $reflectionMethod): bool { + $attributes = $reflectionMethod->getAttributes(PasswordConfirmationRequired::class); + return !empty($attributes) && ($attributes[0]->newInstance()->getStrict()); + } } diff --git a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php index d593bf5019f..2d19be97993 100644 --- a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php @@ -9,8 +9,10 @@ declare(strict_types=1); namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Security\Ip\BruteforceAllowList; use OC\Security\RateLimiting\Exception\RateLimitExceededException; use OC\Security\RateLimiting\Limiter; +use OC\User\Session; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\AnonRateLimit; use OCP\AppFramework\Http\Attribute\ARateLimit; @@ -19,6 +21,7 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; +use OCP\IAppConfig; use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; @@ -52,6 +55,8 @@ class RateLimitingMiddleware extends Middleware { protected ControllerMethodReflector $reflector, protected Limiter $limiter, protected ISession $session, + protected IAppConfig $appConfig, + protected BruteforceAllowList $bruteForceAllowList, ) { } @@ -63,8 +68,8 @@ class RateLimitingMiddleware extends Middleware { parent::beforeController($controller, $methodName); $rateLimitIdentifier = get_class($controller) . '::' . $methodName; - if ($this->session->exists('app_api_system')) { - // Bypass rate limiting for app_api + if ($this->userSession instanceof Session && $this->userSession->getSession()->get('app_api') === true && $this->userSession->getUser() === null) { + // if userId is not specified and the request is authenticated by AppAPI, we skip the rate limit return; } @@ -72,6 +77,11 @@ class RateLimitingMiddleware extends Middleware { $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class); if ($rateLimit !== null) { + if ($this->appConfig->getValueBool('bruteforcesettings', 'apply_allowlist_to_ratelimit') + && $this->bruteForceAllowList->isBypassListed($this->request->getRemoteAddress())) { + return; + } + $this->limiter->registerUserRequest( $rateLimitIdentifier, $rateLimit->getLimit(), @@ -111,8 +121,8 @@ class RateLimitingMiddleware extends Middleware { if ($annotationLimit !== '' && $annotationPeriod !== '') { return new $attributeClass( - (int) $annotationLimit, - (int) $annotationPeriod, + (int)$annotationLimit, + (int)$annotationPeriod, ); } diff --git a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php index e0bb96f132b..097ed1b2b8f 100644 --- a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -13,16 +14,10 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; class SameSiteCookieMiddleware extends Middleware { - /** @var Request */ - private $request; - - /** @var ControllerMethodReflector */ - private $reflector; - - public function __construct(Request $request, - ControllerMethodReflector $reflector) { - $this->request = $request; - $this->reflector = $reflector; + public function __construct( + private Request $request, + private ControllerMethodReflector $reflector, + ) { } public function beforeController($controller, $methodName) { @@ -46,19 +41,19 @@ class SameSiteCookieMiddleware extends Middleware { public function afterException($controller, $methodName, \Exception $exception) { if ($exception instanceof LaxSameSiteCookieFailedException) { - $respone = new Response(); - $respone->setStatus(Http::STATUS_FOUND); - $respone->addHeader('Location', $this->request->getRequestUri()); + $response = new Response(); + $response->setStatus(Http::STATUS_FOUND); + $response->addHeader('Location', $this->request->getRequestUri()); $this->setSameSiteCookie(); - return $respone; + return $response; } throw $exception; } - protected function setSameSiteCookie() { + protected function setSameSiteCookie(): void { $cookieParams = $this->request->getCookieParams(); $secureCookie = ($cookieParams['secure'] === true) ? 'secure; ' : ''; $policies = [ diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index a38ad610fc6..e3a293e0fd9 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -8,18 +8,23 @@ declare(strict_types=1); */ namespace OC\AppFramework\Middleware\Security; +use OC\AppFramework\Middleware\Security\Exceptions\AdminIpNotAllowedException; use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException; use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException; +use OC\AppFramework\Middleware\Security\Exceptions\ExAppRequiredException; use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException; use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Settings\AuthorizedGroupMapper; +use OC\User\Session; use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AppApiAdminAccessWithoutUser; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; +use OCP\AppFramework\Http\Attribute\ExAppRequired; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\PublicPage; @@ -31,11 +36,14 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; use OCP\AppFramework\OCSController; +use OCP\Group\ISubAdmin; +use OCP\IGroupManager; use OCP\IL10N; use OCP\INavigationManager; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserSession; +use OCP\Security\Ip\IRemoteAddress; use OCP\Util; use Psr\Log\LoggerInterface; use ReflectionMethod; @@ -47,60 +55,41 @@ use ReflectionMethod; * check fails */ class SecurityMiddleware extends Middleware { - /** @var INavigationManager */ - private $navigationManager; - /** @var IRequest */ - private $request; - /** @var ControllerMethodReflector */ - private $reflector; - /** @var string */ - private $appName; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var LoggerInterface */ - private $logger; - /** @var bool */ - private $isLoggedIn; - /** @var bool */ - private $isAdminUser; - /** @var bool */ - private $isSubAdmin; - /** @var IAppManager */ - private $appManager; - /** @var IL10N */ - private $l10n; - /** @var AuthorizedGroupMapper */ - private $groupAuthorizationMapper; - /** @var IUserSession */ - private $userSession; - - public function __construct(IRequest $request, - ControllerMethodReflector $reflector, - INavigationManager $navigationManager, - IURLGenerator $urlGenerator, - LoggerInterface $logger, - string $appName, - bool $isLoggedIn, - bool $isAdminUser, - bool $isSubAdmin, - IAppManager $appManager, - IL10N $l10n, - AuthorizedGroupMapper $mapper, - IUserSession $userSession + private ?bool $isAdminUser = null; + private ?bool $isSubAdmin = null; + + public function __construct( + private IRequest $request, + private ControllerMethodReflector $reflector, + private INavigationManager $navigationManager, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, + private string $appName, + private bool $isLoggedIn, + private IGroupManager $groupManager, + private ISubAdmin $subAdminManager, + private IAppManager $appManager, + private IL10N $l10n, + private AuthorizedGroupMapper $groupAuthorizationMapper, + private IUserSession $userSession, + private IRemoteAddress $remoteAddress, ) { - $this->navigationManager = $navigationManager; - $this->request = $request; - $this->reflector = $reflector; - $this->appName = $appName; - $this->urlGenerator = $urlGenerator; - $this->logger = $logger; - $this->isLoggedIn = $isLoggedIn; - $this->isAdminUser = $isAdminUser; - $this->isSubAdmin = $isSubAdmin; - $this->appManager = $appManager; - $this->l10n = $l10n; - $this->groupAuthorizationMapper = $mapper; - $this->userSession = $userSession; + } + + private function isAdminUser(): bool { + if ($this->isAdminUser === null) { + $user = $this->userSession->getUser(); + $this->isAdminUser = $user && $this->groupManager->isAdmin($user->getUID()); + } + return $this->isAdminUser; + } + + private function isSubAdmin(): bool { + if ($this->isSubAdmin === null) { + $user = $this->userSession->getUser(); + $this->isSubAdmin = $user && $this->subAdminManager->isSubAdmin($user); + } + return $this->isSubAdmin; } /** @@ -127,16 +116,29 @@ class SecurityMiddleware extends Middleware { // security checks $isPublicPage = $this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class); - if (!$isPublicPage) { - if (!$this->isLoggedIn) { - throw new NotLoggedInException(); + + if ($this->hasAnnotationOrAttribute($reflectionMethod, 'ExAppRequired', ExAppRequired::class)) { + if (!$this->userSession instanceof Session || $this->userSession->getSession()->get('app_api') !== true) { + throw new ExAppRequiredException(); } + } elseif (!$isPublicPage) { $authorized = false; - if ($this->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) { - $authorized = $this->isAdminUser; + if ($this->hasAnnotationOrAttribute($reflectionMethod, null, AppApiAdminAccessWithoutUser::class)) { + // this attribute allows ExApp to access admin endpoints only if "userId" is "null" + if ($this->userSession instanceof Session && $this->userSession->getSession()->get('app_api') === true && $this->userSession->getUser() === null) { + $authorized = true; + } + } + + if (!$authorized && !$this->isLoggedIn) { + throw new NotLoggedInException(); + } + + if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) { + $authorized = $this->isAdminUser(); if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)) { - $authorized = $this->isSubAdmin; + $authorized = $this->isSubAdmin(); } if (!$authorized) { @@ -153,24 +155,37 @@ class SecurityMiddleware extends Middleware { if (!$authorized) { throw new NotAdminException($this->l10n->t('Logged in account must be an admin, a sub admin or gotten special right to access this setting')); } + if (!$this->remoteAddress->allowsAdminActions()) { + throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions')); + } } if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) - && !$this->isSubAdmin - && !$this->isAdminUser + && !$this->isSubAdmin() + && !$this->isAdminUser() && !$authorized) { throw new NotAdminException($this->l10n->t('Logged in account must be an admin or sub admin')); } if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) && !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class) - && !$this->isAdminUser + && !$this->isAdminUser() && !$authorized) { throw new NotAdminException($this->l10n->t('Logged in account must be an admin')); } + if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) + && !$this->remoteAddress->allowsAdminActions()) { + throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions')); + } + if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) + && !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class) + && !$this->remoteAddress->allowsAdminActions()) { + throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions')); + } + } // Check for strict cookie requirement - if ($this->hasAnnotationOrAttribute($reflectionMethod, 'StrictCookieRequired', StrictCookiesRequired::class) || - !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) { + if ($this->hasAnnotationOrAttribute($reflectionMethod, 'StrictCookieRequired', StrictCookiesRequired::class) + || !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) { if (!$this->request->passesStrictCookieCheck()) { throw new StrictCookieMissingException(); } @@ -225,16 +240,17 @@ class SecurityMiddleware extends Middleware { * @template T * * @param ReflectionMethod $reflectionMethod - * @param string $annotationName + * @param ?string $annotationName * @param class-string<T> $attributeClass * @return boolean */ - protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool { + protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, ?string $annotationName, string $attributeClass): bool { if (!empty($reflectionMethod->getAttributes($attributeClass))) { return true; } - if ($this->reflector->hasAnnotation($annotationName)) { + if ($annotationName && $this->reflector->hasAnnotation($annotationName)) { + $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead'); return true; } |