Signed-off-by: Joas Schilling <coding@schilljs.com>tags/v27.0.0beta1
@@ -37,7 +37,15 @@ return array( | |||
'OCP\\AppFramework\\Http' => $baseDir . '/lib/public/AppFramework/Http.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\ARateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/ARateLimit.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\AnonRateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\AuthorizedAdminSetting' => $baseDir . '/lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => $baseDir . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\CORS' => $baseDir . '/lib/public/AppFramework/Http/Attribute/CORS.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\NoAdminRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/NoAdminRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\NoCSRFRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/NoCSRFRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\PasswordConfirmationRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/PasswordConfirmationRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\PublicPage' => $baseDir . '/lib/public/AppFramework/Http/Attribute/PublicPage.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\StrictCookiesRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/StrictCookiesRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\SubAdminRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/SubAdminRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\UseSession' => $baseDir . '/lib/public/AppFramework/Http/Attribute/UseSession.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\UserRateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/UserRateLimit.php', | |||
'OCP\\AppFramework\\Http\\ContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/ContentSecurityPolicy.php', |
@@ -70,7 +70,15 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 | |||
'OCP\\AppFramework\\Http' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\ARateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/ARateLimit.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\AnonRateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\AuthorizedAdminSetting' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\CORS' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/CORS.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\NoAdminRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/NoAdminRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\NoCSRFRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/NoCSRFRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\PasswordConfirmationRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/PasswordConfirmationRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\PublicPage' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/PublicPage.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\StrictCookiesRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/StrictCookiesRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\SubAdminRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/SubAdminRequired.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\UseSession' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/UseSession.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\UserRateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/UserRateLimit.php', | |||
'OCP\\AppFramework\\Http\\ContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/ContentSecurityPolicy.php', |
@@ -33,10 +33,13 @@ use OC\Security\Bruteforce\Throttler; | |||
use OC\User\Session; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http; | |||
use OCP\AppFramework\Http\Attribute\CORS; | |||
use OCP\AppFramework\Http\Attribute\PublicPage; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\AppFramework\Http\Response; | |||
use OCP\AppFramework\Middleware; | |||
use OCP\IRequest; | |||
use ReflectionMethod; | |||
/** | |||
* This middleware sets the correct CORS headers on a response if the | |||
@@ -81,9 +84,12 @@ class CORSMiddleware extends Middleware { | |||
* @since 6.0.0 | |||
*/ | |||
public function beforeController($controller, $methodName) { | |||
$reflectionMethod = new ReflectionMethod($controller, $methodName); | |||
// ensure that @CORS annotated API routes are not used in conjunction | |||
// with session authentication since this enables CSRF attack vectors | |||
if ($this->reflector->hasAnnotation('CORS') && (!$this->reflector->hasAnnotation('PublicPage') || $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; | |||
@@ -103,7 +109,28 @@ class CORSMiddleware extends Middleware { | |||
} | |||
/** | |||
* This is being run after a successful controllermethod call and allows | |||
* @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 ($this->reflector->hasAnnotation($annotationName)) { | |||
return true; | |||
} | |||
if (!empty($reflectionMethod->getAttributes($attributeClass))) { | |||
return true; | |||
} | |||
return false; | |||
} | |||
/** | |||
* This is being run after a successful controller method call and allows | |||
* the manipulation of a Response object. The middleware is run in reverse order | |||
* | |||
* @param Controller $controller the controller that is being called | |||
@@ -114,23 +141,25 @@ class CORSMiddleware extends Middleware { | |||
* @throws SecurityException | |||
*/ | |||
public function afterController($controller, $methodName, Response $response) { | |||
// only react if its a CORS request and if the request sends origin and | |||
// only react if it's a CORS request and if the request sends origin and | |||
if (isset($this->request->server['HTTP_ORIGIN']) && | |||
$this->reflector->hasAnnotation('CORS')) { | |||
// 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'; | |||
throw new SecurityException($msg); | |||
if (isset($this->request->server['HTTP_ORIGIN'])) { | |||
$reflectionMethod = new ReflectionMethod($controller, $methodName); | |||
if ($this->hasAnnotationOrAttribute($reflectionMethod, 'CORS', CORS::class)) { | |||
// 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'; | |||
throw new SecurityException($msg); | |||
} | |||
} | |||
} | |||
$origin = $this->request->server['HTTP_ORIGIN']; | |||
$response->addHeader('Access-Control-Allow-Origin', $origin); | |||
$origin = $this->request->server['HTTP_ORIGIN']; | |||
$response->addHeader('Access-Control-Allow-Origin', $origin); | |||
} | |||
} | |||
return $response; | |||
} |
@@ -26,11 +26,13 @@ namespace OC\AppFramework\Middleware\Security; | |||
use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException; | |||
use OC\AppFramework\Utility\ControllerMethodReflector; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; | |||
use OCP\AppFramework\Middleware; | |||
use OCP\AppFramework\Utility\ITimeFactory; | |||
use OCP\ISession; | |||
use OCP\IUserSession; | |||
use OCP\User\Backend\IPasswordConfirmationBackend; | |||
use ReflectionMethod; | |||
class PasswordConfirmationMiddleware extends Middleware { | |||
/** @var ControllerMethodReflector */ | |||
@@ -68,7 +70,9 @@ class PasswordConfirmationMiddleware extends Middleware { | |||
* @throws NotConfirmedException | |||
*/ | |||
public function beforeController($controller, $methodName) { | |||
if ($this->reflector->hasAnnotation('PasswordConfirmationRequired')) { | |||
$reflectionMethod = new ReflectionMethod($controller, $methodName); | |||
if ($this->hasAnnotationOrAttribute($reflectionMethod, 'PasswordConfirmationRequired', PasswordConfirmationRequired::class)) { | |||
$user = $this->userSession->getUser(); | |||
$backendClassName = ''; | |||
if ($user !== null) { | |||
@@ -89,4 +93,24 @@ 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))) { | |||
return true; | |||
} | |||
if ($this->reflector->hasAnnotation($annotationName)) { | |||
return true; | |||
} | |||
return false; | |||
} | |||
} |
@@ -58,7 +58,7 @@ class ReloadExecutionMiddleware extends Middleware { | |||
return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute( | |||
'core.login.showLoginForm', | |||
['clear' => true] // this param the the code in login.js may be removed when the "Clear-Site-Data" is working in the browsers | |||
['clear' => true] // this param the code in login.js may be removed when the "Clear-Site-Data" is working in the browsers | |||
)); | |||
} | |||
@@ -48,6 +48,12 @@ use OC\Settings\AuthorizedGroupMapper; | |||
use OCP\App\AppPathNotFoundException; | |||
use OCP\App\IAppManager; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; | |||
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; | |||
@@ -61,6 +67,7 @@ use OCP\IURLGenerator; | |||
use OCP\IUserSession; | |||
use OCP\Util; | |||
use Psr\Log\LoggerInterface; | |||
use ReflectionMethod; | |||
/** | |||
* Used to do all the authentication and checking stuff for a controller method | |||
@@ -145,22 +152,24 @@ class SecurityMiddleware extends Middleware { | |||
$this->navigationManager->setActiveEntry('spreed'); | |||
} | |||
$reflectionMethod = new ReflectionMethod($controller, $methodName); | |||
// security checks | |||
$isPublicPage = $this->reflector->hasAnnotation('PublicPage'); | |||
$isPublicPage = $this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class); | |||
if (!$isPublicPage) { | |||
if (!$this->isLoggedIn) { | |||
throw new NotLoggedInException(); | |||
} | |||
$authorized = false; | |||
if ($this->reflector->hasAnnotation('AuthorizedAdminSetting')) { | |||
if ($this->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) { | |||
$authorized = $this->isAdminUser; | |||
if (!$authorized && $this->reflector->hasAnnotation('SubAdminRequired')) { | |||
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); | |||
@@ -174,14 +183,14 @@ class SecurityMiddleware extends Middleware { | |||
throw new NotAdminException($this->l10n->t('Logged in user must be an admin, a sub admin or gotten special right to access this setting')); | |||
} | |||
} | |||
if ($this->reflector->hasAnnotation('SubAdminRequired') | |||
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')); | |||
} | |||
if (!$this->reflector->hasAnnotation('SubAdminRequired') | |||
&& !$this->reflector->hasAnnotation('NoAdminRequired') | |||
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')); | |||
@@ -189,14 +198,15 @@ class SecurityMiddleware extends Middleware { | |||
} | |||
// 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->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) { | |||
/* | |||
* 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 | |||
@@ -232,6 +242,48 @@ class SecurityMiddleware 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))) { | |||
return true; | |||
} | |||
if ($this->reflector->hasAnnotation($annotationName)) { | |||
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 |
@@ -23,6 +23,8 @@ | |||
*/ | |||
namespace OCP\AppFramework; | |||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired; | |||
use OCP\AppFramework\Http\Attribute\PublicPage; | |||
use OCP\AppFramework\Http\Response; | |||
use OCP\IRequest; | |||
@@ -70,6 +72,8 @@ abstract class ApiController extends Controller { | |||
* @PublicPage | |||
* @since 7.0.0 | |||
*/ | |||
#[NoCSRFRequired] | |||
#[PublicPage] | |||
public function preflightedCors() { | |||
if (isset($this->request->server['HTTP_ORIGIN'])) { | |||
$origin = $this->request->server['HTTP_ORIGIN']; |
@@ -28,6 +28,10 @@ declare(strict_types=1); | |||
*/ | |||
namespace OCP\AppFramework; | |||
use OCP\AppFramework\Http\Attribute\BruteForceProtection; | |||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired; | |||
use OCP\AppFramework\Http\Attribute\PublicPage; | |||
use OCP\AppFramework\Http\Attribute\UseSession; | |||
use OCP\AppFramework\Http\RedirectResponse; | |||
use OCP\AppFramework\Http\TemplateResponse; | |||
use OCP\IRequest; | |||
@@ -70,6 +74,8 @@ abstract class AuthPublicShareController extends PublicShareController { | |||
* | |||
* @since 14.0.0 | |||
*/ | |||
#[NoCSRFRequired] | |||
#[PublicPage] | |||
public function showAuthenticate(): TemplateResponse { | |||
return new TemplateResponse('core', 'publicshareauth', [], 'guest'); | |||
} | |||
@@ -129,7 +135,7 @@ abstract class AuthPublicShareController extends PublicShareController { | |||
} | |||
/** | |||
* Function called after successfull authentication | |||
* Function called after successful authentication | |||
* | |||
* You can use this to do some logging for example | |||
* | |||
@@ -147,6 +153,9 @@ abstract class AuthPublicShareController extends PublicShareController { | |||
* | |||
* @since 14.0.0 | |||
*/ | |||
#[BruteForceProtection(action: 'publicLinkAuth')] | |||
#[PublicPage] | |||
#[UseSession] | |||
final public function authenticate(string $password = '', string $passwordRequest = 'no', string $identityToken = '') { | |||
// Already authenticated | |||
if ($this->isAuthenticated()) { |
@@ -0,0 +1,56 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\AppFramework\Http\Attribute; | |||
use Attribute; | |||
use OCP\Settings\IDelegatedSettings; | |||
/** | |||
* Attribute for controller methods that should be only accessible with | |||
* full admin or partial admin permissions. | |||
* | |||
* @since 27.0.0 | |||
*/ | |||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] | |||
class AuthorizedAdminSetting { | |||
/** | |||
* @param class-string<IDelegatedSettings> $settings A settings section the user needs to be able to access | |||
* @since 27.0.0 | |||
*/ | |||
public function __construct( | |||
protected string $settings | |||
) { | |||
} | |||
/** | |||
* | |||
* @return class-string<IDelegatedSettings> | |||
* @since 27.0.0 | |||
*/ | |||
public function getSettings(): string { | |||
return $this->settings; | |||
} | |||
} |
@@ -0,0 +1,37 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\AppFramework\Http\Attribute; | |||
use Attribute; | |||
/** | |||
* Attribute for controller methods that can also be accessed by not logged-in user | |||
* | |||
* @since 27.0.0 | |||
*/ | |||
#[Attribute] | |||
class CORS { | |||
} |
@@ -0,0 +1,37 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\AppFramework\Http\Attribute; | |||
use Attribute; | |||
/** | |||
* Attribute for controller methods that can be accessed by any logged-in user | |||
* | |||
* @since 27.0.0 | |||
*/ | |||
#[Attribute] | |||
class NoAdminRequired { | |||
} |
@@ -0,0 +1,37 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\AppFramework\Http\Attribute; | |||
use Attribute; | |||
/** | |||
* Attribute for controller methods that are not CSRF protected | |||
* | |||
* @since 27.0.0 | |||
*/ | |||
#[Attribute] | |||
class NoCSRFRequired { | |||
} |
@@ -0,0 +1,37 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\AppFramework\Http\Attribute; | |||
use Attribute; | |||
/** | |||
* Attribute for controller methods that require the password to be confirmed with in the last 30 minutes | |||
* | |||
* @since 27.0.0 | |||
*/ | |||
#[Attribute] | |||
class PasswordConfirmationRequired { | |||
} |
@@ -0,0 +1,37 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\AppFramework\Http\Attribute; | |||
use Attribute; | |||
/** | |||
* Attribute for controller methods that can also be accessed by not logged-in user | |||
* | |||
* @since 27.0.0 | |||
*/ | |||
#[Attribute] | |||
class PublicPage { | |||
} |
@@ -0,0 +1,37 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\AppFramework\Http\Attribute; | |||
use Attribute; | |||
/** | |||
* Attribute for controller methods that require strict cookies | |||
* | |||
* @since 27.0.0 | |||
*/ | |||
#[Attribute] | |||
class StrictCookiesRequired { | |||
} |
@@ -0,0 +1,37 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\AppFramework\Http\Attribute; | |||
use Attribute; | |||
/** | |||
* Attribute for controller methods that can be accessed by sub-admins | |||
* | |||
* @since 27.0.0 | |||
*/ | |||
#[Attribute] | |||
class SubAdminRequired { | |||
} |
@@ -17,11 +17,12 @@ use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; | |||
use OC\AppFramework\Utility\ControllerMethodReflector; | |||
use OC\Security\Bruteforce\Throttler; | |||
use OC\User\Session; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\AppFramework\Http\Response; | |||
use OCP\IConfig; | |||
use OCP\IRequest; | |||
use OCP\IRequestId; | |||
use Test\AppFramework\Middleware\Security\Mock\CORSMiddlewareController; | |||
class CORSMiddlewareTest extends \Test\TestCase { | |||
/** @var ControllerMethodReflector */ | |||
@@ -30,7 +31,7 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
private $session; | |||
/** @var Throttler */ | |||
private $throttler; | |||
/** @var Controller */ | |||
/** @var CORSMiddlewareController */ | |||
private $controller; | |||
protected function setUp(): void { | |||
@@ -38,13 +39,23 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
$this->reflector = new ControllerMethodReflector(); | |||
$this->session = $this->createMock(Session::class); | |||
$this->throttler = $this->createMock(Throttler::class); | |||
$this->controller = $this->createMock(Controller::class); | |||
$this->controller = new CORSMiddlewareController( | |||
'test', | |||
$this->createMock(IRequest::class) | |||
); | |||
} | |||
public function dataSetCORSAPIHeader(): array { | |||
return [ | |||
['testSetCORSAPIHeader'], | |||
['testSetCORSAPIHeaderAttribute'], | |||
]; | |||
} | |||
/** | |||
* @CORS | |||
* @dataProvider dataSetCORSAPIHeader | |||
*/ | |||
public function testSetCORSAPIHeader() { | |||
public function testSetCORSAPIHeader(string $method): void { | |||
$request = new Request( | |||
[ | |||
'server' => [ | |||
@@ -54,16 +65,15 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
$this->createMock(IRequestId::class), | |||
$this->createMock(IConfig::class) | |||
); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, $method); | |||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); | |||
$response = $middleware->afterController($this->controller, __FUNCTION__, new Response()); | |||
$response = $middleware->afterController($this->controller, $method, new Response()); | |||
$headers = $response->getHeaders(); | |||
$this->assertEquals('test', $headers['Access-Control-Allow-Origin']); | |||
} | |||
public function testNoAnnotationNoCORSHEADER() { | |||
public function testNoAnnotationNoCORSHEADER(): void { | |||
$request = new Request( | |||
[ | |||
'server' => [ | |||
@@ -80,29 +90,41 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
$this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers)); | |||
} | |||
public function dataNoOriginHeaderNoCORSHEADER(): array { | |||
return [ | |||
['testNoOriginHeaderNoCORSHEADER'], | |||
['testNoOriginHeaderNoCORSHEADERAttribute'], | |||
]; | |||
} | |||
/** | |||
* @CORS | |||
* @dataProvider dataNoOriginHeaderNoCORSHEADER | |||
*/ | |||
public function testNoOriginHeaderNoCORSHEADER() { | |||
public function testNoOriginHeaderNoCORSHEADER(string $method): void { | |||
$request = new Request( | |||
[], | |||
$this->createMock(IRequestId::class), | |||
$this->createMock(IConfig::class) | |||
); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, $method); | |||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); | |||
$response = $middleware->afterController($this->controller, __FUNCTION__, new Response()); | |||
$response = $middleware->afterController($this->controller, $method, new Response()); | |||
$headers = $response->getHeaders(); | |||
$this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers)); | |||
} | |||
public function dataCorsIgnoredIfWithCredentialsHeaderPresent(): array { | |||
return [ | |||
['testCorsIgnoredIfWithCredentialsHeaderPresent'], | |||
['testCorsAttributeIgnoredIfWithCredentialsHeaderPresent'], | |||
]; | |||
} | |||
/** | |||
* @CORS | |||
* @dataProvider dataCorsIgnoredIfWithCredentialsHeaderPresent | |||
*/ | |||
public function testCorsIgnoredIfWithCredentialsHeaderPresent() { | |||
public function testCorsIgnoredIfWithCredentialsHeaderPresent(string $method): void { | |||
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\SecurityException::class); | |||
$request = new Request( | |||
@@ -114,27 +136,33 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
$this->createMock(IRequestId::class), | |||
$this->createMock(IConfig::class) | |||
); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, $method); | |||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); | |||
$response = new Response(); | |||
$response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE'); | |||
$middleware->afterController($this->controller, __FUNCTION__, $response); | |||
$middleware->afterController($this->controller, $method, $response); | |||
} | |||
public function dataNoCORSOnAnonymousPublicPage(): array { | |||
return [ | |||
['testNoCORSOnAnonymousPublicPage'], | |||
['testNoCORSOnAnonymousPublicPageAttribute'], | |||
['testNoCORSAttributeOnAnonymousPublicPage'], | |||
['testNoCORSAttributeOnAnonymousPublicPageAttribute'], | |||
]; | |||
} | |||
/** | |||
* CORS must not be enforced for anonymous users on public pages | |||
* | |||
* @CORS | |||
* @PublicPage | |||
* @dataProvider dataNoCORSOnAnonymousPublicPage | |||
*/ | |||
public function testNoCORSOnAnonymousPublicPage() { | |||
public function testNoCORSOnAnonymousPublicPage(string $method): void { | |||
$request = new Request( | |||
[], | |||
$this->createMock(IRequestId::class), | |||
$this->createMock(IConfig::class) | |||
); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, $method); | |||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); | |||
$this->session->expects($this->once()) | |||
->method('isLoggedIn') | |||
@@ -145,25 +173,30 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
->method('logClientIn') | |||
->with($this->equalTo('user'), $this->equalTo('pass')) | |||
->willReturn(true); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, $method); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$middleware->beforeController($this->controller, $method); | |||
} | |||
public function dataCORSShouldNeverAllowCookieAuth(): array { | |||
return [ | |||
['testCORSShouldNeverAllowCookieAuth'], | |||
['testCORSShouldNeverAllowCookieAuthAttribute'], | |||
['testCORSAttributeShouldNeverAllowCookieAuth'], | |||
['testCORSAttributeShouldNeverAllowCookieAuthAttribute'], | |||
]; | |||
} | |||
/** | |||
* Even on public pages users logged in using session cookies, | |||
* that do not provide a valid CSRF token are disallowed | |||
* | |||
* @CORS | |||
* @PublicPage | |||
* @dataProvider dataCORSShouldNeverAllowCookieAuth | |||
*/ | |||
public function testCORSShouldNeverAllowCookieAuth() { | |||
public function testCORSShouldNeverAllowCookieAuth(string $method): void { | |||
$request = new Request( | |||
[], | |||
$this->createMock(IRequestId::class), | |||
$this->createMock(IConfig::class) | |||
); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, $method); | |||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); | |||
$this->session->expects($this->once()) | |||
->method('isLoggedIn') | |||
@@ -176,13 +209,20 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
->willReturn(true); | |||
$this->expectException(SecurityException::class); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$middleware->beforeController($this->controller, $method); | |||
} | |||
public function dataCORSShouldRelogin(): array { | |||
return [ | |||
['testCORSShouldRelogin'], | |||
['testCORSAttributeShouldRelogin'], | |||
]; | |||
} | |||
/** | |||
* @CORS | |||
* @dataProvider dataCORSShouldRelogin | |||
*/ | |||
public function testCORSShouldRelogin() { | |||
public function testCORSShouldRelogin(string $method): void { | |||
$request = new Request( | |||
['server' => [ | |||
'PHP_AUTH_USER' => 'user', | |||
@@ -197,16 +237,23 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
->method('logClientIn') | |||
->with($this->equalTo('user'), $this->equalTo('pass')) | |||
->willReturn(true); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, $method); | |||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$middleware->beforeController($this->controller, $method); | |||
} | |||
public function dataCORSShouldFailIfPasswordLoginIsForbidden(): array { | |||
return [ | |||
['testCORSShouldFailIfPasswordLoginIsForbidden'], | |||
['testCORSAttributeShouldFailIfPasswordLoginIsForbidden'], | |||
]; | |||
} | |||
/** | |||
* @CORS | |||
* @dataProvider dataCORSShouldFailIfPasswordLoginIsForbidden | |||
*/ | |||
public function testCORSShouldFailIfPasswordLoginIsForbidden() { | |||
public function testCORSShouldFailIfPasswordLoginIsForbidden(string $method): void { | |||
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\SecurityException::class); | |||
$request = new Request( | |||
@@ -223,16 +270,23 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
->method('logClientIn') | |||
->with($this->equalTo('user'), $this->equalTo('pass')) | |||
->will($this->throwException(new \OC\Authentication\Exceptions\PasswordLoginForbiddenException)); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, $method); | |||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$middleware->beforeController($this->controller, $method); | |||
} | |||
public function dataCORSShouldNotAllowCookieAuth(): array { | |||
return [ | |||
['testCORSShouldNotAllowCookieAuth'], | |||
['testCORSAttributeShouldNotAllowCookieAuth'], | |||
]; | |||
} | |||
/** | |||
* @CORS | |||
* @dataProvider dataCORSShouldNotAllowCookieAuth | |||
*/ | |||
public function testCORSShouldNotAllowCookieAuth() { | |||
public function testCORSShouldNotAllowCookieAuth(string $method): void { | |||
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\SecurityException::class); | |||
$request = new Request( | |||
@@ -249,10 +303,10 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
->method('logClientIn') | |||
->with($this->equalTo('user'), $this->equalTo('pass')) | |||
->willReturn(false); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, $method); | |||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$middleware->beforeController($this->controller, $method); | |||
} | |||
public function testAfterExceptionWithSecurityExceptionNoStatus() { | |||
@@ -287,7 +341,6 @@ class CORSMiddlewareTest extends \Test\TestCase { | |||
$this->assertEquals($expected, $response); | |||
} | |||
public function testAfterExceptionWithRegularException() { | |||
$this->expectException(\Exception::class); | |||
$this->expectExceptionMessage('A regular exception'); |
@@ -0,0 +1,160 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace Test\AppFramework\Middleware\Security\Mock; | |||
use OCP\AppFramework\Http\Attribute\CORS; | |||
use OCP\AppFramework\Http\Attribute\PublicPage; | |||
class CORSMiddlewareController extends \OCP\AppFramework\Controller { | |||
/** | |||
* @CORS | |||
*/ | |||
public function testSetCORSAPIHeader() { | |||
} | |||
#[CORS] | |||
public function testSetCORSAPIHeaderAttribute() { | |||
} | |||
public function testNoAnnotationNoCORSHEADER() { | |||
} | |||
/** | |||
* @CORS | |||
*/ | |||
public function testNoOriginHeaderNoCORSHEADER() { | |||
} | |||
#[CORS] | |||
public function testNoOriginHeaderNoCORSHEADERAttribute() { | |||
} | |||
/** | |||
* @CORS | |||
*/ | |||
public function testCorsIgnoredIfWithCredentialsHeaderPresent() { | |||
} | |||
#[CORS] | |||
public function testCorsAttributeIgnoredIfWithCredentialsHeaderPresent() { | |||
} | |||
/** | |||
* CORS must not be enforced for anonymous users on public pages | |||
* | |||
* @CORS | |||
* @PublicPage | |||
*/ | |||
public function testNoCORSOnAnonymousPublicPage() { | |||
} | |||
/** | |||
* CORS must not be enforced for anonymous users on public pages | |||
* | |||
* @CORS | |||
*/ | |||
#[PublicPage] | |||
public function testNoCORSOnAnonymousPublicPageAttribute() { | |||
} | |||
/** | |||
* @PublicPage | |||
*/ | |||
#[CORS] | |||
public function testNoCORSAttributeOnAnonymousPublicPage() { | |||
} | |||
#[CORS] | |||
#[PublicPage] | |||
public function testNoCORSAttributeOnAnonymousPublicPageAttribute() { | |||
} | |||
/** | |||
* @CORS | |||
* @PublicPage | |||
*/ | |||
public function testCORSShouldNeverAllowCookieAuth() { | |||
} | |||
/** | |||
* @CORS | |||
*/ | |||
#[PublicPage] | |||
public function testCORSShouldNeverAllowCookieAuthAttribute() { | |||
} | |||
/** | |||
* @PublicPage | |||
*/ | |||
#[CORS] | |||
public function testCORSAttributeShouldNeverAllowCookieAuth() { | |||
} | |||
#[CORS] | |||
#[PublicPage] | |||
public function testCORSAttributeShouldNeverAllowCookieAuthAttribute() { | |||
} | |||
/** | |||
* @CORS | |||
*/ | |||
public function testCORSShouldRelogin() { | |||
} | |||
#[CORS] | |||
public function testCORSAttributeShouldRelogin() { | |||
} | |||
/** | |||
* @CORS | |||
*/ | |||
public function testCORSShouldFailIfPasswordLoginIsForbidden() { | |||
} | |||
#[CORS] | |||
public function testCORSAttributeShouldFailIfPasswordLoginIsForbidden() { | |||
} | |||
/** | |||
* @CORS | |||
*/ | |||
public function testCORSShouldNotAllowCookieAuth() { | |||
} | |||
#[CORS] | |||
public function testCORSAttributeShouldNotAllowCookieAuth() { | |||
} | |||
public function testAfterExceptionWithSecurityExceptionNoStatus() { | |||
} | |||
public function testAfterExceptionWithSecurityExceptionWithStatus() { | |||
} | |||
public function testAfterExceptionWithRegularException() { | |||
} | |||
} |
@@ -0,0 +1,31 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace Test\AppFramework\Middleware\Security\Mock; | |||
class NormalController extends \OCP\AppFramework\Controller { | |||
public function foo() { | |||
} | |||
} |
@@ -0,0 +1,31 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace Test\AppFramework\Middleware\Security\Mock; | |||
class OCSController extends \OCP\AppFramework\OCSController { | |||
public function foo() { | |||
} | |||
} |
@@ -0,0 +1,49 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace Test\AppFramework\Middleware\Security\Mock; | |||
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; | |||
class PasswordConfirmationMiddlewareController extends \OCP\AppFramework\Controller { | |||
public function testNoAnnotationNorAttribute() { | |||
} | |||
/** | |||
* @TestAnnotation | |||
*/ | |||
public function testDifferentAnnotation() { | |||
} | |||
/** | |||
* @PasswordConfirmationRequired | |||
*/ | |||
public function testAnnotation() { | |||
} | |||
#[PasswordConfirmationRequired] | |||
public function testAttribute() { | |||
} | |||
} |
@@ -0,0 +1,175 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* 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 | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace Test\AppFramework\Middleware\Security\Mock; | |||
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; | |||
class SecurityMiddlewareController extends \OCP\AppFramework\Controller { | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
*/ | |||
public function testAnnotationNoCSRFRequiredPublicPage() { | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
*/ | |||
#[PublicPage] | |||
public function testAnnotationNoCSRFRequiredAttributePublicPage() { | |||
} | |||
/** | |||
* @PublicPage | |||
*/ | |||
#[NoCSRFRequired] | |||
public function testAnnotationPublicPageAttributeNoCSRFRequired() { | |||
} | |||
#[NoCSRFRequired] | |||
#[PublicPage] | |||
public function testAttributeNoCSRFRequiredPublicPage() { | |||
} | |||
public function testNoAnnotationNorAttribute() { | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
*/ | |||
public function testAnnotationNoCSRFRequired() { | |||
} | |||
#[NoCSRFRequired] | |||
public function testAttributeNoCSRFRequired() { | |||
} | |||
/** | |||
* @PublicPage | |||
*/ | |||
public function testAnnotationPublicPage() { | |||
} | |||
#[PublicPage] | |||
public function testAttributePublicPage() { | |||
} | |||
/** | |||
* @PublicPage | |||
* @StrictCookieRequired | |||
*/ | |||
public function testAnnotationPublicPageStrictCookieRequired() { | |||
} | |||
/** | |||
* @StrictCookieRequired | |||
*/ | |||
#[PublicPage] | |||
public function testAnnotationStrictCookieRequiredAttributePublicPage() { | |||
} | |||
/** | |||
* @PublicPage | |||
*/ | |||
#[StrictCookiesRequired] | |||
public function testAnnotationPublicPageAttributeStrictCookiesRequired() { | |||
} | |||
#[PublicPage] | |||
#[StrictCookiesRequired] | |||
public function testAttributePublicPageStrictCookiesRequired() { | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* @StrictCookieRequired | |||
*/ | |||
public function testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired() { | |||
} | |||
#[NoCSRFRequired] | |||
#[PublicPage] | |||
#[StrictCookiesRequired] | |||
public function testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired() { | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @NoAdminRequired | |||
*/ | |||
public function testAnnotationNoAdminRequiredNoCSRFRequired() { | |||
} | |||
#[NoAdminRequired] | |||
#[NoCSRFRequired] | |||
public function testAttributeNoAdminRequiredNoCSRFRequired() { | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @SubAdminRequired | |||
*/ | |||
public function testAnnotationNoCSRFRequiredSubAdminRequired() { | |||
} | |||
/** | |||
* @SubAdminRequired | |||
*/ | |||
#[NoCSRFRequired] | |||
public function testAnnotationNoCSRFRequiredAttributeSubAdminRequired() { | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
*/ | |||
#[SubAdminRequired] | |||
public function testAnnotationSubAdminRequiredAttributeNoCSRFRequired() { | |||
} | |||
#[NoCSRFRequired] | |||
#[SubAdminRequired] | |||
public function testAttributeNoCSRFRequiredSubAdminRequired() { | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoAdminRequired | |||
* @NoCSRFRequired | |||
*/ | |||
public function testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage() { | |||
} | |||
#[NoAdminRequired] | |||
#[NoCSRFRequired] | |||
#[PublicPage] | |||
public function testAttributeNoAdminRequiredNoCSRFRequiredPublicPage() { | |||
} | |||
} |
@@ -26,11 +26,12 @@ namespace Test\AppFramework\Middleware\Security; | |||
use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException; | |||
use OC\AppFramework\Middleware\Security\PasswordConfirmationMiddleware; | |||
use OC\AppFramework\Utility\ControllerMethodReflector; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Utility\ITimeFactory; | |||
use OCP\IRequest; | |||
use OCP\ISession; | |||
use OCP\IUser; | |||
use OCP\IUserSession; | |||
use Test\AppFramework\Middleware\Security\Mock\PasswordConfirmationMiddlewareController; | |||
use Test\TestCase; | |||
class PasswordConfirmationMiddlewareTest extends TestCase { | |||
@@ -44,8 +45,8 @@ class PasswordConfirmationMiddlewareTest extends TestCase { | |||
private $user; | |||
/** @var PasswordConfirmationMiddleware */ | |||
private $middleware; | |||
/** @var Controller */ | |||
private $contoller; | |||
/** @var PasswordConfirmationMiddlewareController */ | |||
private $controller; | |||
/** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ | |||
private $timeFactory; | |||
@@ -54,8 +55,11 @@ class PasswordConfirmationMiddlewareTest extends TestCase { | |||
$this->session = $this->createMock(ISession::class); | |||
$this->userSession = $this->createMock(IUserSession::class); | |||
$this->user = $this->createMock(IUser::class); | |||
$this->contoller = $this->createMock(Controller::class); | |||
$this->timeFactory = $this->createMock(ITimeFactory::class); | |||
$this->controller = new PasswordConfirmationMiddlewareController( | |||
'test', | |||
$this->createMock(IRequest::class) | |||
); | |||
$this->middleware = new PasswordConfirmationMiddleware( | |||
$this->reflector, | |||
@@ -65,35 +69,59 @@ class PasswordConfirmationMiddlewareTest extends TestCase { | |||
); | |||
} | |||
public function testNoAnnotation() { | |||
$this->reflector->reflect(__CLASS__, __FUNCTION__); | |||
public function testNoAnnotationNorAttribute() { | |||
$this->reflector->reflect($this->controller, __FUNCTION__); | |||
$this->session->expects($this->never()) | |||
->method($this->anything()); | |||
$this->userSession->expects($this->never()) | |||
->method($this->anything()); | |||
$this->middleware->beforeController($this->contoller, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
} | |||
/** | |||
* @TestAnnotation | |||
*/ | |||
public function testDifferentAnnotation() { | |||
$this->reflector->reflect(__CLASS__, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, __FUNCTION__); | |||
$this->session->expects($this->never()) | |||
->method($this->anything()); | |||
$this->userSession->expects($this->never()) | |||
->method($this->anything()); | |||
$this->middleware->beforeController($this->contoller, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
} | |||
/** | |||
* @PasswordConfirmationRequired | |||
* @dataProvider dataProvider | |||
*/ | |||
public function testAnnotation($backend, $lastConfirm, $currentTime, $exception) { | |||
$this->reflector->reflect(__CLASS__, __FUNCTION__); | |||
$this->reflector->reflect($this->controller, __FUNCTION__); | |||
$this->user->method('getBackendClassName') | |||
->willReturn($backend); | |||
$this->userSession->method('getUser') | |||
->willReturn($this->user); | |||
$this->session->method('get') | |||
->with('last-password-confirm') | |||
->willReturn($lastConfirm); | |||
$this->timeFactory->method('getTime') | |||
->willReturn($currentTime); | |||
$thrown = false; | |||
try { | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
} catch (NotConfirmedException $e) { | |||
$thrown = true; | |||
} | |||
$this->assertSame($exception, $thrown); | |||
} | |||
/** | |||
* @dataProvider dataProvider | |||
*/ | |||
public function testAttribute($backend, $lastConfirm, $currentTime, $exception) { | |||
$this->reflector->reflect($this->controller, __FUNCTION__); | |||
$this->user->method('getBackendClassName') | |||
->willReturn($backend); | |||
@@ -109,7 +137,7 @@ class PasswordConfirmationMiddlewareTest extends TestCase { | |||
$thrown = false; | |||
try { | |||
$this->middleware->beforeController($this->contoller, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
} catch (NotConfirmedException $e) { | |||
$thrown = true; | |||
} |
@@ -34,7 +34,6 @@ use OC\AppFramework\Middleware\Security\SecurityMiddleware; | |||
use OC\AppFramework\Utility\ControllerMethodReflector; | |||
use OC\Settings\AuthorizedGroupMapper; | |||
use OCP\App\IAppManager; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\AppFramework\Http\RedirectResponse; | |||
use OCP\AppFramework\Http\TemplateResponse; | |||
@@ -46,11 +45,14 @@ use OCP\IRequestId; | |||
use OCP\IURLGenerator; | |||
use OCP\IUserSession; | |||
use Psr\Log\LoggerInterface; | |||
use Test\AppFramework\Middleware\Security\Mock\NormalController; | |||
use Test\AppFramework\Middleware\Security\Mock\OCSController; | |||
use Test\AppFramework\Middleware\Security\Mock\SecurityMiddlewareController; | |||
class SecurityMiddlewareTest extends \Test\TestCase { | |||
/** @var SecurityMiddleware|\PHPUnit\Framework\MockObject\MockObject */ | |||
private $middleware; | |||
/** @var Controller|\PHPUnit\Framework\MockObject\MockObject */ | |||
/** @var SecurityMiddlewareController */ | |||
private $controller; | |||
/** @var SecurityException */ | |||
private $secException; | |||
@@ -80,12 +82,15 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
$this->authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::class); | |||
$this->userSession = $this->createMock(IUserSession::class); | |||
$this->controller = $this->createMock(Controller::class); | |||
$this->request = $this->createMock(IRequest::class); | |||
$this->controller = new SecurityMiddlewareController( | |||
'test', | |||
$this->request | |||
); | |||
$this->reader = new ControllerMethodReflector(); | |||
$this->logger = $this->createMock(LoggerInterface::class); | |||
$this->navigationManager = $this->createMock(INavigationManager::class); | |||
$this->urlGenerator = $this->createMock(IURLGenerator::class); | |||
$this->request = $this->createMock(IRequest::class); | |||
$this->l10n = $this->createMock(IL10N::class); | |||
$this->middleware = $this->getMiddleware(true, true, false); | |||
$this->secException = new SecurityException('hey', false); | |||
@@ -115,18 +120,78 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
); | |||
} | |||
public function dataNoCSRFRequiredPublicPage(): array { | |||
return [ | |||
['testAnnotationNoCSRFRequiredPublicPage'], | |||
['testAnnotationNoCSRFRequiredAttributePublicPage'], | |||
['testAnnotationPublicPageAttributeNoCSRFRequired'], | |||
['testAttributeNoCSRFRequiredPublicPage'], | |||
]; | |||
} | |||
public function dataPublicPage(): array { | |||
return [ | |||
['testAnnotationPublicPage'], | |||
['testAttributePublicPage'], | |||
]; | |||
} | |||
public function dataNoCSRFRequired(): array { | |||
return [ | |||
['testAnnotationNoCSRFRequired'], | |||
['testAttributeNoCSRFRequired'], | |||
]; | |||
} | |||
public function dataPublicPageStrictCookieRequired(): array { | |||
return [ | |||
['testAnnotationPublicPageStrictCookieRequired'], | |||
['testAnnotationStrictCookieRequiredAttributePublicPage'], | |||
['testAnnotationPublicPageAttributeStrictCookiesRequired'], | |||
['testAttributePublicPageStrictCookiesRequired'], | |||
]; | |||
} | |||
public function dataNoCSRFRequiredPublicPageStrictCookieRequired(): array { | |||
return [ | |||
['testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired'], | |||
['testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired'], | |||
]; | |||
} | |||
public function dataNoAdminRequiredNoCSRFRequired(): array { | |||
return [ | |||
['testAnnotationNoAdminRequiredNoCSRFRequired'], | |||
['testAttributeNoAdminRequiredNoCSRFRequired'], | |||
]; | |||
} | |||
public function dataNoAdminRequiredNoCSRFRequiredPublicPage(): array { | |||
return [ | |||
['testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage'], | |||
['testAttributeNoAdminRequiredNoCSRFRequiredPublicPage'], | |||
]; | |||
} | |||
public function dataNoCSRFRequiredSubAdminRequired(): array { | |||
return [ | |||
['testAnnotationNoCSRFRequiredSubAdminRequired'], | |||
['testAnnotationNoCSRFRequiredAttributeSubAdminRequired'], | |||
['testAnnotationSubAdminRequiredAttributeNoCSRFRequired'], | |||
['testAttributeNoCSRFRequiredSubAdminRequired'], | |||
]; | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* @dataProvider dataNoCSRFRequiredPublicPage | |||
*/ | |||
public function testSetNavigationEntry() { | |||
public function testSetNavigationEntry(string $method): void { | |||
$this->navigationManager->expects($this->once()) | |||
->method('setActiveEntry') | |||
->with($this->equalTo('files')); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->middleware->beforeController($this->controller, $method); | |||
} | |||
@@ -146,7 +211,7 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
$sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false); | |||
try { | |||
$this->reader->reflect(__CLASS__, $method); | |||
$this->reader->reflect($this->controller, $method); | |||
$sec->beforeController($this->controller, $method); | |||
} catch (SecurityException $ex) { | |||
$this->assertEquals($status, $ex->getCode()); | |||
@@ -159,75 +224,71 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
} | |||
} | |||
public function testAjaxStatusLoggedInCheck() { | |||
public function testAjaxStatusLoggedInCheck(): void { | |||
$this->ajaxExceptionStatus( | |||
__FUNCTION__, | |||
'testNoAnnotationNorAttribute', | |||
'isLoggedIn', | |||
Http::STATUS_UNAUTHORIZED | |||
); | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @dataProvider dataNoCSRFRequired | |||
*/ | |||
public function testAjaxNotAdminCheck() { | |||
public function testAjaxNotAdminCheck(string $method): void { | |||
$this->ajaxExceptionStatus( | |||
__FUNCTION__, | |||
$method, | |||
'isAdminUser', | |||
Http::STATUS_FORBIDDEN | |||
); | |||
} | |||
/** | |||
* @PublicPage | |||
* @dataProvider dataPublicPage | |||
*/ | |||
public function testAjaxStatusCSRFCheck() { | |||
public function testAjaxStatusCSRFCheck(string $method): void { | |||
$this->ajaxExceptionStatus( | |||
__FUNCTION__, | |||
$method, | |||
'passesCSRFCheck', | |||
Http::STATUS_PRECONDITION_FAILED | |||
); | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* @dataProvider dataNoCSRFRequiredPublicPage | |||
*/ | |||
public function testAjaxStatusAllGood() { | |||
public function testAjaxStatusAllGood(string $method): void { | |||
$this->ajaxExceptionStatus( | |||
__FUNCTION__, | |||
$method, | |||
'isLoggedIn', | |||
0 | |||
); | |||
$this->ajaxExceptionStatus( | |||
__FUNCTION__, | |||
$method, | |||
'isAdminUser', | |||
0 | |||
); | |||
$this->ajaxExceptionStatus( | |||
__FUNCTION__, | |||
$method, | |||
'passesCSRFCheck', | |||
0 | |||
); | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* @dataProvider dataNoCSRFRequiredPublicPage | |||
*/ | |||
public function testNoChecks() { | |||
public function testNoChecks(string $method): void { | |||
$this->request->expects($this->never()) | |||
->method('passesCSRFCheck') | |||
->willReturn(false); | |||
$sec = $this->getMiddleware(false, false, false); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$sec->beforeController($this->controller, __FUNCTION__); | |||
$this->reader->reflect($this->controller, $method); | |||
$sec->beforeController($this->controller, $method); | |||
} | |||
/** | |||
* @param string $method | |||
* @param string $expects | |||
@@ -250,15 +311,15 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
$this->addToAssertionCount(1); | |||
} | |||
$this->reader->reflect(__CLASS__, $method); | |||
$this->reader->reflect($this->controller, $method); | |||
$sec->beforeController($this->controller, $method); | |||
} | |||
/** | |||
* @PublicPage | |||
* @dataProvider dataPublicPage | |||
*/ | |||
public function testCsrfCheck() { | |||
public function testCsrfCheck(string $method): void { | |||
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException::class); | |||
$this->request->expects($this->once()) | |||
@@ -267,28 +328,26 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
$this->request->expects($this->once()) | |||
->method('passesStrictCookieCheck') | |||
->willReturn(true); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->middleware->beforeController($this->controller, $method); | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* @dataProvider dataNoCSRFRequiredPublicPage | |||
*/ | |||
public function testNoCsrfCheck() { | |||
public function testNoCsrfCheck(string $method) { | |||
$this->request->expects($this->never()) | |||
->method('passesCSRFCheck') | |||
->willReturn(false); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->middleware->beforeController($this->controller, $method); | |||
} | |||
/** | |||
* @PublicPage | |||
* @dataProvider dataPublicPage | |||
*/ | |||
public function testPassesCsrfCheck() { | |||
public function testPassesCsrfCheck(string $method): void { | |||
$this->request->expects($this->once()) | |||
->method('passesCSRFCheck') | |||
->willReturn(true); | |||
@@ -296,14 +355,14 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
->method('passesStrictCookieCheck') | |||
->willReturn(true); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->middleware->beforeController($this->controller, $method); | |||
} | |||
/** | |||
* @PublicPage | |||
* @dataProvider dataPublicPage | |||
*/ | |||
public function testFailCsrfCheck() { | |||
public function testFailCsrfCheck(string $method): void { | |||
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException::class); | |||
$this->request->expects($this->once()) | |||
@@ -313,16 +372,15 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
->method('passesStrictCookieCheck') | |||
->willReturn(true); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->middleware->beforeController($this->controller, $method); | |||
} | |||
/** | |||
* @PublicPage | |||
* @StrictCookieRequired | |||
* @dataProvider dataPublicPageStrictCookieRequired | |||
*/ | |||
public function testStrictCookieRequiredCheck() { | |||
$this->expectException(\OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException::class); | |||
public function testStrictCookieRequiredCheck(string $method): void { | |||
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException::class); | |||
$this->request->expects($this->never()) | |||
->method('passesCSRFCheck'); | |||
@@ -330,68 +388,57 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
->method('passesStrictCookieCheck') | |||
->willReturn(false); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->middleware->beforeController($this->controller, $method); | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* @dataProvider dataNoCSRFRequiredPublicPage | |||
*/ | |||
public function testNoStrictCookieRequiredCheck() { | |||
public function testNoStrictCookieRequiredCheck(string $method): void { | |||
$this->request->expects($this->never()) | |||
->method('passesStrictCookieCheck') | |||
->willReturn(false); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->middleware->beforeController($this->controller, $method); | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* @StrictCookieRequired | |||
* @dataProvider dataNoCSRFRequiredPublicPageStrictCookieRequired | |||
*/ | |||
public function testPassesStrictCookieRequiredCheck() { | |||
public function testPassesStrictCookieRequiredCheck(string $method): void { | |||
$this->request | |||
->expects($this->once()) | |||
->method('passesStrictCookieCheck') | |||
->willReturn(true); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->middleware->beforeController($this->controller, $method); | |||
} | |||
public function dataCsrfOcsController() { | |||
$controller = $this->getMockBuilder('OCP\AppFramework\Controller') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
$ocsController = $this->getMockBuilder('OCP\AppFramework\OCSController') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
public function dataCsrfOcsController(): array { | |||
return [ | |||
[$controller, false, false, true], | |||
[$controller, false, true, true], | |||
[$controller, true, false, true], | |||
[$controller, true, true, true], | |||
[$ocsController, false, false, true], | |||
[$ocsController, false, true, false], | |||
[$ocsController, true, false, false], | |||
[$ocsController, true, true, false], | |||
[NormalController::class, false, false, true], | |||
[NormalController::class, false, true, true], | |||
[NormalController::class, true, false, true], | |||
[NormalController::class, true, true, true], | |||
[OCSController::class, false, false, true], | |||
[OCSController::class, false, true, false], | |||
[OCSController::class, true, false, false], | |||
[OCSController::class, true, true, false], | |||
]; | |||
} | |||
/** | |||
* @dataProvider dataCsrfOcsController | |||
* @param Controller $controller | |||
* @param string $controllerClass | |||
* @param bool $hasOcsApiHeader | |||
* @param bool $hasBearerAuth | |||
* @param bool $exception | |||
*/ | |||
public function testCsrfOcsController(Controller $controller, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception) { | |||
public function testCsrfOcsController(string $controllerClass, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception): void { | |||
$this->request | |||
->method('getHeader') | |||
->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) { | |||
@@ -407,6 +454,8 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
->method('passesStrictCookieCheck') | |||
->willReturn(true); | |||
$controller = new $controllerClass('test', $this->request); | |||
try { | |||
$this->middleware->beforeController($controller, 'foo'); | |||
$this->assertFalse($exception); | |||
@@ -416,71 +465,117 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @NoAdminRequired | |||
* @dataProvider dataNoAdminRequiredNoCSRFRequired | |||
*/ | |||
public function testLoggedInCheck() { | |||
$this->securityCheck(__FUNCTION__, 'isLoggedIn'); | |||
public function testLoggedInCheck(string $method): void { | |||
$this->securityCheck($method, 'isLoggedIn'); | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @NoAdminRequired | |||
* @dataProvider dataNoAdminRequiredNoCSRFRequired | |||
*/ | |||
public function testFailLoggedInCheck() { | |||
$this->securityCheck(__FUNCTION__, 'isLoggedIn', true); | |||
public function testFailLoggedInCheck(string $method): void { | |||
$this->securityCheck($method, 'isLoggedIn', true); | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @dataProvider dataNoCSRFRequired | |||
*/ | |||
public function testIsAdminCheck() { | |||
$this->securityCheck(__FUNCTION__, 'isAdminUser'); | |||
public function testIsAdminCheck(string $method): void { | |||
$this->securityCheck($method, 'isAdminUser'); | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @SubAdminRequired | |||
* @dataProvider dataNoCSRFRequiredSubAdminRequired | |||
*/ | |||
public function testIsNotSubAdminCheck() { | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
public function testIsNotSubAdminCheck(string $method): void { | |||
$this->reader->reflect($this->controller, $method); | |||
$sec = $this->getMiddleware(true, false, false); | |||
$this->expectException(SecurityException::class); | |||
$sec->beforeController($this, __METHOD__); | |||
$sec->beforeController($this->controller, $method); | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @SubAdminRequired | |||
* @dataProvider dataNoCSRFRequiredSubAdminRequired | |||
*/ | |||
public function testIsSubAdminCheck() { | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
public function testIsSubAdminCheck(string $method): void { | |||
$this->reader->reflect($this->controller, $method); | |||
$sec = $this->getMiddleware(true, false, true); | |||
$sec->beforeController($this, __METHOD__); | |||
$sec->beforeController($this->controller, $method); | |||
$this->addToAssertionCount(1); | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @SubAdminRequired | |||
* @dataProvider dataNoCSRFRequiredSubAdminRequired | |||
*/ | |||
public function testIsSubAdminAndAdminCheck() { | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
public function testIsSubAdminAndAdminCheck(string $method): void { | |||
$this->reader->reflect($this->controller, $method); | |||
$sec = $this->getMiddleware(true, true, true); | |||
$sec->beforeController($this, __METHOD__); | |||
$sec->beforeController($this->controller, $method); | |||
$this->addToAssertionCount(1); | |||
} | |||
/** | |||
* @NoCSRFRequired | |||
* @dataProvider dataNoCSRFRequired | |||
*/ | |||
public function testFailIsAdminCheck() { | |||
$this->securityCheck(__FUNCTION__, 'isAdminUser', true); | |||
public function testFailIsAdminCheck(string $method): void { | |||
$this->securityCheck($method, 'isAdminUser', true); | |||
} | |||
/** | |||
* @dataProvider dataNoAdminRequiredNoCSRFRequiredPublicPage | |||
*/ | |||
public function testRestrictedAppLoggedInPublicPage(string $method): void { | |||
$middleware = $this->getMiddleware(true, false, false); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->appManager->method('getAppPath') | |||
->with('files') | |||
->willReturn('foo'); | |||
$this->appManager->method('isEnabledForUser') | |||
->with('files') | |||
->willReturn(false); | |||
$middleware->beforeController($this->controller, $method); | |||
$this->addToAssertionCount(1); | |||
} | |||
/** | |||
* @dataProvider dataNoAdminRequiredNoCSRFRequiredPublicPage | |||
*/ | |||
public function testRestrictedAppNotLoggedInPublicPage(string $method): void { | |||
$middleware = $this->getMiddleware(false, false, false); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->appManager->method('getAppPath') | |||
->with('files') | |||
->willReturn('foo'); | |||
$this->appManager->method('isEnabledForUser') | |||
->with('files') | |||
->willReturn(false); | |||
$middleware->beforeController($this->controller, $method); | |||
$this->addToAssertionCount(1); | |||
} | |||
/** | |||
* @dataProvider dataNoAdminRequiredNoCSRFRequired | |||
*/ | |||
public function testRestrictedAppLoggedIn(string $method): void { | |||
$middleware = $this->getMiddleware(true, false, false, false); | |||
$this->reader->reflect($this->controller, $method); | |||
$this->appManager->method('getAppPath') | |||
->with('files') | |||
->willReturn('foo'); | |||
$this->expectException(AppNotEnabledException::class); | |||
$middleware->beforeController($this->controller, $method); | |||
} | |||
@@ -602,75 +697,4 @@ class SecurityMiddlewareTest extends \Test\TestCase { | |||
$this->assertTrue($response instanceof JSONResponse); | |||
} | |||
public function dataRestrictedApp() { | |||
return [ | |||
[false, false, false,], | |||
[false, false, true,], | |||
[false, true, false,], | |||
[false, true, true,], | |||
[ true, false, false,], | |||
[ true, false, true,], | |||
[ true, true, false,], | |||
[ true, true, true,], | |||
]; | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoAdminRequired | |||
* @NoCSRFRequired | |||
*/ | |||
public function testRestrictedAppLoggedInPublicPage() { | |||
$middleware = $this->getMiddleware(true, false, false); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->appManager->method('getAppPath') | |||
->with('files') | |||
->willReturn('foo'); | |||
$this->appManager->method('isEnabledForUser') | |||
->with('files') | |||
->willReturn(false); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->addToAssertionCount(1); | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoAdminRequired | |||
* @NoCSRFRequired | |||
*/ | |||
public function testRestrictedAppNotLoggedInPublicPage() { | |||
$middleware = $this->getMiddleware(false, false, false); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->appManager->method('getAppPath') | |||
->with('files') | |||
->willReturn('foo'); | |||
$this->appManager->method('isEnabledForUser') | |||
->with('files') | |||
->willReturn(false); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->addToAssertionCount(1); | |||
} | |||
/** | |||
* @NoAdminRequired | |||
* @NoCSRFRequired | |||
*/ | |||
public function testRestrictedAppLoggedIn() { | |||
$middleware = $this->getMiddleware(true, false, false, false); | |||
$this->reader->reflect(__CLASS__, __FUNCTION__); | |||
$this->appManager->method('getAppPath') | |||
->with('files') | |||
->willReturn('foo'); | |||
$this->expectException(AppNotEnabledException::class); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
} | |||
} |