diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 7ce87d72d4..51307cc6e1 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,8 +1,16 @@ security: enable_authenticator_manager: true access_control: - - { path: ^/api/doc, roles: PUBLIC_ACCESS } - - { path: ^/(auth|application)/(login|config|logout), roles: PUBLIC_ACCESS } + - { path: '^/api/doc', roles: PUBLIC_ACCESS } + - { path: '^/api$', roles: PUBLIC_ACCESS } + - { path: '^/application/config', roles: PUBLIC_ACCESS } + - { path: '^/auth/(login|logout)', roles: PUBLIC_ACCESS } + - { path: '^/auth', roles: IS_AUTHENTICATED_FULLY } + - { path: '^/api', roles: IS_AUTHENTICATED_FULLY } + - { path: '^/application', roles: IS_AUTHENTICATED_FULLY } + - { path: '^/upload', roles: IS_AUTHENTICATED_FULLY } + - { path: '^/error', roles: IS_AUTHENTICATED_FULLY } + - { path: '^/', roles: PUBLIC_ACCESS } access_decision_manager: allow_if_all_abstain: false strategy: unanimous @@ -18,32 +26,7 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false - authenticated_auth: - pattern: ^/auth - stateless: true - custom_authenticators: - - App\Security\JsonWebTokenAuthenticator - provider: session_user - authenticated_application: - pattern: ^/application - stateless: true - custom_authenticators: - - App\Security\JsonWebTokenAuthenticator - provider: session_user - upload: - pattern: ^/upload - stateless: true - custom_authenticators: - - App\Security\JsonWebTokenAuthenticator - provider: session_user - errors: - pattern: ^/errors - stateless: true - custom_authenticators: - - App\Security\JsonWebTokenAuthenticator - provider: session_user - default: - pattern: ^/api + main: stateless: true custom_authenticators: - App\Security\JsonWebTokenAuthenticator diff --git a/src/Controller/AuthController.php b/src/Controller/AuthController.php index bd82124dce..18a22a4709 100644 --- a/src/Controller/AuthController.php +++ b/src/Controller/AuthController.php @@ -17,6 +17,8 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use function sleep; + class AuthController extends AbstractController { /** @@ -35,15 +37,12 @@ public function loginAction(Request $request, AuthenticationInterface $authentic public function whoamiAction(TokenStorageInterface $tokenStorage): JsonResponse { $token = $tokenStorage->getToken(); - if ($token?->isAuthenticated()) { - /** @var SessionUserInterface $sessionUser */ - $sessionUser = $token->getUser(); - if ($sessionUser instanceof SessionUserInterface) { - return new JsonResponse(['userId' => $sessionUser->getId()], Response::HTTP_OK); - } + $sessionUser = $token?->getUser(); + if (!$token?->isAuthenticated() || !$sessionUser instanceof SessionUserInterface) { + throw new Exception('Attempted to access whoami with no valid user'); } - return new JsonResponse(['userId' => null], Response::HTTP_UNAUTHORIZED); + return new JsonResponse(['userId' => $sessionUser->getId()], Response::HTTP_OK); } /** @@ -56,16 +55,14 @@ public function tokenAction( JsonWebTokenManager $jwtManager ): JsonResponse { $token = $tokenStorage->getToken(); - if ($token?->isAuthenticated()) { - $sessionUser = $token->getUser(); - if ($sessionUser instanceof SessionUserInterface) { - $ttl = $request->get('ttl') ? $request->get('ttl') : 'PT8H'; - $jwt = $jwtManager->createJwtFromSessionUser($sessionUser, $ttl); - return new JsonResponse(['jwt' => $jwt], Response::HTTP_OK); - } + $sessionUser = $token?->getUser(); + if (!$token?->isAuthenticated() || !$sessionUser instanceof SessionUserInterface) { + throw new Exception('Attempted to access token with no valid user'); } - return new JsonResponse(['jwt' => null], Response::HTTP_UNAUTHORIZED); + $ttl = $request->get('ttl') ? $request->get('ttl') : 'PT8H'; + $jwt = $jwtManager->createJwtFromSessionUser($sessionUser, $ttl); + return new JsonResponse(['jwt' => $jwt], Response::HTTP_OK); } /** @@ -92,28 +89,25 @@ public function invalidateTokensAction( ): JsonResponse { $now = new \DateTime(); $token = $tokenStorage->getToken(); - if ($token?->isAuthenticated()) { - /** @var SessionUserInterface $sessionUser */ - $sessionUser = $token->getUser(); - if ($sessionUser instanceof SessionUserInterface) { - /** @var UserInterface $user */ - $user = $userRepository->findOneBy(['id' => $sessionUser->getId()]); - $authentication = $authenticationRepository->findOneBy(['user' => $user->getId()]); - if (!$authentication) { - $authentication = $authenticationRepository->create(); - $authentication->setUser($user); - } + $sessionUser = $token?->getUser(); + if (!$token?->isAuthenticated() || !$sessionUser instanceof SessionUserInterface) { + throw new Exception('Attempted to access invalidate tokens with no valid user'); + } - $authentication->setInvalidateTokenIssuedBefore($now); - $authenticationRepository->update($authentication); + /** @var UserInterface $user */ + $user = $userRepository->findOneBy(['id' => $sessionUser->getId()]); + $authentication = $authenticationRepository->findOneBy(['user' => $user->getId()]); + if (!$authentication) { + $authentication = $authenticationRepository->create(); + $authentication->setUser($user); + } - sleep(1); - $jwt = $jwtManager->createJwtFromSessionUser($sessionUser); + $authentication->setInvalidateTokenIssuedBefore($now); + $authenticationRepository->update($authentication); - return new JsonResponse(['jwt' => $jwt], Response::HTTP_OK); - } - } + sleep(1); + $jwt = $jwtManager->createJwtFromSessionUser($sessionUser); - throw new Exception('Attempted to invalidate token with no valid user'); + return new JsonResponse(['jwt' => $jwt], Response::HTTP_OK); } } diff --git a/tests/AbstractEndpointTest.php b/tests/AbstractEndpointTest.php index 6c8182b636..6bf9b5b1aa 100644 --- a/tests/AbstractEndpointTest.php +++ b/tests/AbstractEndpointTest.php @@ -960,6 +960,28 @@ protected function badPostTest(array $data, $code = Response::HTTP_BAD_REQUEST) $this->assertJsonResponse($response, $code); } + /** + * Test POSTing without authentication to the API + */ + protected function anonymousDeniedPostTest(array $data) + { + $endpoint = $this->getPluralName(); + $responseKey = $this->getCamelCasedPluralName(); + $this->createJsonRequest( + 'POST', + $this->getUrl( + $this->kernelBrowser, + "app_api_${endpoint}_post", + ['version' => $this->apiVersion] + ), + json_encode([$responseKey => [$data]]), + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + /** * Test POSTing bad data to the API * @param array $data @@ -985,6 +1007,28 @@ protected function badPutTest(array $data, $id, $code = Response::HTTP_BAD_REQUE $this->assertJsonResponse($response, $code); } + /** + * Test PUTing as anonymous to the API + */ + protected function anonymousDeniedPutTest(array $data) + { + $endpoint = $this->getPluralName(); + $responseKey = $this->getCamelCasedPluralName(); + $this->createJsonRequest( + 'PUT', + $this->getUrl( + $this->kernelBrowser, + "app_api_${endpoint}_put", + ['version' => $this->apiVersion, 'id' => $data['id']] + ), + json_encode([$responseKey => [$data]]), + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + /** * When relational data is sent to the API ensure it * is recorded on the non-owning side of the relationship @@ -1126,6 +1170,28 @@ protected function patchJsonApiTest(array $data, object $postData) return $fetchedResponseData; } + /** + * Test PATCHing as anonymous to the API + */ + protected function anonymousDeniedPatchTest(array $data) + { + $endpoint = $this->getPluralName(); + $responseKey = $this->getCamelCasedPluralName(); + $this->createJsonRequest( + 'PATCH', + $this->getUrl( + $this->kernelBrowser, + "app_api_${endpoint}_patch", + ['version' => $this->apiVersion, 'id' => $data['id']] + ), + json_encode([$responseKey => [$data]]), + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + /** * Test deleting an object from the API * @@ -1191,6 +1257,49 @@ protected function notFoundTest($badId) $this->assertJsonResponse($response, Response::HTTP_NOT_FOUND); } + /** + * Ensure that anonymous users cannot access the resource + */ + protected function anonymousAccessDeniedOneTest() + { + $endpoint = $this->getPluralName(); + $loader = $this->getDataLoader(); + $data = $loader->getOne(); + $this->createJsonRequest( + 'GET', + $this->getUrl( + $this->kernelBrowser, + "app_api_${endpoint}_getone", + ['version' => $this->apiVersion, 'id' => $data['id']] + ), + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + + /** + * Ensure that anonymous users cannot access the resource + */ + protected function anonymousAccessDeniedAllTest() + { + $endpoint = $this->getPluralName(); + $loader = $this->getDataLoader(); + $this->createJsonRequest( + 'GET', + $this->getUrl( + $this->kernelBrowser, + "app_api_${endpoint}_getall", + ['version' => $this->apiVersion] + ), + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + /** * Test that a filter returns the expected data * @param array $filters we are using diff --git a/tests/Controller/ApiControllerTest.php b/tests/Controller/ApiControllerTest.php index 1191439de9..b8c171f2f2 100644 --- a/tests/Controller/ApiControllerTest.php +++ b/tests/Controller/ApiControllerTest.php @@ -72,4 +72,31 @@ public function testBadVersion() $response = $this->kernelBrowser->getResponse(); $this->assertJsonResponse($response, Response::HTTP_NOT_FOUND); } + + public function testApiInfoAuthenticated() + { + $this->kernelBrowser->request( + 'GET', + '/api', + [], + [], + ['HTTP_X-JWT-Authorization' => 'Token ' . $this->getTokenForUser($this->kernelBrowser, 1)], + ); + + $response = $this->kernelBrowser->getResponse(); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $this->assertStringContainsString('

API Info

', $response->getContent()); + } + + public function testApiInfoNotAuthenticated() + { + $this->kernelBrowser->request( + 'GET', + '/api', + ); + + $response = $this->kernelBrowser->getResponse(); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $this->assertStringContainsString('

API Info

', $response->getContent()); + } } diff --git a/tests/Controller/AuthControllerTest.php b/tests/Controller/AuthControllerTest.php index 76950e9526..248f8ef72b 100644 --- a/tests/Controller/AuthControllerTest.php +++ b/tests/Controller/AuthControllerTest.php @@ -158,9 +158,6 @@ public function testWhoAmIUnauthenticated() $response = $this->kernelBrowser->getResponse(); $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); - $response = json_decode($response->getContent(), true); - $this->assertArrayHasKey('userId', $response); - $this->assertSame($response['userId'], null); } public function testWhoAmIExpiredToken() @@ -242,9 +239,6 @@ public function testGetTokenForUnauthenticatedUser() ); $response = $this->kernelBrowser->getResponse(); $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); - $response = json_decode($response->getContent(), true); - $this->assertArrayHasKey('jwt', $response); - $this->assertSame($response['jwt'], null); } public function testGetTokenForExpiredToken() @@ -301,7 +295,7 @@ public function testInvalidateTokenForUnauthenticatedUser() ); $response = $this->kernelBrowser->getResponse(); - $this->assertJsonResponse($response, Response::HTTP_INTERNAL_SERVER_ERROR); + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); } protected function getExpiredToken(int $userId): string diff --git a/tests/Controller/ErrorControllerTest.php b/tests/Controller/ErrorControllerTest.php index f7ec4b20c2..40fbb51c72 100644 --- a/tests/Controller/ErrorControllerTest.php +++ b/tests/Controller/ErrorControllerTest.php @@ -53,4 +53,23 @@ public function testIndex() $response = $this->kernelBrowser->getResponse(); $this->assertEquals(Response::HTTP_NO_CONTENT, $response->getStatusCode(), $response->getContent()); } + + public function testAnonymousAccessDenied() + { + $faker = FakerFactory::create(); + + $data = [ + 'mainMessage' => $faker->text(100), + 'stack' => $faker->text(1000) + ]; + $this->makeJsonRequest( + $this->kernelBrowser, + 'POST', + '/errors', + json_encode(['data' => json_encode($data)]) + ); + + $response = $this->kernelBrowser->getResponse(); + $this->assertEquals(Response::HTTP_UNAUTHORIZED, $response->getStatusCode()); + } } diff --git a/tests/Controller/UploadControllerTest.php b/tests/Controller/UploadControllerTest.php index 118fa41e50..af35a019de 100644 --- a/tests/Controller/UploadControllerTest.php +++ b/tests/Controller/UploadControllerTest.php @@ -65,6 +65,22 @@ public function testUploadFile() $this->assertSame($data['filename'], 'TESTFILE.txt'); $this->assertSame($data['fileHash'], md5_file(__FILE__)); } + public function testAnonymousUploadFileDenied() + { + $client = static::createClient(); + + $this->makeJsonRequest( + $client, + 'POST', + '/upload', + null, + [], + ['file' => $this->fakeTestFile] + ); + + $response = $client->getResponse(); + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } public function testBadUpload() { diff --git a/tests/Endpoints/AcademicYearTest.php b/tests/Endpoints/AcademicYearTest.php index 0fcd78b464..6382a5a1b9 100644 --- a/tests/Endpoints/AcademicYearTest.php +++ b/tests/Endpoints/AcademicYearTest.php @@ -137,4 +137,37 @@ protected function getYears() return $academicYears; } + + public function anonymousAccessDeniedOneTest() + { + $academicYears = $this->getYears(); + $id = $academicYears[0]['id']; + $this->createJsonRequest( + 'GET', + $this->getUrl( + $this->kernelBrowser, + "app_api_academicyears_getone", + ['version' => $this->apiVersion, 'id' => $id] + ), + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + public function anonymousAccessDeniedAllTest() + { + $this->createJsonRequest( + 'GET', + $this->getUrl( + $this->kernelBrowser, + "app_api_academicyears_getall", + ['version' => $this->apiVersion] + ), + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } } diff --git a/tests/Endpoints/AuthenticationTest.php b/tests/Endpoints/AuthenticationTest.php index 08f6154bee..8567750f85 100644 --- a/tests/Endpoints/AuthenticationTest.php +++ b/tests/Endpoints/AuthenticationTest.php @@ -552,4 +552,49 @@ public function testPutReadOnly($key = null, $id = null, $value = null, $skipped $this->putTest($data, $postData, $id); } } + + public function anonymousAccessDeniedOneTest() + { + $loader = $this->getDataLoader(); + $data = $loader->getOne(); + $this->createJsonRequest( + 'GET', + $this->getUrl( + $this->kernelBrowser, + "app_api_authentications_getone", + ['version' => $this->apiVersion, 'id' => $data['user']] + ), + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + public function anonymousAccessDeniedAllTest() + { + $this->createJsonRequest( + 'GET', + $this->getUrl( + $this->kernelBrowser, + "app_api_authentications_getall", + ['version' => $this->apiVersion] + ), + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + + protected function anonymousDeniedPutTest(array $data) + { + $data['id'] = $data['user']; + parent::anonymousDeniedPutTest($data); + } + + protected function anonymousDeniedPatchTest(array $data) + { + $data['id'] = $data['user']; + parent::anonymousDeniedPatchTest($data); + } } diff --git a/tests/Endpoints/CurrentSessionTest.php b/tests/Endpoints/CurrentSessionTest.php index bed82319cf..875cc9f7e7 100644 --- a/tests/Endpoints/CurrentSessionTest.php +++ b/tests/Endpoints/CurrentSessionTest.php @@ -42,7 +42,7 @@ public function tearDown(): void unset($this->fixtures); } - public function testGetGetCurrentSession() + public function testGetCurrentSession() { $url = $this->getUrl( $this->kernelBrowser, @@ -69,4 +69,20 @@ public function testGetGetCurrentSession() $this->assertEquals(2, $data['userId']); } + public function testAccessDeniedForAnonymousUser() + { + $url = $this->getUrl( + $this->kernelBrowser, + 'ilios_api_currentsession', + ['version' => $this->apiVersion] + ); + $this->makeJsonRequest( + $this->kernelBrowser, + 'GET', + $url, + ); + + $response = $this->kernelBrowser->getResponse(); + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } } diff --git a/tests/Endpoints/CurriculumInventoryExportTest.php b/tests/Endpoints/CurriculumInventoryExportTest.php index fdd3506e7d..26f3ceb12f 100644 --- a/tests/Endpoints/CurriculumInventoryExportTest.php +++ b/tests/Endpoints/CurriculumInventoryExportTest.php @@ -132,4 +132,16 @@ protected function fourOhFourTest($type, array $parameters = []) $this->assertJsonResponse($response, Response::HTTP_NOT_FOUND); } + public function testAnonymousPostDenied() + { + $url = '/api/' . $this->apiVersion . '/curriculuminventoryexports/'; + $this->createJsonRequest( + 'POST', + $url, + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } } diff --git a/tests/Endpoints/SchooleventsTest.php b/tests/Endpoints/SchooleventsTest.php index 7330ea04a0..2d43bc15ad 100644 --- a/tests/Endpoints/SchooleventsTest.php +++ b/tests/Endpoints/SchooleventsTest.php @@ -681,6 +681,29 @@ public function testMissingT0() $this->assertJsonResponse($response, Response::HTTP_BAD_REQUEST); } + public function testAccessDenied() + { + $parameters = [ + 'version' => $this->apiVersion, + 'id' => 1, + 'from' => 1000000, + 'to' => 1000000, + ]; + $url = $this->getUrl( + $this->kernelBrowser, + 'ilios_api_schoolevents', + $parameters + ); + $this->createJsonRequest( + 'GET', + $url, + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + protected function getEvents($schoolId, $from, $to, $userId = null) { $parameters = [ diff --git a/tests/Endpoints/UsereventTest.php b/tests/Endpoints/UsereventTest.php index d98bf483eb..e728aff92a 100644 --- a/tests/Endpoints/UsereventTest.php +++ b/tests/Endpoints/UsereventTest.php @@ -1091,6 +1091,29 @@ public function testMissingTo() $this->assertJsonResponse($response, Response::HTTP_BAD_REQUEST); } + public function testAccessDenied() + { + $parameters = [ + 'version' => $this->apiVersion, + 'from' => 1000000, + 'to' => 1000000, + 'id' => 99, + ]; + $url = $this->getUrl( + $this->kernelBrowser, + 'ilios_api_userevents', + $parameters + ); + $this->createJsonRequest( + 'GET', + $url, + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + /** * @param int $userId * @param int $from diff --git a/tests/Endpoints/UsermaterialsTest.php b/tests/Endpoints/UsermaterialsTest.php index a0eb7eec9c..b595882e52 100644 --- a/tests/Endpoints/UsermaterialsTest.php +++ b/tests/Endpoints/UsermaterialsTest.php @@ -156,6 +156,28 @@ public function testGetMaterialsBeforeTheEndOfTime() $this->assertCount(9, $materials, 'All materials returned'); } + public function testAccessDenied() + { + $parameters = [ + 'version' => $this->apiVersion, + 'id' => 99 + ]; + $url = $this->getUrl( + $this->kernelBrowser, + 'ilios_api_usermaterials', + $parameters + ); + + $this->createJsonRequest( + 'GET', + $url + ); + + $response = $this->kernelBrowser->getResponse(); + + $this->assertJsonResponse($response, Response::HTTP_UNAUTHORIZED); + } + protected function getMaterials($userId, $before = null, $after = null, $authUser = null) { $parameters = [ diff --git a/tests/GetEndpointTestable.php b/tests/GetEndpointTestable.php index c0ebfc028e..3878f5e87a 100644 --- a/tests/GetEndpointTestable.php +++ b/tests/GetEndpointTestable.php @@ -66,4 +66,10 @@ public function testFilters(array $dataKeys = [], array $filterParts = [], $skip } $this->filterTest($filters, $expectedData); } + + public function testAccessDenied() + { + $this->anonymousAccessDeniedOneTest(); + $this->anonymousAccessDeniedAllTest(); + } } diff --git a/tests/ReadWriteEndpointTest.php b/tests/ReadWriteEndpointTest.php index 66e81ce072..f8dd9a0ffa 100644 --- a/tests/ReadWriteEndpointTest.php +++ b/tests/ReadWriteEndpointTest.php @@ -66,6 +66,16 @@ public function testPostBad() $this->badPostTest($data); } + /** + * Test a failure when posting an object + */ + public function testPostAnonymousAccessDenied() + { + $dataLoader = $this->getDataLoader(); + $data = $dataLoader->create(); + $this->anonymousDeniedPostTest($data); + } + /** * Test POST several of this type of object */ @@ -226,4 +236,20 @@ public function testDelete() $data = $dataLoader->getOne(); $this->deleteTest($data['id']); } + + public function testPutAnonymousAccessDenied() + { + $dataLoader = $this->getDataLoader(); + $data = $dataLoader->getOne(); + + $this->anonymousDeniedPutTest($data); + } + + public function testPatchAnonymousAccessDenied() + { + $dataLoader = $this->getDataLoader(); + $data = $dataLoader->getOne(); + + $this->anonymousDeniedPatchTest($data); + } }