Browse Source

feat(security): Add PHP \Attribute for remaining security annotations

Signed-off-by: Joas Schilling <coding@schilljs.com>
tags/v27.0.0beta1
Joas Schilling 1 year ago
parent
commit
ecb8b55c5c
No account linked to committer's email address
24 changed files with 1278 additions and 278 deletions
  1. 8
    0
      lib/composer/composer/autoload_classmap.php
  2. 8
    0
      lib/composer/composer/autoload_static.php
  3. 45
    16
      lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
  4. 25
    1
      lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php
  5. 1
    1
      lib/private/AppFramework/Middleware/Security/ReloadExecutionMiddleware.php
  6. 61
    9
      lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
  7. 4
    0
      lib/public/AppFramework/ApiController.php
  8. 10
    1
      lib/public/AppFramework/AuthPublicShareController.php
  9. 56
    0
      lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php
  10. 37
    0
      lib/public/AppFramework/Http/Attribute/CORS.php
  11. 37
    0
      lib/public/AppFramework/Http/Attribute/NoAdminRequired.php
  12. 37
    0
      lib/public/AppFramework/Http/Attribute/NoCSRFRequired.php
  13. 37
    0
      lib/public/AppFramework/Http/Attribute/PasswordConfirmationRequired.php
  14. 37
    0
      lib/public/AppFramework/Http/Attribute/PublicPage.php
  15. 37
    0
      lib/public/AppFramework/Http/Attribute/StrictCookiesRequired.php
  16. 37
    0
      lib/public/AppFramework/Http/Attribute/SubAdminRequired.php
  17. 99
    46
      tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php
  18. 160
    0
      tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php
  19. 31
    0
      tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php
  20. 31
    0
      tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php
  21. 49
    0
      tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php
  22. 175
    0
      tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php
  23. 43
    15
      tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php
  24. 213
    189
      tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php

+ 8
- 0
lib/composer/composer/autoload_classmap.php View File

@@ -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',

+ 8
- 0
lib/composer/composer/autoload_static.php View File

@@ -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',

+ 45
- 16
lib/private/AppFramework/Middleware/Security/CORSMiddleware.php View File

@@ -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;
}

+ 25
- 1
lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php View File

@@ -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;
}
}

+ 1
- 1
lib/private/AppFramework/Middleware/Security/ReloadExecutionMiddleware.php View File

@@ -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
));
}


+ 61
- 9
lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php View File

@@ -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

+ 4
- 0
lib/public/AppFramework/ApiController.php View File

@@ -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'];

+ 10
- 1
lib/public/AppFramework/AuthPublicShareController.php View File

@@ -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()) {

+ 56
- 0
lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php View File

@@ -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;
}
}

+ 37
- 0
lib/public/AppFramework/Http/Attribute/CORS.php View File

@@ -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 {
}

+ 37
- 0
lib/public/AppFramework/Http/Attribute/NoAdminRequired.php View File

@@ -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 {
}

+ 37
- 0
lib/public/AppFramework/Http/Attribute/NoCSRFRequired.php View File

@@ -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 {
}

+ 37
- 0
lib/public/AppFramework/Http/Attribute/PasswordConfirmationRequired.php View File

@@ -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 {
}

+ 37
- 0
lib/public/AppFramework/Http/Attribute/PublicPage.php View File

@@ -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 {
}

+ 37
- 0
lib/public/AppFramework/Http/Attribute/StrictCookiesRequired.php View File

@@ -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 {
}

+ 37
- 0
lib/public/AppFramework/Http/Attribute/SubAdminRequired.php View File

@@ -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 {
}

+ 99
- 46
tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php View File

@@ -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');

+ 160
- 0
tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php View File

@@ -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() {
}
}

+ 31
- 0
tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php View File

@@ -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() {
}
}

+ 31
- 0
tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php View File

@@ -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() {
}
}

+ 49
- 0
tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php View File

@@ -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() {
}
}

+ 175
- 0
tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php View File

@@ -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() {
}
}

+ 43
- 15
tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php View File

@@ -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;
}

+ 213
- 189
tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php View File

@@ -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__);
}
}

Loading…
Cancel
Save