0
0
Fork 0
mirror of https://github.com/nextcloud/server.git synced 2025-02-19 07:14:48 +00:00
nextcloud_server/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
Christoph Wurst 49dd79eabb
refactor: Add void return type to PHPUnit test methods
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2024-09-15 22:32:31 +02:00

748 lines
22 KiB
PHP

<?php
/**
* 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;
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;
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\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 SecurityMiddlewareController */
private $controller;
/** @var SecurityException */
private $secException;
/** @var SecurityException */
private $secAjaxException;
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
private $request;
/** @var ControllerMethodReflector */
private $reader;
/** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
private $logger;
/** @var INavigationManager|\PHPUnit\Framework\MockObject\MockObject */
private $navigationManager;
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
private $urlGenerator;
/** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
private $appManager;
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
private $l10n;
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
private $userSession;
/** @var AuthorizedGroupMapper|\PHPUnit\Framework\MockObject\MockObject */
private $authorizedGroupMapper;
protected function setUp(): void {
parent::setUp();
$this->authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::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->l10n = $this->createMock(IL10N::class);
$this->middleware = $this->getMiddleware(true, true, false);
$this->secException = new SecurityException('hey', false);
$this->secAjaxException = new SecurityException('hey', true);
}
private function getMiddleware(bool $isLoggedIn, bool $isAdminUser, bool $isSubAdmin, bool $isAppEnabledForUser = true): SecurityMiddleware {
$this->appManager = $this->createMock(IAppManager::class);
$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,
$this->reader,
$this->navigationManager,
$this->urlGenerator,
$this->logger,
'files',
$isLoggedIn,
$groupManager,
$subAdminManager,
$this->appManager,
$this->l10n,
$this->authorizedGroupMapper,
$this->userSession,
$remoteIpAddress
);
}
public function dataNoCSRFRequiredPublicPage(): array {
return [
['testAnnotationNoCSRFRequiredPublicPage'],
['testAnnotationNoCSRFRequiredAttributePublicPage'],
['testAnnotationPublicPageAttributeNoCSRFRequired'],
['testAttributeNoCSRFRequiredPublicPage'],
];
}
public function dataPublicPage(): array {
return [
['testAnnotationPublicPage'],
['testAttributePublicPage'],
];
}
public function dataNoCSRFRequired(): array {
return [
['testAnnotationNoCSRFRequired'],
['testAttributeNoCSRFRequired'],
];
}
public function dataPublicPageStrictCookieRequired(): array {
return [
['testAnnotationPublicPageStrictCookieRequired'],
['testAnnotationStrictCookieRequiredAttributePublicPage'],
['testAnnotationPublicPageAttributeStrictCookiesRequired'],
['testAttributePublicPageStrictCookiesRequired'],
];
}
public function dataNoCSRFRequiredPublicPageStrictCookieRequired(): array {
return [
['testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired'],
['testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired'],
];
}
public function dataNoAdminRequiredNoCSRFRequired(): array {
return [
['testAnnotationNoAdminRequiredNoCSRFRequired'],
['testAttributeNoAdminRequiredNoCSRFRequired'],
];
}
public function dataNoAdminRequiredNoCSRFRequiredPublicPage(): array {
return [
['testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage'],
['testAttributeNoAdminRequiredNoCSRFRequiredPublicPage'],
];
}
public function dataNoCSRFRequiredSubAdminRequired(): array {
return [
['testAnnotationNoCSRFRequiredSubAdminRequired'],
['testAnnotationNoCSRFRequiredAttributeSubAdminRequired'],
['testAnnotationSubAdminRequiredAttributeNoCSRFRequired'],
['testAttributeNoCSRFRequiredSubAdminRequired'],
];
}
public static function dataExAppRequired(): array {
return [
['testAnnotationExAppRequired'],
['testAttributeExAppRequired'],
];
}
/**
* @dataProvider dataNoCSRFRequiredPublicPage
*/
public function testSetNavigationEntry(string $method): void {
$this->navigationManager->expects($this->once())
->method('setActiveEntry')
->with($this->equalTo('files'));
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
}
/**
* @param string $method
* @param string $test
*/
private function ajaxExceptionStatus($method, $test, $status) {
$isLoggedIn = false;
$isAdminUser = false;
// isAdminUser requires isLoggedIn call to return true
if ($test === 'isAdminUser') {
$isLoggedIn = true;
}
$sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
try {
$this->reader->reflect($this->controller, $method);
$sec->beforeController($this->controller, $method);
} catch (SecurityException $ex) {
$this->assertEquals($status, $ex->getCode());
}
// add assertion if everything should work fine otherwise phpunit will
// complain
if ($status === 0) {
$this->addToAssertionCount(1);
}
}
public function testAjaxStatusLoggedInCheck(): void {
$this->ajaxExceptionStatus(
'testNoAnnotationNorAttribute',
'isLoggedIn',
Http::STATUS_UNAUTHORIZED
);
}
/**
* @dataProvider dataNoCSRFRequired
*/
public function testAjaxNotAdminCheck(string $method): void {
$this->ajaxExceptionStatus(
$method,
'isAdminUser',
Http::STATUS_FORBIDDEN
);
}
/**
* @dataProvider dataPublicPage
*/
public function testAjaxStatusCSRFCheck(string $method): void {
$this->ajaxExceptionStatus(
$method,
'passesCSRFCheck',
Http::STATUS_PRECONDITION_FAILED
);
}
/**
* @dataProvider dataNoCSRFRequiredPublicPage
*/
public function testAjaxStatusAllGood(string $method): void {
$this->ajaxExceptionStatus(
$method,
'isLoggedIn',
0
);
$this->ajaxExceptionStatus(
$method,
'isAdminUser',
0
);
$this->ajaxExceptionStatus(
$method,
'passesCSRFCheck',
0
);
}
/**
* @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($this->controller, $method);
$sec->beforeController($this->controller, $method);
}
/**
* @param string $method
* @param string $expects
*/
private function securityCheck($method, $expects, $shouldFail = false) {
// admin check requires login
if ($expects === 'isAdminUser') {
$isLoggedIn = true;
$isAdminUser = !$shouldFail;
} else {
$isLoggedIn = !$shouldFail;
$isAdminUser = false;
}
$sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
if ($shouldFail) {
$this->expectException(SecurityException::class);
} else {
$this->addToAssertionCount(1);
}
$this->reader->reflect($this->controller, $method);
$sec->beforeController($this->controller, $method);
}
/**
* @dataProvider dataPublicPage
*/
public function testCsrfCheck(string $method): void {
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException::class);
$this->request->expects($this->once())
->method('passesCSRFCheck')
->willReturn(false);
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
}
/**
* @dataProvider dataNoCSRFRequiredPublicPage
*/
public function testNoCsrfCheck(string $method): void {
$this->request->expects($this->never())
->method('passesCSRFCheck')
->willReturn(false);
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
}
/**
* @dataProvider dataPublicPage
*/
public function testPassesCsrfCheck(string $method): void {
$this->request->expects($this->once())
->method('passesCSRFCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
}
/**
* @dataProvider dataPublicPage
*/
public function testFailCsrfCheck(string $method): void {
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException::class);
$this->request->expects($this->once())
->method('passesCSRFCheck')
->willReturn(false);
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
}
/**
* @dataProvider dataPublicPageStrictCookieRequired
*/
public function testStrictCookieRequiredCheck(string $method): void {
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException::class);
$this->request->expects($this->never())
->method('passesCSRFCheck');
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(false);
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
}
/**
* @dataProvider dataNoCSRFRequiredPublicPage
*/
public function testNoStrictCookieRequiredCheck(string $method): void {
$this->request->expects($this->never())
->method('passesStrictCookieCheck')
->willReturn(false);
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
}
/**
* @dataProvider dataNoCSRFRequiredPublicPageStrictCookieRequired
*/
public function testPassesStrictCookieRequiredCheck(string $method): void {
$this->request
->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
}
public function dataCsrfOcsController(): array {
return [
[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 string $controllerClass
* @param bool $hasOcsApiHeader
* @param bool $hasBearerAuth
* @param bool $exception
*/
public function testCsrfOcsController(string $controllerClass, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception): void {
$this->request
->method('getHeader')
->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) {
if ($header === 'OCS-APIREQUEST' && $hasOcsApiHeader) {
return 'true';
}
if ($header === 'Authorization' && $hasBearerAuth) {
return 'Bearer TOKEN!';
}
return '';
});
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$controller = new $controllerClass('test', $this->request);
try {
$this->middleware->beforeController($controller, 'foo');
$this->assertFalse($exception);
} catch (CrossSiteRequestForgeryException $e) {
$this->assertTrue($exception);
}
}
/**
* @dataProvider dataNoAdminRequiredNoCSRFRequired
*/
public function testLoggedInCheck(string $method): void {
$this->securityCheck($method, 'isLoggedIn');
}
/**
* @dataProvider dataNoAdminRequiredNoCSRFRequired
*/
public function testFailLoggedInCheck(string $method): void {
$this->securityCheck($method, 'isLoggedIn', true);
}
/**
* @dataProvider dataNoCSRFRequired
*/
public function testIsAdminCheck(string $method): void {
$this->securityCheck($method, 'isAdminUser');
}
/**
* @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->controller, $method);
}
/**
* @dataProvider dataNoCSRFRequiredSubAdminRequired
*/
public function testIsSubAdminCheck(string $method): void {
$this->reader->reflect($this->controller, $method);
$sec = $this->getMiddleware(true, false, true);
$sec->beforeController($this->controller, $method);
$this->addToAssertionCount(1);
}
/**
* @dataProvider dataNoCSRFRequiredSubAdminRequired
*/
public function testIsSubAdminAndAdminCheck(string $method): void {
$this->reader->reflect($this->controller, $method);
$sec = $this->getMiddleware(true, true, true);
$sec->beforeController($this->controller, $method);
$this->addToAssertionCount(1);
}
/**
* @dataProvider dataNoCSRFRequired
*/
public function testFailIsAdminCheck(string $method): void {
$this->securityCheck($method, 'isAdminUser', true);
}
/**
* @dataProvider dataNoAdminRequiredNoCSRFRequiredPublicPage
*/
public function testRestrictedAppLoggedInPublicPage(string $method): void {
$middleware = $this->getMiddleware(true, false, false);
$this->reader->reflect($this->controller, $method);
$this->appManager->method('getAppPath')
->with('files')
->willReturn('foo');
$this->appManager->method('isEnabledForUser')
->with('files')
->willReturn(false);
$middleware->beforeController($this->controller, $method);
$this->addToAssertionCount(1);
}
/**
* @dataProvider dataNoAdminRequiredNoCSRFRequiredPublicPage
*/
public function testRestrictedAppNotLoggedInPublicPage(string $method): void {
$middleware = $this->getMiddleware(false, false, false);
$this->reader->reflect($this->controller, $method);
$this->appManager->method('getAppPath')
->with('files')
->willReturn('foo');
$this->appManager->method('isEnabledForUser')
->with('files')
->willReturn(false);
$middleware->beforeController($this->controller, $method);
$this->addToAssertionCount(1);
}
/**
* @dataProvider dataNoAdminRequiredNoCSRFRequired
*/
public function testRestrictedAppLoggedIn(string $method): void {
$middleware = $this->getMiddleware(true, false, false, false);
$this->reader->reflect($this->controller, $method);
$this->appManager->method('getAppPath')
->with('files')
->willReturn('foo');
$this->expectException(AppNotEnabledException::class);
$middleware->beforeController($this->controller, $method);
}
public function testAfterExceptionNotCaughtThrowsItAgain(): void {
$ex = new \Exception();
$this->expectException(\Exception::class);
$this->middleware->afterException($this->controller, 'test', $ex);
}
public function testAfterExceptionReturnsRedirectForNotLoggedInUser(): void {
$this->request = new Request(
[
'server' =>
[
'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
]
],
$this->createMock(IRequestId::class),
$this->createMock(IConfig::class)
);
$this->middleware = $this->getMiddleware(false, false, false);
$this->urlGenerator
->expects($this->once())
->method('linkToRoute')
->with(
'core.login.showLoginForm',
[
'redirect_url' => 'nextcloud/index.php/apps/specialapp',
]
)
->willReturn('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
$this->logger
->expects($this->once())
->method('debug');
$response = $this->middleware->afterException(
$this->controller,
'test',
new NotLoggedInException()
);
$expected = new RedirectResponse('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
$this->assertEquals($expected, $response);
}
public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail(): void {
$this->request = new Request(
[
'server' => [
'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp',
],
],
$this->createMock(IRequestId::class),
$this->createMock(IConfig::class)
);
$this->middleware = $this->getMiddleware(false, false, false);
$response = $this->middleware->afterException(
$this->controller,
'test',
new StrictCookieMissingException()
);
$expected = new RedirectResponse(\OC::$WEBROOT . '/');
$this->assertEquals($expected, $response);
}
/**
* @return array
*/
public function exceptionProvider() {
return [
[
new AppNotEnabledException(),
],
[
new CrossSiteRequestForgeryException(),
],
[
new NotAdminException(''),
],
];
}
/**
* @dataProvider exceptionProvider
* @param SecurityException $exception
*/
public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception): void {
$this->request = new Request(
[
'server' =>
[
'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
]
],
$this->createMock(IRequestId::class),
$this->createMock(IConfig::class)
);
$this->middleware = $this->getMiddleware(false, false, false);
$this->logger
->expects($this->once())
->method('debug');
$response = $this->middleware->afterException(
$this->controller,
'test',
$exception
);
$expected = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
$expected->setStatus($exception->getCode());
$this->assertEquals($expected, $response);
}
public function testAfterAjaxExceptionReturnsJSONError(): void {
$response = $this->middleware->afterException($this->controller, 'test',
$this->secAjaxException);
$this->assertTrue($response instanceof JSONResponse);
}
/**
* @dataProvider dataExAppRequired
*/
public function testExAppRequired(string $method): void {
$middleware = $this->getMiddleware(true, false, false);
$this->reader->reflect($this->controller, $method);
$session = $this->createMock(ISession::class);
$session->method('get')->with('app_api')->willReturn(true);
$this->userSession->method('getSession')->willReturn($session);
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('passesCSRFCheck')
->willReturn(true);
$middleware->beforeController($this->controller, $method);
}
/**
* @dataProvider dataExAppRequired
*/
public function testExAppRequiredError(string $method): void {
$middleware = $this->getMiddleware(true, false, false, false);
$this->reader->reflect($this->controller, $method);
$session = $this->createMock(ISession::class);
$session->method('get')->with('app_api')->willReturn(false);
$this->userSession->method('getSession')->willReturn($session);
$this->expectException(ExAppRequiredException::class);
$middleware->beforeController($this->controller, $method);
}
}