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