2013-08-17 05:16:48 -04:00
< ? php
2019-12-03 13:57:53 -05:00
2018-03-08 04:11:47 -05:00
declare ( strict_types = 1 );
2013-08-17 05:16:48 -04:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2016 - 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2013-08-17 05:16:48 -04:00
*/
namespace OC\AppFramework\Middleware\Security ;
2016-04-22 09:28:48 -04:00
use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException ;
use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException ;
2024-06-07 05:34:40 -04:00
use OC\AppFramework\Middleware\Security\Exceptions\ExAppRequiredException ;
2016-04-22 09:28:48 -04:00
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException ;
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException ;
2019-11-22 14:52:10 -05:00
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException ;
2016-07-20 11:37:30 -04:00
use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException ;
2014-05-06 10:29:19 -04:00
use OC\AppFramework\Utility\ControllerMethodReflector ;
2021-07-22 05:41:29 -04:00
use OC\Settings\AuthorizedGroupMapper ;
2024-06-07 05:34:40 -04:00
use OC\User\Session ;
2018-03-08 05:05:18 -05:00
use OCP\App\AppPathNotFoundException ;
2017-10-23 17:40:17 -04:00
use OCP\App\IAppManager ;
2019-11-22 14:52:10 -05:00
use OCP\AppFramework\Controller ;
2023-04-24 11:13:18 -04:00
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting ;
2024-06-07 05:34:40 -04:00
use OCP\AppFramework\Http\Attribute\ExAppRequired ;
2023-04-24 11:13:18 -04:00
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 ;
2019-11-22 14:52:10 -05:00
use OCP\AppFramework\Http\JSONResponse ;
2014-04-20 10:12:46 -04:00
use OCP\AppFramework\Http\RedirectResponse ;
2019-11-22 14:52:10 -05:00
use OCP\AppFramework\Http\Response ;
2015-11-28 05:06:46 -05:00
use OCP\AppFramework\Http\TemplateResponse ;
2013-10-05 10:59:06 -04:00
use OCP\AppFramework\Middleware ;
2016-07-29 07:41:30 -04:00
use OCP\AppFramework\OCSController ;
2018-02-26 09:32:17 -05:00
use OCP\IL10N ;
2014-05-27 20:12:01 -04:00
use OCP\INavigationManager ;
2013-10-06 18:33:54 -04:00
use OCP\IRequest ;
2019-11-22 14:52:10 -05:00
use OCP\IURLGenerator ;
2021-07-22 05:41:29 -04:00
use OCP\IUserSession ;
2014-11-17 09:10:47 -05:00
use OCP\Util ;
2021-04-16 08:26:43 -04:00
use Psr\Log\LoggerInterface ;
2023-04-24 11:13:18 -04:00
use ReflectionMethod ;
2013-08-17 05:16:48 -04:00
/**
* Used to do all the authentication and checking stuff for a controller method
* It reads out the annotations of a controller method and checks which if
* security things should be checked and also handles errors in case a security
* check fails
*/
class SecurityMiddleware extends Middleware {
2016-01-28 08:33:02 -05:00
/** @var INavigationManager */
2014-05-27 20:12:01 -04:00
private $navigationManager ;
2016-01-28 08:33:02 -05:00
/** @var IRequest */
2013-08-17 05:16:48 -04:00
private $request ;
2016-01-28 08:33:02 -05:00
/** @var ControllerMethodReflector */
2014-05-06 13:13:59 -04:00
private $reflector ;
2016-01-28 08:33:02 -05:00
/** @var string */
2014-05-27 20:12:01 -04:00
private $appName ;
2016-01-28 08:33:02 -05:00
/** @var IURLGenerator */
2014-05-27 20:12:01 -04:00
private $urlGenerator ;
2021-04-16 08:26:43 -04:00
/** @var LoggerInterface */
2014-05-27 20:12:01 -04:00
private $logger ;
2017-04-12 16:14:11 -04:00
/** @var bool */
private $isLoggedIn ;
2016-01-28 08:33:02 -05:00
/** @var bool */
2014-05-27 20:12:01 -04:00
private $isAdminUser ;
2019-05-22 04:48:51 -04:00
/** @var bool */
private $isSubAdmin ;
2017-10-23 17:40:17 -04:00
/** @var IAppManager */
private $appManager ;
2018-02-26 09:32:17 -05:00
/** @var IL10N */
private $l10n ;
2021-07-22 05:41:29 -04:00
/** @var AuthorizedGroupMapper */
private $groupAuthorizationMapper ;
/** @var IUserSession */
private $userSession ;
2014-05-06 13:13:59 -04:00
2014-05-27 20:12:01 -04:00
public function __construct ( IRequest $request ,
2023-11-23 04:22:34 -05:00
ControllerMethodReflector $reflector ,
INavigationManager $navigationManager ,
IURLGenerator $urlGenerator ,
LoggerInterface $logger ,
string $appName ,
bool $isLoggedIn ,
bool $isAdminUser ,
bool $isSubAdmin ,
IAppManager $appManager ,
IL10N $l10n ,
AuthorizedGroupMapper $mapper ,
IUserSession $userSession
2017-12-13 08:41:56 -05:00
) {
2014-05-27 20:12:01 -04:00
$this -> navigationManager = $navigationManager ;
2013-08-17 05:16:48 -04:00
$this -> request = $request ;
2014-05-06 10:29:19 -04:00
$this -> reflector = $reflector ;
2014-05-27 20:12:01 -04:00
$this -> appName = $appName ;
$this -> urlGenerator = $urlGenerator ;
$this -> logger = $logger ;
2017-04-12 16:14:11 -04:00
$this -> isLoggedIn = $isLoggedIn ;
2014-05-27 20:12:01 -04:00
$this -> isAdminUser = $isAdminUser ;
2019-05-22 04:48:51 -04:00
$this -> isSubAdmin = $isSubAdmin ;
2017-10-23 17:40:17 -04:00
$this -> appManager = $appManager ;
2018-02-26 09:32:17 -05:00
$this -> l10n = $l10n ;
2021-07-22 05:41:29 -04:00
$this -> groupAuthorizationMapper = $mapper ;
$this -> userSession = $userSession ;
2013-08-17 05:16:48 -04:00
}
/**
* This runs all the security checks before a method call . The
* security checks are determined by inspecting the controller method
* annotations
2021-07-22 05:41:29 -04:00
*
2016-07-29 07:41:30 -04:00
* @ param Controller $controller the controller
2013-08-17 05:16:48 -04:00
* @ param string $methodName the name of the method
* @ throws SecurityException when a security check fails
2019-11-19 10:16:26 -05:00
*
* @ suppress PhanUndeclaredClassConstant
2013-08-17 05:16:48 -04:00
*/
2017-08-01 11:32:03 -04:00
public function beforeController ( $controller , $methodName ) {
2013-08-17 05:16:48 -04:00
// this will set the current navigation entry of the app, use this only
// for normal HTML requests and not for AJAX requests
2014-05-27 20:12:01 -04:00
$this -> navigationManager -> setActiveEntry ( $this -> appName );
2013-08-17 05:16:48 -04:00
2020-01-21 10:35:10 -05:00
if ( get_class ( $controller ) === \OCA\Talk\Controller\PageController :: class && $methodName === 'showCall' ) {
2019-11-07 17:40:02 -05:00
$this -> navigationManager -> setActiveEntry ( 'spreed' );
}
2023-04-24 11:13:18 -04:00
$reflectionMethod = new ReflectionMethod ( $controller , $methodName );
2013-08-17 05:16:48 -04:00
// security checks
2023-04-24 11:13:18 -04:00
$isPublicPage = $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'PublicPage' , PublicPage :: class );
2024-06-07 05:34:40 -04:00
if ( $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'ExAppRequired' , ExAppRequired :: class )) {
if ( ! $this -> userSession instanceof Session || $this -> userSession -> getSession () -> get ( 'app_api' ) !== true ) {
throw new ExAppRequiredException ();
}
} elseif ( ! $isPublicPage ) {
2020-04-10 08:19:56 -04:00
if ( ! $this -> isLoggedIn ) {
2015-11-28 05:06:46 -05:00
throw new NotLoggedInException ();
2013-08-17 05:16:48 -04:00
}
2021-07-22 05:41:29 -04:00
$authorized = false ;
2023-04-24 11:13:18 -04:00
if ( $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'AuthorizedAdminSetting' , AuthorizedAdminSetting :: class )) {
2021-07-22 05:41:29 -04:00
$authorized = $this -> isAdminUser ;
2023-04-24 11:13:18 -04:00
if ( ! $authorized && $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'SubAdminRequired' , SubAdminRequired :: class )) {
2021-07-22 05:41:29 -04:00
$authorized = $this -> isSubAdmin ;
}
if ( ! $authorized ) {
2023-04-24 11:13:18 -04:00
$settingClasses = $this -> getAuthorizedAdminSettingClasses ( $reflectionMethod );
2021-07-22 05:41:29 -04:00
$authorizedClasses = $this -> groupAuthorizationMapper -> findAllClassesForUser ( $this -> userSession -> getUser ());
foreach ( $settingClasses as $settingClass ) {
$authorized = in_array ( $settingClass , $authorizedClasses , true );
2013-08-17 05:16:48 -04:00
2021-07-22 05:41:29 -04:00
if ( $authorized ) {
break ;
}
}
}
if ( ! $authorized ) {
2022-09-21 11:44:32 -04:00
throw new NotAdminException ( $this -> l10n -> t ( 'Logged in account must be an admin, a sub admin or gotten special right to access this setting' ));
2021-07-22 05:41:29 -04:00
}
}
2023-04-24 11:13:18 -04:00
if ( $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'SubAdminRequired' , SubAdminRequired :: class )
2019-05-22 04:48:51 -04:00
&& ! $this -> isSubAdmin
2021-07-22 05:41:29 -04:00
&& ! $this -> isAdminUser
&& ! $authorized ) {
2022-09-21 11:44:32 -04:00
throw new NotAdminException ( $this -> l10n -> t ( 'Logged in account must be an admin or sub admin' ));
2019-05-22 04:48:51 -04:00
}
2023-04-24 11:13:18 -04:00
if ( ! $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'SubAdminRequired' , SubAdminRequired :: class )
&& ! $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'NoAdminRequired' , NoAdminRequired :: class )
2021-07-22 05:41:29 -04:00
&& ! $this -> isAdminUser
&& ! $authorized ) {
2022-09-21 11:44:32 -04:00
throw new NotAdminException ( $this -> l10n -> t ( 'Logged in account must be an admin' ));
2013-08-17 05:16:48 -04:00
}
}
2016-07-20 11:37:30 -04:00
// Check for strict cookie requirement
2023-04-24 11:13:18 -04:00
if ( $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'StrictCookieRequired' , StrictCookiesRequired :: class ) ||
! $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'NoCSRFRequired' , NoCSRFRequired :: class )) {
2020-04-10 08:19:56 -04:00
if ( ! $this -> request -> passesStrictCookieCheck ()) {
2016-07-20 11:37:30 -04:00
throw new StrictCookieMissingException ();
}
}
2014-11-17 09:10:47 -05:00
// CSRF check - also registers the CSRF token since the session may be closed later
Util :: callRegister ();
2023-07-03 12:59:51 -04:00
if ( $this -> isInvalidCSRFRequired ( $reflectionMethod )) {
2016-07-29 07:41:30 -04:00
/*
* 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
* do want to offer CSRF checks for web requests .
2018-01-15 16:05:06 -05:00
*
* Additionally we allow Bearer authenticated requests to pass on OCS routes .
* This allows oauth apps ( e . g . moodle ) to use the OCS endpoints
2016-07-29 07:41:30 -04:00
*/
2023-07-03 12:59:51 -04:00
if ( ! $controller instanceof OCSController || ! $this -> isValidOCSRequest ()) {
2015-11-28 05:06:46 -05:00
throw new CrossSiteRequestForgeryException ();
2013-08-17 05:16:48 -04:00
}
}
2014-11-14 11:20:51 -05:00
/**
2015-11-28 05:06:46 -05:00
* Checks if app is enabled ( also includes a check whether user is allowed to access the resource )
2014-11-14 11:20:51 -05:00
* The getAppPath () check is here since components such as settings also use the AppFramework and
* therefore won ' t pass this check .
2018-02-28 14:26:03 -05:00
* If page is public , app does not need to be enabled for current user / visitor
2014-11-14 11:20:51 -05:00
*/
2018-03-08 05:05:18 -05:00
try {
$appPath = $this -> appManager -> getAppPath ( $this -> appName );
} catch ( AppPathNotFoundException $e ) {
$appPath = false ;
2014-11-14 11:20:51 -05:00
}
2017-04-12 16:14:11 -04:00
2018-03-08 05:05:18 -05:00
if ( $appPath !== false && ! $isPublicPage && ! $this -> appManager -> isEnabledForUser ( $this -> appName )) {
throw new AppNotEnabledException ();
}
2013-08-17 05:16:48 -04:00
}
2023-07-03 12:59:51 -04:00
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 ' );
}
2023-04-24 11:13:18 -04:00
/**
* @ 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 ( $this -> reflector -> hasAnnotation ( $annotationName )) {
2024-07-15 09:25:45 -04:00
$this -> logger -> debug ( $reflectionMethod -> getDeclaringClass () -> getName () . '::' . $reflectionMethod -> getName () . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead' );
2023-04-24 11:13:18 -04:00
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 ;
}
2013-08-17 05:16:48 -04:00
/**
* If an SecurityException is being caught , ajax requests return a JSON error
* response and non ajax requests redirect to the index
2021-07-22 05:41:29 -04:00
*
2013-08-17 05:16:48 -04:00
* @ param Controller $controller the controller that is being called
* @ param string $methodName the name of the method that will be called on
* the controller
* @ param \Exception $exception the thrown exception
* @ return Response a Response object or null in case that the exception could not be handled
2021-07-22 05:41:29 -04:00
* @ throws \Exception the passed in exception if it can ' t handle it
2013-08-17 05:16:48 -04:00
*/
2018-03-08 04:11:47 -05:00
public function afterException ( $controller , $methodName , \Exception $exception ) : Response {
2020-04-10 08:19:56 -04:00
if ( $exception instanceof SecurityException ) {
if ( $exception instanceof StrictCookieMissingException ) {
2020-01-13 11:53:08 -05:00
return new RedirectResponse ( \OC :: $WEBROOT . '/' );
2020-04-09 03:22:29 -04:00
}
2021-07-22 05:41:29 -04:00
if ( stripos ( $this -> request -> getHeader ( 'Accept' ), 'html' ) === false ) {
2013-08-17 05:16:48 -04:00
$response = new JSONResponse (
2018-03-08 04:11:47 -05:00
[ 'message' => $exception -> getMessage ()],
2013-08-17 05:16:48 -04:00
$exception -> getCode ()
);
} else {
2020-04-10 08:19:56 -04:00
if ( $exception instanceof NotLoggedInException ) {
2017-05-15 08:33:27 -04:00
$params = [];
if ( isset ( $this -> request -> server [ 'REQUEST_URI' ])) {
$params [ 'redirect_url' ] = $this -> request -> server [ 'REQUEST_URI' ];
}
2021-11-03 05:53:05 -04:00
$usernamePrefill = $this -> request -> getParam ( 'user' , '' );
if ( $usernamePrefill !== '' ) {
$params [ 'user' ] = $usernamePrefill ;
}
2022-01-25 11:47:58 -05:00
if ( $this -> request -> getParam ( 'direct' )) {
$params [ 'direct' ] = 1 ;
}
2017-05-15 08:33:27 -04:00
$url = $this -> urlGenerator -> linkToRoute ( 'core.login.showLoginForm' , $params );
2015-11-28 05:06:46 -05:00
$response = new RedirectResponse ( $url );
} else {
2018-08-09 08:27:20 -04:00
$response = new TemplateResponse ( 'core' , '403' , [ 'message' => $exception -> getMessage ()], 'guest' );
2015-11-28 05:06:46 -05:00
$response -> setStatus ( $exception -> getCode ());
}
2013-08-17 05:16:48 -04:00
}
2021-04-16 08:26:43 -04:00
$this -> logger -> debug ( $exception -> getMessage (), [
'exception' => $exception ,
2018-01-17 09:21:56 -05:00
]);
2013-08-17 05:16:48 -04:00
return $response ;
}
throw $exception ;
}
}