From ba88a2b79d338e916a7198f64d77efc76395842a Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Wed, 28 Feb 2018 16:10:00 +0100 Subject: [PATCH] [FEATURE] REST API endpoint for deleting a session (log out) --- CHANGELOG.md | 1 + docs/Api/RestApi.apib | 35 ++++++++++ src/Controller/SessionController.php | 32 ++++++++++ src/Controller/Traits/AuthenticationTrait.php | 7 +- .../Controller/AbstractControllerTest.php | 11 +--- .../Controller/Fixtures/Administrator.csv | 1 + .../Fixtures/AdministratorToken.csv | 1 + .../Controller/SessionControllerTest.php | 64 +++++++++++++++++++ 8 files changed, 140 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4994f..c8acfe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## x.y.z ### Added +- REST API endpoint for deleting a session (log out) (#101) - REST API endpoint for deleting a list (#98) - REST API endpoint for getting list details (#89) - System tests for the test and dev environment (#81) diff --git a/docs/Api/RestApi.apib b/docs/Api/RestApi.apib index d55379e..026075a 100644 --- a/docs/Api/RestApi.apib +++ b/docs/Api/RestApi.apib @@ -78,6 +78,41 @@ that require authentication. (The basic auth user name can be any string.) "message": "Not authorized" } +## Single session [/sessions/{session}] + +### Destroy a the session (log out) [DELETE] + ++ Request (application/json) + ++ Response 204 (application/json) + ++ Response 403 (application/json) + + + Body + + { + "code": 403, + "message": "No valid session key was provided as basic auth password." + } + ++ Response 403 (application/json) + + + Body + + { + "code": 403, + "message": "You do not have access to this session." + } + ++ Response 404 (application/json) + + + Body + + { + "code": 404, + "message": "There is no session with that ID." + } + # Group Lists diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index ebdd09a..851e7dc 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -10,8 +10,11 @@ use PhpList\PhpList4\Domain\Model\Identity\AdministratorToken; use PhpList\PhpList4\Domain\Repository\Identity\AdministratorRepository; use PhpList\PhpList4\Domain\Repository\Identity\AdministratorTokenRepository; +use PhpList\PhpList4\Security\Authentication; +use PhpList\RestBundle\Controller\Traits\AuthenticationTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; @@ -22,6 +25,8 @@ */ class SessionController extends FOSRestController implements ClassResourceInterface { + use AuthenticationTrait; + /** * @var AdministratorRepository */ @@ -33,13 +38,16 @@ class SessionController extends FOSRestController implements ClassResourceInterf private $administratorTokenRepository = null; /** + * @param Authentication $authentication * @param AdministratorRepository $administratorRepository * @param AdministratorTokenRepository $tokenRepository */ public function __construct( + Authentication $authentication, AdministratorRepository $administratorRepository, AdministratorTokenRepository $tokenRepository ) { + $this->authentication = $authentication; $this->administratorRepository = $administratorRepository; $this->administratorTokenRepository = $tokenRepository; } @@ -69,6 +77,30 @@ public function postAction(Request $request): View return View::create()->setStatusCode(Response::HTTP_CREATED)->setData($token); } + /** + * Deletes a session. + * + * This action may only be called for sessions that are owned by the authenticated administrator. + * + * @param Request $request + * @param AdministratorToken $token + * + * @return View + * + * @throws AccessDeniedHttpException + */ + public function deleteAction(Request $request, AdministratorToken $token): View + { + $administrator = $this->requireAuthentication($request); + if ($token->getAdministrator() !== $administrator) { + throw new AccessDeniedHttpException('You do not have access to this session.', null, 1519831644); + } + + $this->administratorTokenRepository->remove($token); + + return View::create(); + } + /** * Validates the request. If is it not valid, throws an exception. * diff --git a/src/Controller/Traits/AuthenticationTrait.php b/src/Controller/Traits/AuthenticationTrait.php index ba0353b..851fc8c 100644 --- a/src/Controller/Traits/AuthenticationTrait.php +++ b/src/Controller/Traits/AuthenticationTrait.php @@ -3,6 +3,7 @@ namespace PhpList\RestBundle\Controller\Traits; +use PhpList\PhpList4\Domain\Model\Identity\Administrator; use PhpList\PhpList4\Security\Authentication; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -26,11 +27,11 @@ trait AuthenticationTrait * * @param Request $request * - * @return void + * @return Administrator the authenticated administrator * * @throws AccessDeniedHttpException */ - private function requireAuthentication(Request $request) + private function requireAuthentication(Request $request): Administrator { $administrator = $this->authentication->authenticateByApiKey($request); if ($administrator === null) { @@ -40,5 +41,7 @@ private function requireAuthentication(Request $request) 1512749701851 ); } + + return $administrator; } } diff --git a/tests/Integration/Controller/AbstractControllerTest.php b/tests/Integration/Controller/AbstractControllerTest.php index 654b9ed..bb8051f 100644 --- a/tests/Integration/Controller/AbstractControllerTest.php +++ b/tests/Integration/Controller/AbstractControllerTest.php @@ -192,22 +192,13 @@ protected function assertHttpNotFound() } /** - * Asserts that the current client response has a HTTP FORBIDDEN status and the corresponding error message - * provided in the JSON response. + * Asserts that the current client response has a HTTP FORBIDDEN status. * * @return void */ protected function assertHttpForbidden() { $this->assertHttpStatusWithJsonContentType(Response::HTTP_FORBIDDEN); - - static::assertSame( - [ - 'code' => Response::HTTP_FORBIDDEN, - 'message' => 'No valid session key was provided as basic auth password.', - ], - $this->getDecodedJsonResponseContent() - ); } /** diff --git a/tests/Integration/Controller/Fixtures/Administrator.csv b/tests/Integration/Controller/Fixtures/Administrator.csv index feb074c..3232667 100644 --- a/tests/Integration/Controller/Fixtures/Administrator.csv +++ b/tests/Integration/Controller/Fixtures/Administrator.csv @@ -1,2 +1,3 @@ id,loginname,email,created,modified,password,passwordchanged,disabled,superuser 1,"john.doe","john@example.com","2017-06-22 15:01:17","2017-06-23 19:50:43","1491a3c7e7b23b9a6393323babbb095dee0d7d81b2199617b487bd0fb5236f3c","2017-06-28",0,1 +2,"jane.doe","jane@example.com","2017-06-22 15:01:17","2017-06-23 19:50:43","1491a3c7e7b23b9a6393323babbb095dee0d7d81b2199617b487bd0fb5236f3d","2017-06-28",0,1 diff --git a/tests/Integration/Controller/Fixtures/AdministratorToken.csv b/tests/Integration/Controller/Fixtures/AdministratorToken.csv index d3bb72e..87da431 100644 --- a/tests/Integration/Controller/Fixtures/AdministratorToken.csv +++ b/tests/Integration/Controller/Fixtures/AdministratorToken.csv @@ -1,3 +1,4 @@ id,value,expires,adminid,entered 1,"cfdf64eecbbf336628b0f3071adba762","2027-06-22 16:43:29",1,1512582100 2,"cfdf64eecbbf336628b0f3071adba763","2017-06-22 16:43:29",1,1512582100 +3,"cfdf64eecbbf336628b0f3071adba764","2017-06-22 16:43:29",2,1512582100 diff --git a/tests/Integration/Controller/SessionControllerTest.php b/tests/Integration/Controller/SessionControllerTest.php index e06fc31..4f47347 100644 --- a/tests/Integration/Controller/SessionControllerTest.php +++ b/tests/Integration/Controller/SessionControllerTest.php @@ -196,4 +196,68 @@ public function postSessionsActionWithValidCredentialsCreatesToken() static::assertSame($expiry, $token->getExpiry()->format(\DateTime::ATOM)); static::assertSame($administratorId, $token->getAdministrator()->getId()); } + + /** + * @test + */ + public function deleteSessionWithoutSessionKeyForExistingSessionReturnsForbiddenStatus() + { + $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); + $this->getDataSet()->addTable(static::TOKEN_TABLE_NAME, __DIR__ . '/Fixtures/AdministratorToken.csv'); + $this->applyDatabaseChanges(); + + $this->client->request('delete', '/api/v2/sessions/1'); + + $this->assertHttpForbidden(); + } + + /** + * @test + */ + public function deleteSessionWithCurrentSessionKeyForExistingSessionReturnsNoContentStatus() + { + $this->authenticatedJsonRequest('delete', '/api/v2/sessions/1'); + + $this->assertHttpNoContent(); + } + + /** + * @test + */ + public function deleteSessionWithCurrentSessionKeyForInexistentSessionReturnsNotFoundStatus() + { + $this->authenticatedJsonRequest('delete', '/api/v2/sessions/999'); + + $this->assertHttpNotFound(); + } + + /** + * @test + */ + public function deleteSessionWithCurrentSessionAndOwnSessionKeyDeletesSession() + { + $this->authenticatedJsonRequest('delete', '/api/v2/sessions/1'); + + static::assertNull($this->administratorTokenRepository->find(1)); + } + + /** + * @test + */ + public function deleteSessionWithCurrentSessionAndOwnSessionKeyKeepsReturnsForbiddenStatus() + { + $this->authenticatedJsonRequest('delete', '/api/v2/sessions/3'); + + $this->assertHttpForbidden(); + } + + /** + * @test + */ + public function deleteSessionWithCurrentSessionAndOwnSessionKeyKeepsSession() + { + $this->authenticatedJsonRequest('delete', '/api/v2/sessions/3'); + + static::assertNotNull($this->administratorTokenRepository->find(3)); + } }