diff --git a/Makefile b/Makefile index 6e7eafd74c..d6249375ea 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ rebuild: .PHONY: rebuild down: - $(COMPOSE) down $(filter-out $@,$(MAKECMDGOALS)) + $(COMPOSE) down --remove-orphans $(filter-out $@,$(MAKECMDGOALS)) .PHONY: down destroy: diff --git a/mock-integrations/image-request-handler/mock-config.yaml b/mock-integrations/image-request-handler/mock-config.yaml index 597e9fbfdf..4155ca4481 100644 --- a/mock-integrations/image-request-handler/mock-config.yaml +++ b/mock-integrations/image-request-handler/mock-config.yaml @@ -5,6 +5,36 @@ specFile: "mock-openapi.yaml" response: scriptFile: "mock-responses.js" +security: + # no requests permitted by default + default: Deny + + # only requests meeting all these conditions are permitted + conditions: + - effect: Permit + requestHeaders: + Authorization: + value: AWS4-HMAC-SHA256 .* + operator: Matches + +resources: + # always permit status endpoint + - method: GET + path: /system/status + security: + default: Permit + + # always permit _spec viewer endpoint + - method: GET + path: /_spec + security: + default: Permit + - method: GET + path: /_spec/combined.json + security: + default: Permit + + pickFirstIfNoneMatch: false validation: request: true diff --git a/mock-integrations/image-request-handler/mocked-images/v1/healthcheck b/mock-integrations/image-request-handler/mocked-images/v1/healthcheck new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mock-integrations/lpa-data-store/mock-config.yaml b/mock-integrations/lpa-data-store/mock-config.yaml index 597e9fbfdf..632db484fa 100644 --- a/mock-integrations/lpa-data-store/mock-config.yaml +++ b/mock-integrations/lpa-data-store/mock-config.yaml @@ -5,6 +5,41 @@ specFile: "mock-openapi.yaml" response: scriptFile: "mock-responses.js" +security: + # no requests permitted by default + default: Deny + + # only requests meeting all these conditions are permitted + conditions: + - effect: Permit + requestHeaders: + Authorization: + value: AWS4-HMAC-SHA256 .* + operator: Matches + + - effect: Permit + requestHeaders: + X-Jwt-Authorization: + value: Bearer .* + operator: Matches + +resources: + # always permit status endpoint + - method: GET + path: /system/status + security: + default: Permit + + # always permit _spec viewer endpoint + - method: GET + path: /_spec + security: + default: Permit + - method: GET + path: /_spec/combined.json + security: + default: Permit + pickFirstIfNoneMatch: false validation: request: true diff --git a/service-api/app/src/App/src/DataAccess/ApiGateway/RequestSigner.php b/service-api/app/src/App/src/DataAccess/ApiGateway/RequestSigner.php index 27349759c3..c1de5f90d7 100644 --- a/service-api/app/src/App/src/DataAccess/ApiGateway/RequestSigner.php +++ b/service-api/app/src/App/src/DataAccess/ApiGateway/RequestSigner.php @@ -29,6 +29,14 @@ public function sign(RequestInterface $request): RequestInterface $request = $request->withHeader($header, $value); } - return $this->signer->signRequest($request, $this->credentials); + $request = $this->signer->signRequest($request, $this->credentials); + + // if the Authorization header has been supplied then it is with the understanding + // that it replaces anything generated by the AWS signing process. + if (array_key_exists('Authorization', $this->headers)) { + $request = $request->withHeader('Authorization', $this->headers['Authorization']); + } + + return $request; } } diff --git a/service-api/app/src/App/src/Handler/Factory/HealthcheckHandlerFactory.php b/service-api/app/src/App/src/Handler/Factory/HealthcheckHandlerFactory.php index e66350bd89..411ec1a0ed 100644 --- a/service-api/app/src/App/src/Handler/Factory/HealthcheckHandlerFactory.php +++ b/service-api/app/src/App/src/Handler/Factory/HealthcheckHandlerFactory.php @@ -4,13 +4,14 @@ namespace App\Handler\Factory; -use App\DataAccess\ApiGateway\RequestSigner; +use App\DataAccess\ApiGateway\RequestSignerFactory; use App\DataAccess\Repository\ActorUsersInterface; use App\Handler\HealthcheckHandler; -use GuzzleHttp\Client as HttpClient; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; class HealthcheckHandlerFactory { @@ -22,10 +23,11 @@ public function __invoke(ContainerInterface $container): HealthcheckHandler $config = $container->get('config'); return new HealthcheckHandler( - $config['version'], + $container->get(ClientInterface::class), + $container->get(RequestFactoryInterface::class), + $container->get(RequestSignerFactory::class), $container->get(ActorUsersInterface::class), - $container->get(HttpClient::class), - $container->get(RequestSigner::class), + $config['version'], $config['sirius_api']['endpoint'], $config['codes_api']['endpoint'], $config['iap_images_api']['endpoint'] diff --git a/service-api/app/src/App/src/Handler/HealthcheckHandler.php b/service-api/app/src/App/src/Handler/HealthcheckHandler.php index cab6997e5a..e3243c8f22 100644 --- a/service-api/app/src/App/src/Handler/HealthcheckHandler.php +++ b/service-api/app/src/App/src/Handler/HealthcheckHandler.php @@ -4,12 +4,15 @@ namespace App\Handler; -use App\DataAccess\ApiGateway\RequestSigner; +use App\DataAccess\ApiGateway\RequestSignerFactory; +use App\DataAccess\ApiGateway\SignatureType; use App\DataAccess\Repository\ActorUsersInterface; use Fig\Http\Message\StatusCodeInterface; -use GuzzleHttp\Client as HttpClient; -use GuzzleHttp\Psr7\Request; use Laminas\Diactoros\Response\JsonResponse; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -19,13 +22,14 @@ final class HealthcheckHandler implements RequestHandlerInterface { public function __construct( - private string $version, - private ActorUsersInterface $actorUsers, - private HttpClient $httpClient, - private RequestSigner $awsSignature, - private string $siriusApiUrl, - private string $codesApiUrl, - private string $iapImagesApiUrl, + private readonly ClientInterface $httpClient, + private readonly RequestFactoryInterface $requestFactory, + private readonly RequestSignerFactory $requestSignerFactory, + private readonly ActorUsersInterface $actorUsers, + private readonly string $version, + private readonly string $siriusApiUrl, + private readonly string $codesApiUrl, + private readonly string $iapImagesApiUrl, ) { } @@ -52,18 +56,49 @@ private function isHealthy(array $data): bool && $data['iap_images_api']['healthy']; } + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ private function checkIapImagesApi(): array { - $url = sprintf('%s/v1/healthcheck', $this->iapImagesApiUrl); + $request = $this->requestFactory->createRequest( + 'GET', + sprintf('%s/v1/healthcheck', $this->iapImagesApiUrl), + ); + $request = ($this->requestSignerFactory)()->sign($request); - return $this->apiCall(new Request('GET', $url)); + return $this->apiCall($request); } + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ private function checkApiEndpoint(): array { - $url = sprintf('%s/v1/healthcheck', $this->siriusApiUrl); + $request = $this->requestFactory->createRequest( + 'GET', + sprintf('%s/v1/healthcheck', $this->siriusApiUrl), + ); + $request = ($this->requestSignerFactory)()->sign($request); - return $this->apiCall(new Request('GET', $url)); + return $this->apiCall($request); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function checkCodesApiEndpoint(): array + { + $request = $this->requestFactory->createRequest( + 'GET', + sprintf('%s/v1/healthcheck', $this->codesApiUrl), + ); + $request = ($this->requestSignerFactory)(SignatureType::ActorCodes)->sign($request); + + return $this->apiCall($request); } private function checkDynamoEndpoint(): array @@ -86,24 +121,16 @@ private function checkDynamoEndpoint(): array return $data; } - private function checkCodesApiEndpoint(): array - { - $url = sprintf('%s/v1/healthcheck', $this->codesApiUrl); - - return $this->apiCall(new Request('GET', $url)); - } - /** - * @param RequestInterface $request + * @param RequestInterface $uri * @return array{healthy: bool} */ private function apiCall(RequestInterface $request): array { - $data = []; - $signedRequest = $this->awsSignature->sign($request); + $data = []; try { - $response = $this->httpClient->send($signedRequest); + $response = $this->httpClient->sendRequest($request); if ($response->getStatusCode() === StatusCodeInterface::STATUS_OK) { $data['healthy'] = true; diff --git a/service-api/app/test/AppTest/Handler/Factory/HealthcheckHandlerFactoryTest.php b/service-api/app/test/AppTest/Handler/Factory/HealthcheckHandlerFactoryTest.php index d7f628d24a..e3af39b4c2 100644 --- a/service-api/app/test/AppTest/Handler/Factory/HealthcheckHandlerFactoryTest.php +++ b/service-api/app/test/AppTest/Handler/Factory/HealthcheckHandlerFactoryTest.php @@ -4,15 +4,15 @@ namespace AppTest\Handler\Factory; -use App\DataAccess\ApiGateway\RequestSigner; +use App\DataAccess\ApiGateway\RequestSignerFactory; use App\DataAccess\Repository\ActorUsersInterface; -use App\DataAccess\Repository\LpasInterface; use App\Handler\Factory\HealthcheckHandlerFactory; use App\Handler\HealthcheckHandler; -use GuzzleHttp\Client as HttpClient; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Container\ContainerInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; class HealthcheckHandlerFactoryTest extends TestCase { @@ -22,11 +22,11 @@ public function testItCreatesAHealthcheckHandler(): void { $factory = new HealthcheckHandlerFactory(); - $actorUsers = $this->prophesize(ActorUsersInterface::class); - $lpaInterface = $this->prophesize(LpasInterface::class); - $container = $this->prophesize(ContainerInterface::class); - $httpClientProphecy = $this->prophesize(HttpClient::class); - $requestSignerProphecy = $this->prophesize(RequestSigner::class); + $container = $this->prophesize(ContainerInterface::class); + $actorUsers = $this->prophesize(ActorUsersInterface::class); + $clientProphecy = $this->prophesize(ClientInterface::class); + $requestFactoryProphecy = $this->prophesize(RequestFactoryInterface::class); + $requestSignerProphecy = $this->prophesize(RequestSignerFactory::class); $container->get('config')->willReturn( [ @@ -44,9 +44,9 @@ public function testItCreatesAHealthcheckHandler(): void ); $container->get(ActorUsersInterface::class)->willReturn($actorUsers->reveal()); - $container->get(LpasInterface::class)->willReturn($lpaInterface->reveal()); - $container->get(HttpClient::class)->willReturn($httpClientProphecy->reveal()); - $container->get(RequestSigner::class)->willReturn($requestSignerProphecy->reveal()); + $container->get(ClientInterface::class)->willReturn($clientProphecy->reveal()); + $container->get(RequestFactoryInterface::class)->willReturn($requestFactoryProphecy->reveal()); + $container->get(RequestSignerFactory::class)->willReturn($requestSignerProphecy->reveal()); $handler = $factory($container->reveal()); diff --git a/service-api/app/test/AppTest/Handler/HealthcheckHandlerTest.php b/service-api/app/test/AppTest/Handler/HealthcheckHandlerTest.php index 04b0b7fbd1..c304575290 100644 --- a/service-api/app/test/AppTest/Handler/HealthcheckHandlerTest.php +++ b/service-api/app/test/AppTest/Handler/HealthcheckHandlerTest.php @@ -5,15 +5,17 @@ namespace AppTest\Handler; use App\DataAccess\ApiGateway\RequestSigner; +use App\DataAccess\ApiGateway\RequestSignerFactory; use App\DataAccess\Repository\ActorUsersInterface; use App\Handler\HealthcheckHandler; use Fig\Http\Message\StatusCodeInterface; -use GuzzleHttp\Client as HttpClient; use Laminas\Diactoros\Response\JsonResponse; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -22,9 +24,10 @@ class HealthcheckHandlerTest extends TestCase { use ProphecyTrait; - private ObjectProphecy $actorUsersProphecy; - private ObjectProphecy $httpClientProphecy; - private ObjectProphecy $requestSignerProphecy; + private ObjectProphecy|ActorUsersInterface $actorUsersProphecy; + private ObjectProphecy|ClientInterface $clientProphecy; + private ObjectProphecy|RequestFactoryInterface $requestFactoryProphecy; + private ObjectProphecy|RequestFactoryInterface $requestSignerFactoryProphecy; private string $version; private string $siriusApiUrl; @@ -33,13 +36,14 @@ class HealthcheckHandlerTest extends TestCase protected function setUp(): void { - $this->version = 'dev'; - $this->actorUsersProphecy = $this->prophesize(ActorUsersInterface::class); - $this->httpClientProphecy = $this->prophesize(HttpClient::class); - $this->requestSignerProphecy = $this->prophesize(RequestSigner::class); - $this->siriusApiUrl = 'localhost'; - $this->codesApiUrl = 'localhost'; - $this->iapImagesApiUrl = 'localhost'; + $this->actorUsersProphecy = $this->prophesize(ActorUsersInterface::class); + $this->clientProphecy = $this->prophesize(ClientInterface::class); + $this->requestFactoryProphecy = $this->prophesize(RequestFactoryInterface::class); + $this->requestSignerFactoryProphecy = $this->prophesize(RequestSignerFactory::class); + $this->version = 'dev'; + $this->siriusApiUrl = 'localhost'; + $this->codesApiUrl = 'localhost'; + $this->iapImagesApiUrl = 'localhost'; } public function testReturnsExpectedJsonResponse(): void @@ -50,28 +54,29 @@ public function testReturnsExpectedJsonResponse(): void $responseProphecy = $this->prophesize(ResponseInterface::class); $responseProphecy->getStatusCode()->willReturn(StatusCodeInterface::STATUS_OK); - $this->httpClientProphecy->send( - Argument::that(function (RequestInterface $request) { - $this->assertEquals('GET', $request->getMethod()); - $this->assertEquals('localhost/v1/healthcheck', $request->getUri()); - - return true; - }) - ) + $this->clientProphecy->sendRequest(Argument::any()) ->shouldBeCalledTimes(3) ->willReturn($responseProphecy->reveal()); - $this->requestSignerProphecy - ->sign(Argument::type(RequestInterface::class)) - ->will(function ($args) { - return $args[0]; - }); + $this->requestFactoryProphecy + ->createRequest('GET', Argument::any()) + ->willReturn($this->prophesize(RequestInterface::class)->reveal()); + + $requestSignerProphecy = $this->prophesize(RequestSigner::class); + $requestSignerProphecy + ->sign(Argument::any()) + ->willReturnArgument(0); + + $this->requestSignerFactoryProphecy + ->__invoke(Argument::any(), Argument::any()) + ->willReturn($requestSignerProphecy->reveal()); $healthcheck = new HealthcheckHandler( - $this->version, + $this->clientProphecy->reveal(), + $this->requestFactoryProphecy->reveal(), + $this->requestSignerFactoryProphecy->reveal(), $this->actorUsersProphecy->reveal(), - $this->httpClientProphecy->reveal(), - $this->requestSignerProphecy->reveal(), + $this->version, $this->siriusApiUrl, $this->codesApiUrl, $this->iapImagesApiUrl diff --git a/tests/features/check-service-status.feature b/tests/features/check-service-status.feature index 48c2718d44..2e65318237 100644 --- a/tests/features/check-service-status.feature +++ b/tests/features/check-service-status.feature @@ -8,7 +8,7 @@ Feature: Check status of the service Scenario: I want to check the service health Given I fetch the healthcheck endpoint Then I see JSON output - And it contains a "overall_healthy" key/value pair + And the service is declared healthy @smoke Scenario: I want to discover the service version diff --git a/tests/smoke/context/CommonContext.php b/tests/smoke/context/CommonContext.php index f6eb612b61..95e9c7f372 100644 --- a/tests/smoke/context/CommonContext.php +++ b/tests/smoke/context/CommonContext.php @@ -217,6 +217,17 @@ public function iSeeJsonOutput(): void $this->responseJson = $this->assertJsonResponse(); } + /** + * @Then the service is declared healthy + */ + public function theServiceIsDeclaredHealthy(): void + { + $this->responseJson = $this->assertJsonResponse(); + + $this->itContainsAKeyValuePair('overall_healthy'); + Assert::assertTrue($this->responseJson['overall_healthy']); + } + /** * @Then it contains a :key key\/value pair */