aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php')
-rw-r--r--lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php269
1 files changed, 158 insertions, 111 deletions
diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
index eb8c1b8dc43..e3a293e0fd9 100644
--- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
@@ -1,66 +1,52 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bernhard Posselt <dev@bernhard-posselt.com>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Holger Hees <holger.hees@gmail.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julien Veyssier <eneiluj@posteo.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Stefan Weil <sw@weilnetz.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Thomas Tanghus <thomas@tanghus.net>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
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;
+use OCP\AppFramework\Http\Attribute\StrictCookiesRequired;
+use OCP\AppFramework\Http\Attribute\SubAdminRequired;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse;
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;
/**
* Used to do all the authentication and checking stuff for a controller method
@@ -69,60 +55,41 @@ use Psr\Log\LoggerInterface;
* 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;
}
/**
@@ -145,22 +112,37 @@ class SecurityMiddleware extends Middleware {
$this->navigationManager->setActiveEntry('spreed');
}
+ $reflectionMethod = new ReflectionMethod($controller, $methodName);
+
// security checks
- $isPublicPage = $this->reflector->hasAnnotation('PublicPage');
- if (!$isPublicPage) {
- if (!$this->isLoggedIn) {
- throw new NotLoggedInException();
+ $isPublicPage = $this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class);
+
+ 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->reflector->hasAnnotation('AuthorizedAdminSetting')) {
- $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->reflector->hasAnnotation('SubAdminRequired')) {
- $authorized = $this->isSubAdmin;
+ if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)) {
+ $authorized = $this->isSubAdmin();
}
if (!$authorized) {
- $settingClasses = explode(';', $this->reflector->getAnnotationParameter('AuthorizedAdminSetting', 'settings'));
+ $settingClasses = $this->getAuthorizedAdminSettingClasses($reflectionMethod);
$authorizedClasses = $this->groupAuthorizationMapper->findAllClassesForUser($this->userSession->getUser());
foreach ($settingClasses as $settingClass) {
$authorized = in_array($settingClass, $authorizedClasses, true);
@@ -171,32 +153,46 @@ class SecurityMiddleware extends Middleware {
}
}
if (!$authorized) {
- throw new NotAdminException($this->l10n->t('Logged in user must be an admin, a sub admin or gotten special right to access this setting'));
+ 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->reflector->hasAnnotation('SubAdminRequired')
- && !$this->isSubAdmin
- && !$this->isAdminUser
+ if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
+ && !$this->isSubAdmin()
+ && !$this->isAdminUser()
&& !$authorized) {
- throw new NotAdminException($this->l10n->t('Logged in user must be an admin or sub admin'));
+ throw new NotAdminException($this->l10n->t('Logged in account must be an admin or sub admin'));
}
- if (!$this->reflector->hasAnnotation('SubAdminRequired')
- && !$this->reflector->hasAnnotation('NoAdminRequired')
- && !$this->isAdminUser
+ if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
+ && !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
+ && !$this->isAdminUser()
&& !$authorized) {
- throw new NotAdminException($this->l10n->t('Logged in user must be an admin'));
+ 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->reflector->hasAnnotation('StrictCookieRequired') || !$this->reflector->hasAnnotation('NoCSRFRequired')) {
+ if ($this->hasAnnotationOrAttribute($reflectionMethod, 'StrictCookieRequired', StrictCookiesRequired::class)
+ || !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
if (!$this->request->passesStrictCookieCheck()) {
throw new StrictCookieMissingException();
}
}
// CSRF check - also registers the CSRF token since the session may be closed later
Util::callRegister();
- if (!$this->reflector->hasAnnotation('NoCSRFRequired')) {
+ if ($this->isInvalidCSRFRequired($reflectionMethod)) {
/*
* Only allow the CSRF check to fail on OCS Requests. This kind of
* hacks around that we have no full token auth in place yet and we
@@ -205,12 +201,7 @@ class SecurityMiddleware extends Middleware {
* Additionally we allow Bearer authenticated requests to pass on OCS routes.
* This allows oauth apps (e.g. moodle) to use the OCS endpoints
*/
- if (!$this->request->passesCSRFCheck() && !(
- $controller instanceof OCSController && (
- $this->request->getHeader('OCS-APIREQUEST') === 'true' ||
- strpos($this->request->getHeader('Authorization'), 'Bearer ') === 0
- )
- )) {
+ if (!$controller instanceof OCSController || !$this->isValidOCSRequest()) {
throw new CrossSiteRequestForgeryException();
}
}
@@ -232,6 +223,62 @@ class SecurityMiddleware extends Middleware {
}
}
+ private function isInvalidCSRFRequired(ReflectionMethod $reflectionMethod): bool {
+ if ($this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
+ return false;
+ }
+
+ return !$this->request->passesCSRFCheck();
+ }
+
+ private function isValidOCSRequest(): bool {
+ return $this->request->getHeader('OCS-APIREQUEST') === 'true'
+ || str_starts_with($this->request->getHeader('Authorization'), 'Bearer ');
+ }
+
+ /**
+ * @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))) {
+ return true;
+ }
+
+ 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;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param ReflectionMethod $reflectionMethod
+ * @return string[]
+ */
+ protected function getAuthorizedAdminSettingClasses(ReflectionMethod $reflectionMethod): array {
+ $classes = [];
+ if ($this->reflector->hasAnnotation('AuthorizedAdminSetting')) {
+ $classes = explode(';', $this->reflector->getAnnotationParameter('AuthorizedAdminSetting', 'settings'));
+ }
+
+ $attributes = $reflectionMethod->getAttributes(AuthorizedAdminSetting::class);
+ if (!empty($attributes)) {
+ foreach ($attributes as $attribute) {
+ /** @var AuthorizedAdminSetting $setting */
+ $setting = $attribute->newInstance();
+ $classes[] = $setting->getSettings();
+ }
+ }
+
+ return $classes;
+ }
+
/**
* If an SecurityException is being caught, ajax requests return a JSON error
* response and non ajax requests redirect to the index