diff options
Diffstat (limited to 'tests/lib/AppFramework/Middleware/Security')
13 files changed, 1371 insertions, 698 deletions
diff --git a/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php index cc04992ae18..3fd2cb38a33 100644 --- a/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php @@ -1,69 +1,70 @@ <?php + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\AppFramework\Middleware\Security; use OC\AppFramework\Middleware\Security\BruteForceMiddleware; use OC\AppFramework\Utility\ControllerMethodReflector; -use OC\Security\Bruteforce\Throttler; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Response; use OCP\IRequest; +use OCP\Security\Bruteforce\IThrottler; +use Psr\Log\LoggerInterface; use Test\TestCase; +class TestController extends Controller { + /** + * @BruteForceProtection(action=login) + */ + public function testMethodWithAnnotation() { + } + + public function testMethodWithoutAnnotation() { + } + + #[BruteForceProtection(action: 'single')] + public function singleAttribute(): void { + } + + #[BruteForceProtection(action: 'first')] + #[BruteForceProtection(action: 'second')] + public function multipleAttributes(): void { + } +} + class BruteForceMiddlewareTest extends TestCase { - /** @var ControllerMethodReflector|\PHPUnit\Framework\MockObject\MockObject */ + /** @var ControllerMethodReflector */ private $reflector; - /** @var Throttler|\PHPUnit\Framework\MockObject\MockObject */ + /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */ private $throttler; /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ private $request; - /** @var BruteForceMiddleware */ - private $bruteForceMiddleware; + /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + private BruteForceMiddleware $bruteForceMiddleware; protected function setUp(): void { parent::setUp(); - $this->reflector = $this->createMock(ControllerMethodReflector::class); - $this->throttler = $this->createMock(Throttler::class); + $this->reflector = new ControllerMethodReflector(); + $this->throttler = $this->createMock(IThrottler::class); $this->request = $this->createMock(IRequest::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->bruteForceMiddleware = new BruteForceMiddleware( $this->reflector, $this->throttler, - $this->request + $this->request, + $this->logger, ); } - public function testBeforeControllerWithAnnotation() { - $this->reflector - ->expects($this->once()) - ->method('hasAnnotation') - ->with('BruteForceProtection') - ->willReturn(true); - $this->reflector - ->expects($this->once()) - ->method('getAnnotationParameter') - ->with('BruteForceProtection', 'action') - ->willReturn('login'); + public function testBeforeControllerWithAnnotation(): void { $this->request ->expects($this->once()) ->method('getRemoteAddress') @@ -73,20 +74,51 @@ class BruteForceMiddlewareTest extends TestCase { ->method('sleepDelayOrThrowOnMax') ->with('127.0.0.1', 'login'); - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); - $this->bruteForceMiddleware->beforeController($controller, 'testMethod'); + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithAnnotation'); } - public function testBeforeControllerWithoutAnnotation() { - $this->reflector + public function testBeforeControllerWithSingleAttribute(): void { + $this->request ->expects($this->once()) - ->method('hasAnnotation') - ->with('BruteForceProtection') - ->willReturn(false); - $this->reflector - ->expects($this->never()) - ->method('getAnnotationParameter'); + ->method('getRemoteAddress') + ->willReturn('::1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('::1', 'single'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'singleAttribute'); + $this->bruteForceMiddleware->beforeController($controller, 'singleAttribute'); + } + + public function testBeforeControllerWithMultipleAttributes(): void { + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('::1'); + + $calls = [ + ['::1', 'first'], + ['::1', 'second'], + ]; + $this->throttler + ->expects($this->exactly(2)) + ->method('sleepDelayOrThrowOnMax') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return 0; + }); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'multipleAttributes'); + $this->bruteForceMiddleware->beforeController($controller, 'multipleAttributes'); + } + + public function testBeforeControllerWithoutAnnotation(): void { $this->request ->expects($this->never()) ->method('getRemoteAddress'); @@ -94,19 +126,14 @@ class BruteForceMiddlewareTest extends TestCase { ->expects($this->never()) ->method('sleepDelayOrThrowOnMax'); - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); - $this->bruteForceMiddleware->beforeController($controller, 'testMethod'); + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); } - public function testAfterControllerWithAnnotationAndThrottledRequest() { + public function testAfterControllerWithAnnotationAndThrottledRequest(): void { /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ $response = $this->createMock(Response::class); - $this->reflector - ->expects($this->once()) - ->method('hasAnnotation') - ->with('BruteForceProtection') - ->willReturn(true); $response ->expects($this->once()) ->method('isThrottled') @@ -115,79 +142,187 @@ class BruteForceMiddlewareTest extends TestCase { ->expects($this->once()) ->method('getThrottleMetadata') ->willReturn([]); - $this->reflector - ->expects($this->once()) - ->method('getAnnotationParameter') - ->with('BruteForceProtection', 'action') - ->willReturn('login'); $this->request ->expects($this->once()) ->method('getRemoteAddress') ->willReturn('127.0.0.1'); $this->throttler ->expects($this->once()) - ->method('sleepDelay') + ->method('sleepDelayOrThrowOnMax') ->with('127.0.0.1', 'login'); $this->throttler ->expects($this->once()) ->method('registerAttempt') ->with('login', '127.0.0.1'); - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); - $this->bruteForceMiddleware->afterController($controller, 'testMethod', $response); + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response); } - public function testAfterControllerWithAnnotationAndNotThrottledRequest() { + public function testAfterControllerWithAnnotationAndNotThrottledRequest(): void { /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ $response = $this->createMock(Response::class); - $this->reflector - ->expects($this->once()) - ->method('hasAnnotation') - ->with('BruteForceProtection') - ->willReturn(true); $response ->expects($this->once()) ->method('isThrottled') ->willReturn(false); - $this->reflector - ->expects($this->never()) - ->method('getAnnotationParameter'); $this->request ->expects($this->never()) ->method('getRemoteAddress'); $this->throttler ->expects($this->never()) - ->method('sleepDelay'); + ->method('sleepDelayOrThrowOnMax'); $this->throttler ->expects($this->never()) ->method('registerAttempt'); - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); - $this->bruteForceMiddleware->afterController($controller, 'testMethod', $response); + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response); } - public function testAfterControllerWithoutAnnotation() { - $this->reflector + public function testAfterControllerWithSingleAttribute(): void { + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $response ->expects($this->once()) - ->method('hasAnnotation') - ->with('BruteForceProtection') - ->willReturn(false); - $this->reflector + ->method('isThrottled') + ->willReturn(true); + $response + ->expects($this->once()) + ->method('getThrottleMetadata') + ->willReturn([]); + + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('::1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('::1', 'single'); + $this->throttler + ->expects($this->once()) + ->method('registerAttempt') + ->with('single', '::1'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'singleAttribute'); + $this->bruteForceMiddleware->afterController($controller, 'singleAttribute', $response); + } + + public function testAfterControllerWithMultipleAttributesGeneralMatch(): void { + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $response + ->expects($this->once()) + ->method('isThrottled') + ->willReturn(true); + $response + ->expects($this->once()) + ->method('getThrottleMetadata') + ->willReturn([]); + + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('::1'); + + $sleepCalls = [ + ['::1', 'first'], + ['::1', 'second'], + ]; + $this->throttler + ->expects($this->exactly(2)) + ->method('sleepDelayOrThrowOnMax') + ->willReturnCallback(function () use (&$sleepCalls) { + $expected = array_shift($sleepCalls); + $this->assertEquals($expected, func_get_args()); + return 0; + }); + + $attemptCalls = [ + ['first', '::1', []], + ['second', '::1', []], + ]; + $this->throttler + ->expects($this->exactly(2)) + ->method('registerAttempt') + ->willReturnCallback(function () use (&$attemptCalls): void { + $expected = array_shift($attemptCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'multipleAttributes'); + $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response); + } + + public function testAfterControllerWithMultipleAttributesSpecificMatch(): void { + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $response + ->expects($this->once()) + ->method('isThrottled') + ->willReturn(true); + $response + ->expects($this->once()) + ->method('getThrottleMetadata') + ->willReturn(['action' => 'second']); + + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('::1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('::1', 'second'); + $this->throttler + ->expects($this->once()) + ->method('registerAttempt') + ->with('second', '::1'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'multipleAttributes'); + $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response); + } + + public function testAfterControllerWithoutAnnotation(): void { + $this->request + ->expects($this->never()) + ->method('getRemoteAddress'); + $this->throttler ->expects($this->never()) - ->method('getAnnotationParameter'); + ->method('sleepDelayOrThrowOnMax'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response); + } + + public function testAfterControllerWithThrottledResponseButUnhandled(): void { $this->request ->expects($this->never()) ->method('getRemoteAddress'); $this->throttler ->expects($this->never()) - ->method('sleepDelay'); + ->method('sleepDelayOrThrowOnMax'); - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ $response = $this->createMock(Response::class); - $this->bruteForceMiddleware->afterController($controller, 'testMethod', $response); + $response->method('isThrottled') + ->willReturn(true); + + $this->logger->expects($this->once()) + ->method('debug') + ->with('Response for Test\AppFramework\Middleware\Security\TestController::testMethodWithoutAnnotation got bruteforce throttled but has no annotation nor attribute defined.'); + + $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response); } } diff --git a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php index cc6c74c16c4..c325ae638fb 100644 --- a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php @@ -1,51 +1,60 @@ <?php + /** - * ownCloud - App Framework - * - * This file is licensed under the Affero General Public License version 3 or - * later. See the COPYING file. - * - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @copyright Bernhard Posselt 2014 + * SPDX-FileCopyrightText: 2016-2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace Test\AppFramework\Middleware\Security; use OC\AppFramework\Http\Request; use OC\AppFramework\Middleware\Security\CORSMiddleware; use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; use OC\AppFramework\Utility\ControllerMethodReflector; -use OC\Security\Bruteforce\Throttler; +use OC\Authentication\Exceptions\PasswordLoginForbiddenException; 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 OCP\Security\Bruteforce\IThrottler; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\AppFramework\Middleware\Security\Mock\CORSMiddlewareController; class CORSMiddlewareTest extends \Test\TestCase { - /** @var ControllerMethodReflector */ private $reflector; - /** @var Session|\PHPUnit\Framework\MockObject\MockObject */ + /** @var Session|MockObject */ private $session; - /** @var Throttler */ + /** @var IThrottler|MockObject */ private $throttler; - /** @var Controller */ + /** @var CORSMiddlewareController */ private $controller; + private LoggerInterface $logger; protected function setUp(): void { parent::setUp(); $this->reflector = new ControllerMethodReflector(); $this->session = $this->createMock(Session::class); - $this->throttler = $this->createMock(Throttler::class); - $this->controller = $this->createMock(Controller::class); + $this->throttler = $this->createMock(IThrottler::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->controller = new CORSMiddlewareController( + 'test', + $this->createMock(IRequest::class) + ); + } + + public static function dataSetCORSAPIHeader(): array { + return [ + ['testSetCORSAPIHeader'], + ['testSetCORSAPIHeaderAttribute'], + ]; } - /** - * @CORS - */ - public function testSetCORSAPIHeader() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataSetCORSAPIHeader')] + public function testSetCORSAPIHeader(string $method): void { $request = new Request( [ 'server' => [ @@ -55,16 +64,15 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->createMock(IRequestId::class), $this->createMock(IConfig::class) ); - $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); - $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' => [ @@ -74,37 +82,45 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->createMock(IRequestId::class), $this->createMock(IConfig::class) ); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); $response = $middleware->afterController($this->controller, __FUNCTION__, new Response()); $headers = $response->getHeaders(); $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers)); } + public static function dataNoOriginHeaderNoCORSHEADER(): array { + return [ + ['testNoOriginHeaderNoCORSHEADER'], + ['testNoOriginHeaderNoCORSHEADERAttribute'], + ]; + } - /** - * @CORS - */ - public function testNoOriginHeaderNoCORSHEADER() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoOriginHeaderNoCORSHEADER')] + public function testNoOriginHeaderNoCORSHEADER(string $method): void { $request = new Request( [], $this->createMock(IRequestId::class), $this->createMock(IConfig::class) ); - $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); - $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 static function dataCorsIgnoredIfWithCredentialsHeaderPresent(): array { + return [ + ['testCorsIgnoredIfWithCredentialsHeaderPresent'], + ['testCorsAttributeIgnoredIfWithCredentialsHeaderPresent'], + ]; + } - /** - * @CORS - */ - public function testCorsIgnoredIfWithCredentialsHeaderPresent() { - $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\SecurityException::class); + #[\PHPUnit\Framework\Attributes\DataProvider('dataCorsIgnoredIfWithCredentialsHeaderPresent')] + public function testCorsIgnoredIfWithCredentialsHeaderPresent(string $method): void { + $this->expectException(SecurityException::class); $request = new Request( [ @@ -115,41 +131,87 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->createMock(IRequestId::class), $this->createMock(IConfig::class) ); - $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); $response = new Response(); $response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE'); - $middleware->afterController($this->controller, __FUNCTION__, $response); + $middleware->afterController($this->controller, $method, $response); + } + + public static function dataNoCORSOnAnonymousPublicPage(): array { + return [ + ['testNoCORSOnAnonymousPublicPage'], + ['testNoCORSOnAnonymousPublicPageAttribute'], + ['testNoCORSAttributeOnAnonymousPublicPage'], + ['testNoCORSAttributeOnAnonymousPublicPageAttribute'], + ]; } - /** - * @CORS - * @PublicPage - */ - public function testNoCORSShouldAllowCookieAuth() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCORSOnAnonymousPublicPage')] + public function testNoCORSOnAnonymousPublicPage(string $method): void { $request = new Request( [], $this->createMock(IRequestId::class), $this->createMock(IConfig::class) ); - $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + $this->session->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); $this->session->expects($this->never()) ->method('logout'); $this->session->expects($this->never()) ->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, $method); + } + + public static function dataCORSShouldNeverAllowCookieAuth(): array { + return [ + ['testCORSShouldNeverAllowCookieAuth'], + ['testCORSShouldNeverAllowCookieAuthAttribute'], + ['testCORSAttributeShouldNeverAllowCookieAuth'], + ['testCORSAttributeShouldNeverAllowCookieAuthAttribute'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNeverAllowCookieAuth')] + public function testCORSShouldNeverAllowCookieAuth(string $method): void { + $request = new Request( + [], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + $this->session->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->session->expects($this->once()) + ->method('logout'); + $this->session->expects($this->never()) + ->method('logClientIn') + ->with($this->equalTo('user'), $this->equalTo('pass')) + ->willReturn(true); + + $this->expectException(SecurityException::class); + $middleware->beforeController($this->controller, $method); + } - $middleware->beforeController($this->controller, __FUNCTION__); + public static function dataCORSShouldRelogin(): array { + return [ + ['testCORSShouldRelogin'], + ['testCORSAttributeShouldRelogin'], + ]; } - /** - * @CORS - */ - public function testCORSShouldRelogin() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldRelogin')] + public function testCORSShouldRelogin(string $method): void { $request = new Request( ['server' => [ 'PHP_AUTH_USER' => 'user', @@ -164,17 +226,22 @@ class CORSMiddlewareTest extends \Test\TestCase { ->method('logClientIn') ->with($this->equalTo('user'), $this->equalTo('pass')) ->willReturn(true); - $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); - $middleware->beforeController($this->controller, __FUNCTION__); + $middleware->beforeController($this->controller, $method); } - /** - * @CORS - */ - public function testCORSShouldFailIfPasswordLoginIsForbidden() { - $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\SecurityException::class); + public static function dataCORSShouldFailIfPasswordLoginIsForbidden(): array { + return [ + ['testCORSShouldFailIfPasswordLoginIsForbidden'], + ['testCORSAttributeShouldFailIfPasswordLoginIsForbidden'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldFailIfPasswordLoginIsForbidden')] + public function testCORSShouldFailIfPasswordLoginIsForbidden(string $method): void { + $this->expectException(SecurityException::class); $request = new Request( ['server' => [ @@ -189,18 +256,23 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->session->expects($this->once()) ->method('logClientIn') ->with($this->equalTo('user'), $this->equalTo('pass')) - ->will($this->throwException(new \OC\Authentication\Exceptions\PasswordLoginForbiddenException)); - $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + ->willThrowException(new PasswordLoginForbiddenException); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); - $middleware->beforeController($this->controller, __FUNCTION__); + $middleware->beforeController($this->controller, $method); } - /** - * @CORS - */ - public function testCORSShouldNotAllowCookieAuth() { - $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\SecurityException::class); + public static function dataCORSShouldNotAllowCookieAuth(): array { + return [ + ['testCORSShouldNotAllowCookieAuth'], + ['testCORSAttributeShouldNotAllowCookieAuth'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNotAllowCookieAuth')] + public function testCORSShouldNotAllowCookieAuth(string $method): void { + $this->expectException(SecurityException::class); $request = new Request( ['server' => [ @@ -216,13 +288,13 @@ class CORSMiddlewareTest extends \Test\TestCase { ->method('logClientIn') ->with($this->equalTo('user'), $this->equalTo('pass')) ->willReturn(false); - $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); - $middleware->beforeController($this->controller, __FUNCTION__); + $middleware->beforeController($this->controller, $method); } - public function testAfterExceptionWithSecurityExceptionNoStatus() { + public function testAfterExceptionWithSecurityExceptionNoStatus(): void { $request = new Request( ['server' => [ 'PHP_AUTH_USER' => 'user', @@ -231,14 +303,14 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->createMock(IRequestId::class), $this->createMock(IConfig::class) ); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception')); $expected = new JSONResponse(['message' => 'A security exception'], 500); $this->assertEquals($expected, $response); } - public function testAfterExceptionWithSecurityExceptionWithStatus() { + public function testAfterExceptionWithSecurityExceptionWithStatus(): void { $request = new Request( ['server' => [ 'PHP_AUTH_USER' => 'user', @@ -247,15 +319,14 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->createMock(IRequestId::class), $this->createMock(IConfig::class) ); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception', 501)); $expected = new JSONResponse(['message' => 'A security exception'], 501); $this->assertEquals($expected, $response); } - - public function testAfterExceptionWithRegularException() { + public function testAfterExceptionWithRegularException(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('A regular exception'); @@ -267,7 +338,7 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->createMock(IRequestId::class), $this->createMock(IConfig::class) ); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); $middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception')); } } diff --git a/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php index 2dfb04e138e..b0b41b27cb9 100644 --- a/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php @@ -2,25 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\AppFramework\Middleware\Security; @@ -29,24 +12,19 @@ use OC\AppFramework\Middleware\Security\CSPMiddleware; use OC\Security\CSP\ContentSecurityPolicy; use OC\Security\CSP\ContentSecurityPolicyManager; use OC\Security\CSP\ContentSecurityPolicyNonceManager; -use OC\Security\CSRF\CsrfToken; -use OC\Security\CSRF\CsrfTokenManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\EmptyContentSecurityPolicy; use OCP\AppFramework\Http\Response; use PHPUnit\Framework\MockObject\MockObject; class CSPMiddlewareTest extends \Test\TestCase { - - /** @var CSPMiddleware|MockObject */ + /** @var CSPMiddleware&MockObject */ private $middleware; - /** @var Controller|MockObject */ + /** @var Controller&MockObject */ private $controller; - /** @var ContentSecurityPolicyManager|MockObject */ + /** @var ContentSecurityPolicyManager&MockObject */ private $contentSecurityPolicyManager; - /** @var CsrfTokenManager|MockObject */ - private $csrfTokenManager; - /** @var ContentSecurityPolicyNonceManager|MockObject */ + /** @var ContentSecurityPolicyNonceManager&MockObject */ private $cspNonceManager; protected function setUp(): void { @@ -54,16 +32,14 @@ class CSPMiddlewareTest extends \Test\TestCase { $this->controller = $this->createMock(Controller::class); $this->contentSecurityPolicyManager = $this->createMock(ContentSecurityPolicyManager::class); - $this->csrfTokenManager = $this->createMock(CsrfTokenManager::class); $this->cspNonceManager = $this->createMock(ContentSecurityPolicyNonceManager::class); $this->middleware = new CSPMiddleware( $this->contentSecurityPolicyManager, $this->cspNonceManager, - $this->csrfTokenManager ); } - public function testAfterController() { + public function testAfterController(): void { $this->cspNonceManager ->expects($this->once()) ->method('browserSupportsCspV3') @@ -95,7 +71,7 @@ class CSPMiddlewareTest extends \Test\TestCase { $this->middleware->afterController($this->controller, 'test', $response); } - public function testAfterControllerEmptyCSP() { + public function testAfterControllerEmptyCSP(): void { $response = $this->createMock(Response::class); $emptyPolicy = new EmptyContentSecurityPolicy(); $response->expects($this->any()) @@ -107,19 +83,15 @@ class CSPMiddlewareTest extends \Test\TestCase { $this->middleware->afterController($this->controller, 'test', $response); } - public function testAfterControllerWithContentSecurityPolicy3Support() { + public function testAfterControllerWithContentSecurityPolicy3Support(): void { $this->cspNonceManager ->expects($this->once()) ->method('browserSupportsCspV3') ->willReturn(true); - $token = $this->createMock(CsrfToken::class); - $token - ->expects($this->once()) - ->method('getEncryptedValue') - ->willReturn('MyEncryptedToken'); - $this->csrfTokenManager + $token = base64_encode('the-nonce'); + $this->cspNonceManager ->expects($this->once()) - ->method('getToken') + ->method('getNonce') ->willReturn($token); $response = $this->createMock(Response::class); $defaultPolicy = new ContentSecurityPolicy(); diff --git a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php index 0a4b3c4f34c..55a70d4c040 100644 --- a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php @@ -2,25 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\AppFramework\Middleware\Security; @@ -34,7 +17,6 @@ use OCP\AppFramework\Http\Response; use PHPUnit\Framework\MockObject\MockObject; class FeaturePolicyMiddlewareTest extends \Test\TestCase { - /** @var FeaturePolicyMiddleware|MockObject */ private $middleware; /** @var Controller|MockObject */ @@ -52,7 +34,7 @@ class FeaturePolicyMiddlewareTest extends \Test\TestCase { ); } - public function testAfterController() { + public function testAfterController(): void { $response = $this->createMock(Response::class); $defaultPolicy = new FeaturePolicy(); $defaultPolicy->addAllowedCameraDomain('defaultpolicy'); @@ -74,7 +56,7 @@ class FeaturePolicyMiddlewareTest extends \Test\TestCase { $this->middleware->afterController($this->controller, 'test', $response); } - public function testAfterControllerEmptyCSP() { + public function testAfterControllerEmptyCSP(): void { $response = $this->createMock(Response::class); $emptyPolicy = new EmptyFeaturePolicy(); $response->method('getFeaturePolicy') diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php new file mode 100644 index 00000000000..8ab3a48b62e --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php @@ -0,0 +1,145 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\CORS; +use OCP\AppFramework\Http\Attribute\PublicPage; + +class CORSMiddlewareController extends 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() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php b/tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php new file mode 100644 index 00000000000..4d6778e98b9 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +use OCP\AppFramework\Controller; + +class NormalController extends Controller { + public function foo() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php b/tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php new file mode 100644 index 00000000000..93e793ecca9 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +class OCSController extends \OCP\AppFramework\OCSController { + public function foo() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php new file mode 100644 index 00000000000..cd1cdaa49ca --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; + +class PasswordConfirmationMiddlewareController extends Controller { + public function testNoAnnotationNorAttribute() { + } + + /** + * @TestAnnotation + */ + public function testDifferentAnnotation() { + } + + /** + * @PasswordConfirmationRequired + */ + public function testAnnotation() { + } + + #[PasswordConfirmationRequired] + public function testAttribute() { + } + + #[PasswordConfirmationRequired] + public function testSSO() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php new file mode 100644 index 00000000000..c8f9878b0c1 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php @@ -0,0 +1,171 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\ExAppRequired; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\Attribute\StrictCookiesRequired; +use OCP\AppFramework\Http\Attribute\SubAdminRequired; + +class SecurityMiddlewareController extends 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() { + } + + /** + * @ExAppRequired + */ + public function testAnnotationExAppRequired() { + } + + #[ExAppRequired] + public function testAttributeExAppRequired() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php index 3153d7f0b08..90e801ca471 100644 --- a/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\AppFramework\Middleware\Security; @@ -26,74 +10,90 @@ 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 OC\Authentication\Token\IProvider; +use OC\User\Manager; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Authentication\Token\IToken; +use OCP\IRequest; use OCP\ISession; use OCP\IUser; use OCP\IUserSession; +use Psr\Log\LoggerInterface; +use Test\AppFramework\Middleware\Security\Mock\PasswordConfirmationMiddlewareController; use Test\TestCase; class PasswordConfirmationMiddlewareTest extends TestCase { /** @var ControllerMethodReflector */ private $reflector; - /** @var ISession|\PHPUnit\Framework\MockObject\MockObject */ + /** @var ISession&\PHPUnit\Framework\MockObject\MockObject */ private $session; - /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ + /** @var IUserSession&\PHPUnit\Framework\MockObject\MockObject */ private $userSession; - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject */ + /** @var IUser&\PHPUnit\Framework\MockObject\MockObject */ private $user; /** @var PasswordConfirmationMiddleware */ private $middleware; - /** @var Controller */ - private $contoller; - /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + /** @var PasswordConfirmationMiddlewareController */ + private $controller; + /** @var ITimeFactory&\PHPUnit\Framework\MockObject\MockObject */ private $timeFactory; + private IProvider&\PHPUnit\Framework\MockObject\MockObject $tokenProvider; + private LoggerInterface $logger; + /** @var IRequest&\PHPUnit\Framework\MockObject\MockObject */ + private IRequest $request; + /** @var Manager&\PHPUnit\Framework\MockObject\MockObject */ + private Manager $userManager; protected function setUp(): void { $this->reflector = new ControllerMethodReflector(); $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->tokenProvider = $this->createMock(IProvider::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->request = $this->createMock(IRequest::class); + $this->userManager = $this->createMock(Manager::class); + $this->controller = new PasswordConfirmationMiddlewareController( + 'test', + $this->createMock(IRequest::class) + ); $this->middleware = new PasswordConfirmationMiddleware( $this->reflector, $this->session, $this->userSession, - $this->timeFactory + $this->timeFactory, + $this->tokenProvider, + $this->logger, + $this->request, + $this->userManager, ); } - public function testNoAnnotation() { - $this->reflector->reflect(__CLASS__, __FUNCTION__); + public function testNoAnnotationNorAttribute(): void { + $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__); + public function testDifferentAnnotation(): void { + $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__); + #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')] + public function testAnnotation($backend, $lastConfirm, $currentTime, $exception): void { + $this->reflector->reflect($this->controller, __FUNCTION__); $this->user->method('getBackendClassName') ->willReturn($backend); @@ -107,9 +107,16 @@ class PasswordConfirmationMiddlewareTest extends TestCase { $this->timeFactory->method('getTime') ->willReturn($currentTime); + $token = $this->createMock(IToken::class); + $token->method('getScopeAsArray') + ->willReturn([]); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->willReturn($token); + $thrown = false; try { - $this->middleware->beforeController($this->contoller, __FUNCTION__); + $this->middleware->beforeController($this->controller, __FUNCTION__); } catch (NotConfirmedException $e) { $thrown = true; } @@ -117,7 +124,42 @@ class PasswordConfirmationMiddlewareTest extends TestCase { $this->assertSame($exception, $thrown); } - public function dataProvider() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')] + public function testAttribute($backend, $lastConfirm, $currentTime, $exception): void { + $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); + + $token = $this->createMock(IToken::class); + $token->method('getScopeAsArray') + ->willReturn([]); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->willReturn($token); + + $thrown = false; + try { + $this->middleware->beforeController($this->controller, __FUNCTION__); + } catch (NotConfirmedException $e) { + $thrown = true; + } + + $this->assertSame($exception, $thrown); + } + + + + public static function dataProvider(): array { return [ ['foo', 2000, 4000, true], ['foo', 2000, 3000, false], @@ -127,4 +169,41 @@ class PasswordConfirmationMiddlewareTest extends TestCase { ['foo', 2000, 3816, true], ]; } + + public function testSSO(): void { + static $sessionId = 'mySession1d'; + + $this->reflector->reflect($this->controller, __FUNCTION__); + + $this->user->method('getBackendClassName') + ->willReturn('fictional_backend'); + $this->userSession->method('getUser') + ->willReturn($this->user); + + $this->session->method('get') + ->with('last-password-confirm') + ->willReturn(0); + $this->session->method('getId') + ->willReturn($sessionId); + + $this->timeFactory->method('getTime') + ->willReturn(9876); + + $token = $this->createMock(IToken::class); + $token->method('getScopeAsArray') + ->willReturn([IToken::SCOPE_SKIP_PASSWORD_VALIDATION => true]); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with($sessionId) + ->willReturn($token); + + $thrown = false; + try { + $this->middleware->beforeController($this->controller, __FUNCTION__); + } catch (NotConfirmedException) { + $thrown = true; + } + + $this->assertSame(false, $thrown); + } } diff --git a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php index aa713b99156..c42baadcb1c 100644 --- a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php @@ -1,91 +1,113 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\AppFramework\Middleware\Security; use OC\AppFramework\Middleware\Security\RateLimitingMiddleware; use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Security\Ip\BruteforceAllowList; use OC\Security\RateLimiting\Exception\RateLimitExceededException; use OC\Security\RateLimiting\Limiter; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\IAppConfig; use OCP\IRequest; +use OCP\ISession; use OCP\IUser; use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; +class TestRateLimitController extends Controller { + /** + * @UserRateThrottle(limit=20, period=200) + * @AnonRateThrottle(limit=10, period=100) + */ + public function testMethodWithAnnotation() { + } + + /** + * @AnonRateThrottle(limit=10, period=100) + */ + public function testMethodWithAnnotationFallback() { + } + + public function testMethodWithoutAnnotation() { + } + + #[UserRateLimit(limit: 20, period: 200)] + #[AnonRateLimit(limit: 10, period: 100)] + public function testMethodWithAttributes() { + } + + #[AnonRateLimit(limit: 10, period: 100)] + public function testMethodWithAttributesFallback() { + } +} + /** * @group DB */ class RateLimitingMiddlewareTest extends TestCase { - /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ - private $request; - /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ - private $userSession; - /** @var ControllerMethodReflector|\PHPUnit\Framework\MockObject\MockObject */ - private $reflector; - /** @var Limiter|\PHPUnit\Framework\MockObject\MockObject */ - private $limiter; - /** @var RateLimitingMiddleware */ - private $rateLimitingMiddleware; + private IRequest|MockObject $request; + private IUserSession|MockObject $userSession; + private ControllerMethodReflector $reflector; + private Limiter|MockObject $limiter; + private ISession|MockObject $session; + private IAppConfig|MockObject $appConfig; + private BruteforceAllowList|MockObject $bruteForceAllowList; + private RateLimitingMiddleware $rateLimitingMiddleware; protected function setUp(): void { parent::setUp(); $this->request = $this->createMock(IRequest::class); $this->userSession = $this->createMock(IUserSession::class); - $this->reflector = $this->createMock(ControllerMethodReflector::class); + $this->reflector = new ControllerMethodReflector(); $this->limiter = $this->createMock(Limiter::class); + $this->session = $this->createMock(ISession::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->bruteForceAllowList = $this->createMock(BruteforceAllowList::class); $this->rateLimitingMiddleware = new RateLimitingMiddleware( $this->request, $this->userSession, $this->reflector, - $this->limiter + $this->limiter, + $this->session, + $this->appConfig, + $this->bruteForceAllowList, ); } - public function testBeforeControllerWithoutAnnotation() { - $this->reflector - ->expects($this->at(0)) - ->method('getAnnotationParameter') - ->with('AnonRateThrottle', 'limit') - ->willReturn(''); - $this->reflector - ->expects($this->at(1)) - ->method('getAnnotationParameter') - ->with('AnonRateThrottle', 'period') - ->willReturn(''); - $this->reflector - ->expects($this->at(2)) - ->method('getAnnotationParameter') - ->with('UserRateThrottle', 'limit') - ->willReturn(''); - $this->reflector - ->expects($this->at(3)) - ->method('getAnnotationParameter') - ->with('UserRateThrottle', 'period') - ->willReturn(''); + public function testBeforeControllerWithoutAnnotationForAnon(): void { + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->never()) + ->method('registerAnonRequest'); + + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + /** @var TestRateLimitController|MockObject $controller */ + $controller = $this->createMock(TestRateLimitController::class); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); + } + public function testBeforeControllerWithoutAnnotationForLoggedIn(): void { $this->limiter ->expects($this->never()) ->method('registerUserRequest'); @@ -93,39 +115,79 @@ class RateLimitingMiddlewareTest extends TestCase { ->expects($this->never()) ->method('registerAnonRequest'); - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + + /** @var TestRateLimitController|MockObject $controller */ + $controller = $this->createMock(TestRateLimitController::class); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); + } + + public function testBeforeControllerForAnon(): void { + $controller = new TestRateLimitController('test', $this->request); + + $this->request + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerAnonRequest') + ->with(get_class($controller) . '::testMethodWithAnnotation', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation'); + } + + public function testBeforeControllerForLoggedIn(): void { + $controller = new TestRateLimitController('test', $this->request); + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->limiter + ->expects($this->never()) + ->method('registerAnonRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerUserRequest') + ->with(get_class($controller) . '::testMethodWithAnnotation', '20', '200', $user); + + + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation'); } - public function testBeforeControllerForAnon() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + public function testBeforeControllerAnonWithFallback(): void { + $controller = new TestRateLimitController('test', $this->request); $this->request ->expects($this->once()) ->method('getRemoteAddress') ->willReturn('127.0.0.1'); - $this->reflector - ->expects($this->at(0)) - ->method('getAnnotationParameter') - ->with('AnonRateThrottle', 'limit') - ->willReturn('100'); - $this->reflector - ->expects($this->at(1)) - ->method('getAnnotationParameter') - ->with('AnonRateThrottle', 'period') - ->willReturn('10'); - $this->reflector - ->expects($this->at(2)) - ->method('getAnnotationParameter') - ->with('UserRateThrottle', 'limit') - ->willReturn(''); - $this->reflector - ->expects($this->at(3)) - ->method('getAnnotationParameter') - ->with('UserRateThrottle', 'period') - ->willReturn(''); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->limiter ->expects($this->never()) @@ -133,16 +195,39 @@ class RateLimitingMiddlewareTest extends TestCase { $this->limiter ->expects($this->once()) ->method('registerAnonRequest') - ->with(get_class($controller) . '::testMethod', '100', '10', '127.0.0.1'); + ->with(get_class($controller) . '::testMethodWithAnnotationFallback', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAnnotationFallback'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotationFallback'); + } + + public function testBeforeControllerAttributesForAnon(): void { + $controller = new TestRateLimitController('test', $this->request); + + $this->request + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerAnonRequest') + ->with(get_class($controller) . '::testMethodWithAttributes', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAttributes'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes'); } - public function testBeforeControllerForLoggedIn() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ + public function testBeforeControllerAttributesForLoggedIn(): void { + $controller = new TestRateLimitController('test', $this->request); + /** @var IUser|MockObject $user */ $user = $this->createMock(IUser::class); $this->userSession @@ -154,42 +239,21 @@ class RateLimitingMiddlewareTest extends TestCase { ->method('getUser') ->willReturn($user); - $this->reflector - ->expects($this->at(0)) - ->method('getAnnotationParameter') - ->with('AnonRateThrottle', 'limit') - ->willReturn(''); - $this->reflector - ->expects($this->at(1)) - ->method('getAnnotationParameter') - ->with('AnonRateThrottle', 'period') - ->willReturn(''); - $this->reflector - ->expects($this->at(2)) - ->method('getAnnotationParameter') - ->with('UserRateThrottle', 'limit') - ->willReturn('100'); - $this->reflector - ->expects($this->at(3)) - ->method('getAnnotationParameter') - ->with('UserRateThrottle', 'period') - ->willReturn('10'); - $this->limiter ->expects($this->never()) ->method('registerAnonRequest'); $this->limiter ->expects($this->once()) ->method('registerUserRequest') - ->with(get_class($controller) . '::testMethod', '100', '10', $user); + ->with(get_class($controller) . '::testMethodWithAttributes', '20', '200', $user); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); + $this->reflector->reflect($controller, 'testMethodWithAttributes'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes'); } - public function testBeforeControllerAnonWithFallback() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + public function testBeforeControllerAttributesAnonWithFallback(): void { + $controller = new TestRateLimitController('test', $this->request); $this->request ->expects($this->once()) ->method('getRemoteAddress') @@ -198,28 +262,8 @@ class RateLimitingMiddlewareTest extends TestCase { $this->userSession ->expects($this->once()) ->method('isLoggedIn') - ->willReturn(false); + ->willReturn(true); - $this->reflector - ->expects($this->at(0)) - ->method('getAnnotationParameter') - ->with('AnonRateThrottle', 'limit') - ->willReturn('200'); - $this->reflector - ->expects($this->at(1)) - ->method('getAnnotationParameter') - ->with('AnonRateThrottle', 'period') - ->willReturn('20'); - $this->reflector - ->expects($this->at(2)) - ->method('getAnnotationParameter') - ->with('UserRateThrottle', 'limit') - ->willReturn('100'); - $this->reflector - ->expects($this->at(3)) - ->method('getAnnotationParameter') - ->with('UserRateThrottle', 'period') - ->willReturn('10'); $this->limiter ->expects($this->never()) @@ -227,25 +271,23 @@ class RateLimitingMiddlewareTest extends TestCase { $this->limiter ->expects($this->once()) ->method('registerAnonRequest') - ->with(get_class($controller) . '::testMethod', '200', '20', '127.0.0.1'); + ->with(get_class($controller) . '::testMethodWithAttributesFallback', '10', '100', '127.0.0.1'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); + $this->reflector->reflect($controller, 'testMethodWithAttributesFallback'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributesFallback'); } - - public function testAfterExceptionWithOtherException() { + public function testAfterExceptionWithOtherException(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('My test exception'); - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + $controller = new TestRateLimitController('test', $this->request); $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new \Exception('My test exception')); } - public function testAfterExceptionWithJsonBody() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + public function testAfterExceptionWithJsonBody(): void { + $controller = new TestRateLimitController('test', $this->request); $this->request ->expects($this->once()) ->method('getHeader') @@ -258,9 +300,8 @@ class RateLimitingMiddlewareTest extends TestCase { $this->assertEquals($expected, $result); } - public function testAfterExceptionWithHtmlBody() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + public function testAfterExceptionWithHtmlBody(): void { + $controller = new TestRateLimitController('test', $this->request); $this->request ->expects($this->once()) ->method('getHeader') diff --git a/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php index 61f5eeb5aea..7800371f68f 100644 --- a/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\AppFramework\Middleware\Security; @@ -33,7 +17,6 @@ use OCP\AppFramework\Http; use Test\TestCase; class SameSiteCookieMiddlewareTest extends TestCase { - /** @var SameSiteCookieMiddleware */ private $middleware; @@ -51,7 +34,7 @@ class SameSiteCookieMiddlewareTest extends TestCase { $this->middleware = new SameSiteCookieMiddleware($this->request, $this->reflector); } - public function testBeforeControllerNoIndex() { + public function testBeforeControllerNoIndex(): void { $this->request->method('getScriptName') ->willReturn('/ocs/v2.php'); @@ -59,7 +42,7 @@ class SameSiteCookieMiddlewareTest extends TestCase { $this->addToAssertionCount(1); } - public function testBeforeControllerIndexHasAnnotation() { + public function testBeforeControllerIndexHasAnnotation(): void { $this->request->method('getScriptName') ->willReturn('/index.php'); @@ -71,7 +54,7 @@ class SameSiteCookieMiddlewareTest extends TestCase { $this->addToAssertionCount(1); } - public function testBeforeControllerIndexNoAnnotationPassingCheck() { + public function testBeforeControllerIndexNoAnnotationPassingCheck(): void { $this->request->method('getScriptName') ->willReturn('/index.php'); @@ -86,7 +69,7 @@ class SameSiteCookieMiddlewareTest extends TestCase { $this->addToAssertionCount(1); } - public function testBeforeControllerIndexNoAnnotationFailingCheck() { + public function testBeforeControllerIndexNoAnnotationFailingCheck(): void { $this->expectException(LaxSameSiteCookieFailedException::class); $this->request->method('getScriptName') @@ -102,7 +85,7 @@ class SameSiteCookieMiddlewareTest extends TestCase { $this->middleware->beforeController($this->createMock(Controller::class), 'foo'); } - public function testAfterExceptionNoLaxCookie() { + public function testAfterExceptionNoLaxCookie(): void { $ex = new SecurityException(); try { @@ -113,7 +96,7 @@ class SameSiteCookieMiddlewareTest extends TestCase { } } - public function testAfterExceptionLaxCookie() { + public function testAfterExceptionLaxCookie(): void { $ex = new LaxSameSiteCookieFailedException(); $this->request->method('getRequestUri') @@ -121,7 +104,7 @@ class SameSiteCookieMiddlewareTest extends TestCase { $middleware = $this->getMockBuilder(SameSiteCookieMiddleware::class) ->setConstructorArgs([$this->request, $this->reflector]) - ->setMethods(['setSameSiteCookie']) + ->onlyMethods(['setSameSiteCookie']) ->getMock(); $middleware->expects($this->once()) diff --git a/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php index 276ebe8f9ac..0c6fc21357d 100644 --- a/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php @@ -1,23 +1,9 @@ <?php + /** - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace Test\AppFramework\Middleware\Security; @@ -26,6 +12,7 @@ use OC\AppFramework\Http; use OC\AppFramework\Http\Request; use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException; use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException; +use OC\AppFramework\Middleware\Security\Exceptions\ExAppRequiredException; use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException; use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; @@ -33,25 +20,32 @@ use OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException; use OC\AppFramework\Middleware\Security\SecurityMiddleware; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Settings\AuthorizedGroupMapper; +use OC\User\Session; use OCP\App\IAppManager; -use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\Group\ISubAdmin; use OCP\IConfig; +use OCP\IGroupManager; use OCP\IL10N; use OCP\INavigationManager; use OCP\IRequest; use OCP\IRequestId; +use OCP\ISession; use OCP\IURLGenerator; +use OCP\IUser; use OCP\IUserSession; +use OCP\Security\Ip\IRemoteAddress; 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,13 +74,19 @@ class SecurityMiddlewareTest extends \Test\TestCase { parent::setUp(); $this->authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->controller = $this->createMock(Controller::class); + $this->userSession = $this->createMock(Session::class); + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('test'); + $this->userSession->method('getUser')->willReturn($user); + $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); @@ -98,6 +98,15 @@ class SecurityMiddlewareTest extends \Test\TestCase { $this->appManager->expects($this->any()) ->method('isEnabledForUser') ->willReturn($isAppEnabledForUser); + $remoteIpAddress = $this->createMock(IRemoteAddress::class); + $remoteIpAddress->method('allowsAdminActions')->willReturn(true); + + $groupManager = $this->createMock(IGroupManager::class); + $groupManager->method('isAdmin') + ->willReturn($isAdminUser); + $subAdminManager = $this->createMock(ISubAdmin::class); + $subAdminManager->method('isSubAdmin') + ->willReturn($isSubAdmin); return new SecurityMiddleware( $this->request, @@ -107,27 +116,93 @@ class SecurityMiddlewareTest extends \Test\TestCase { $this->logger, 'files', $isLoggedIn, - $isAdminUser, - $isSubAdmin, + $groupManager, + $subAdminManager, $this->appManager, $this->l10n, $this->authorizedGroupMapper, - $this->userSession + $this->userSession, + $remoteIpAddress ); } + public static function dataNoCSRFRequiredPublicPage(): array { + return [ + ['testAnnotationNoCSRFRequiredPublicPage'], + ['testAnnotationNoCSRFRequiredAttributePublicPage'], + ['testAnnotationPublicPageAttributeNoCSRFRequired'], + ['testAttributeNoCSRFRequiredPublicPage'], + ]; + } - /** - * @PublicPage - * @NoCSRFRequired - */ - public function testSetNavigationEntry() { + public static function dataPublicPage(): array { + return [ + ['testAnnotationPublicPage'], + ['testAttributePublicPage'], + ]; + } + + public static function dataNoCSRFRequired(): array { + return [ + ['testAnnotationNoCSRFRequired'], + ['testAttributeNoCSRFRequired'], + ]; + } + + public static function dataPublicPageStrictCookieRequired(): array { + return [ + ['testAnnotationPublicPageStrictCookieRequired'], + ['testAnnotationStrictCookieRequiredAttributePublicPage'], + ['testAnnotationPublicPageAttributeStrictCookiesRequired'], + ['testAttributePublicPageStrictCookiesRequired'], + ]; + } + + public static function dataNoCSRFRequiredPublicPageStrictCookieRequired(): array { + return [ + ['testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired'], + ['testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired'], + ]; + } + + public static function dataNoAdminRequiredNoCSRFRequired(): array { + return [ + ['testAnnotationNoAdminRequiredNoCSRFRequired'], + ['testAttributeNoAdminRequiredNoCSRFRequired'], + ]; + } + + public static function dataNoAdminRequiredNoCSRFRequiredPublicPage(): array { + return [ + ['testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage'], + ['testAttributeNoAdminRequiredNoCSRFRequiredPublicPage'], + ]; + } + + public static function dataNoCSRFRequiredSubAdminRequired(): array { + return [ + ['testAnnotationNoCSRFRequiredSubAdminRequired'], + ['testAnnotationNoCSRFRequiredAttributeSubAdminRequired'], + ['testAnnotationSubAdminRequiredAttributeNoCSRFRequired'], + ['testAttributeNoCSRFRequiredSubAdminRequired'], + ]; + } + + public static function dataExAppRequired(): array { + return [ + ['testAnnotationExAppRequired'], + ['testAttributeExAppRequired'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + 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); } @@ -147,7 +222,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()); @@ -160,75 +235,63 @@ class SecurityMiddlewareTest extends \Test\TestCase { } } - public function testAjaxStatusLoggedInCheck() { + public function testAjaxStatusLoggedInCheck(): void { $this->ajaxExceptionStatus( - __FUNCTION__, + 'testNoAnnotationNorAttribute', 'isLoggedIn', Http::STATUS_UNAUTHORIZED ); } - /** - * @NoCSRFRequired - */ - public function testAjaxNotAdminCheck() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')] + public function testAjaxNotAdminCheck(string $method): void { $this->ajaxExceptionStatus( - __FUNCTION__, + $method, 'isAdminUser', Http::STATUS_FORBIDDEN ); } - /** - * @PublicPage - */ - public function testAjaxStatusCSRFCheck() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] + public function testAjaxStatusCSRFCheck(string $method): void { $this->ajaxExceptionStatus( - __FUNCTION__, + $method, 'passesCSRFCheck', Http::STATUS_PRECONDITION_FAILED ); } - /** - * @PublicPage - * @NoCSRFRequired - */ - public function testAjaxStatusAllGood() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + 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 - */ - public function testNoChecks() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + 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 @@ -251,16 +314,14 @@ 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 - */ - public function testCsrfCheck() { - $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException::class); + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] + public function testCsrfCheck(string $method): void { + $this->expectException(CrossSiteRequestForgeryException::class); $this->request->expects($this->once()) ->method('passesCSRFCheck') @@ -268,28 +329,22 @@ 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 - */ - public function testNoCsrfCheck() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + public function testNoCsrfCheck(string $method): void { $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 - */ - public function testPassesCsrfCheck() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] + public function testPassesCsrfCheck(string $method): void { $this->request->expects($this->once()) ->method('passesCSRFCheck') ->willReturn(true); @@ -297,15 +352,13 @@ 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 - */ - public function testFailCsrfCheck() { - $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException::class); + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] + public function testFailCsrfCheck(string $method): void { + $this->expectException(CrossSiteRequestForgeryException::class); $this->request->expects($this->once()) ->method('passesCSRFCheck') @@ -314,16 +367,13 @@ 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 - */ - public function testStrictCookieRequiredCheck() { - $this->expectException(\OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException::class); + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPageStrictCookieRequired')] + public function testStrictCookieRequiredCheck(string $method): void { + $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException::class); $this->request->expects($this->never()) ->method('passesCSRFCheck'); @@ -331,68 +381,53 @@ 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 - */ - public function testNoStrictCookieRequiredCheck() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + 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 - */ - public function testPassesStrictCookieRequiredCheck() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPageStrictCookieRequired')] + 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 static 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) { + #[\PHPUnit\Framework\Attributes\DataProvider('dataCsrfOcsController')] + public function testCsrfOcsController(string $controllerClass, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception): void { $this->request ->method('getHeader') ->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) { @@ -408,6 +443,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,86 +453,112 @@ class SecurityMiddlewareTest extends \Test\TestCase { } } - /** - * @NoCSRFRequired - * @NoAdminRequired - */ - public function testLoggedInCheck() { - $this->securityCheck(__FUNCTION__, 'isLoggedIn'); + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')] + public function testLoggedInCheck(string $method): void { + $this->securityCheck($method, 'isLoggedIn'); } - - /** - * @NoCSRFRequired - * @NoAdminRequired - */ - public function testFailLoggedInCheck() { - $this->securityCheck(__FUNCTION__, 'isLoggedIn', true); + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')] + public function testFailLoggedInCheck(string $method): void { + $this->securityCheck($method, 'isLoggedIn', true); } - - /** - * @NoCSRFRequired - */ - public function testIsAdminCheck() { - $this->securityCheck(__FUNCTION__, 'isAdminUser'); + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')] + public function testIsAdminCheck(string $method): void { + $this->securityCheck($method, 'isAdminUser'); } - /** - * @NoCSRFRequired - * @SubAdminRequired - */ - public function testIsNotSubAdminCheck() { - $this->reader->reflect(__CLASS__, __FUNCTION__); + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')] + 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 - */ - public function testIsSubAdminCheck() { - $this->reader->reflect(__CLASS__, __FUNCTION__); + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')] + 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 - */ - public function testIsSubAdminAndAdminCheck() { - $this->reader->reflect(__CLASS__, __FUNCTION__); + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')] + 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 - */ - public function testFailIsAdminCheck() { - $this->securityCheck(__FUNCTION__, 'isAdminUser', true); + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')] + public function testFailIsAdminCheck(string $method): void { + $this->securityCheck($method, 'isAdminUser', true); + } + + #[\PHPUnit\Framework\Attributes\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); } + #[\PHPUnit\Framework\Attributes\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); + } + + #[\PHPUnit\Framework\Attributes\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); + } - public function testAfterExceptionNotCaughtThrowsItAgain() { + + public function testAfterExceptionNotCaughtThrowsItAgain(): void { $ex = new \Exception(); $this->expectException(\Exception::class); $this->middleware->afterException($this->controller, 'test', $ex); } - public function testAfterExceptionReturnsRedirectForNotLoggedInUser() { + public function testAfterExceptionReturnsRedirectForNotLoggedInUser(): void { $this->request = new Request( [ - 'server' => - [ + 'server' + => [ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp' ] @@ -526,7 +589,7 @@ class SecurityMiddlewareTest extends \Test\TestCase { $this->assertEquals($expected, $response); } - public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail() { + public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail(): void { $this->request = new Request( [ 'server' => [ @@ -553,7 +616,7 @@ class SecurityMiddlewareTest extends \Test\TestCase { /** * @return array */ - public function exceptionProvider() { + public static function exceptionProvider(): array { return [ [ new AppNotEnabledException(), @@ -568,14 +631,14 @@ class SecurityMiddlewareTest extends \Test\TestCase { } /** - * @dataProvider exceptionProvider * @param SecurityException $exception */ - public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception) { + #[\PHPUnit\Framework\Attributes\DataProvider('exceptionProvider')] + public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception): void { $this->request = new Request( [ - 'server' => - [ + 'server' + => [ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp' ] @@ -597,81 +660,42 @@ class SecurityMiddlewareTest extends \Test\TestCase { $this->assertEquals($expected, $response); } - public function testAfterAjaxExceptionReturnsJSONError() { + public function testAfterAjaxExceptionReturnsJSONError(): void { $response = $this->middleware->afterException($this->controller, 'test', $this->secAjaxException); $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() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')] + public function testExAppRequired(string $method): void { $middleware = $this->getMiddleware(true, false, false); - $this->reader->reflect(__CLASS__, __FUNCTION__); - - $this->appManager->method('getAppPath') - ->with('files') - ->willReturn('foo'); + $this->reader->reflect($this->controller, $method); - $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__); + $session = $this->createMock(ISession::class); + $session->method('get')->with('app_api')->willReturn(true); + $this->userSession->method('getSession')->willReturn($session); - $this->appManager->method('getAppPath') - ->with('files') - ->willReturn('foo'); - - $this->appManager->method('isEnabledForUser') - ->with('files') - ->willReturn(false); + $this->request->expects($this->once()) + ->method('passesStrictCookieCheck') + ->willReturn(true); + $this->request->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(true); - $middleware->beforeController($this->controller, __FUNCTION__); - $this->addToAssertionCount(1); + $middleware->beforeController($this->controller, $method); } - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function testRestrictedAppLoggedIn() { + #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')] + public function testExAppRequiredError(string $method): void { $middleware = $this->getMiddleware(true, false, false, false); - $this->reader->reflect(__CLASS__, __FUNCTION__); + $this->reader->reflect($this->controller, $method); - $this->appManager->method('getAppPath') - ->with('files') - ->willReturn('foo'); + $session = $this->createMock(ISession::class); + $session->method('get')->with('app_api')->willReturn(false); + $this->userSession->method('getSession')->willReturn($session); - $this->expectException(AppNotEnabledException::class); - $middleware->beforeController($this->controller, __FUNCTION__); + $this->expectException(ExAppRequiredException::class); + $middleware->beforeController($this->controller, $method); } } |