feat: require the CMS API key on /api/hotel and /api/product

This commit is contained in:
fromme committed 2026-10-08 11:37:57 +02:00
1 parent 7839b97e63
commit 32b70f9b70
3 files changed
+51 -21

No files matched your search

+37 -7
View File
@@ -1,12 +1,42 @@
# Backchannel endpoints MyE&P reads hotel and product data from. Same environment files and key
# as content-feed.http: pick "local" and put cmsApiKey in http-client.private.env.json.
### GET product data for myep ### GET product data for myep
GET https://ep-reisen.ddev.site/api/product GET {{host}}/api/product?product=3VIIN&hotel=VLTS
?product=SSTMR X-Api-Key: {{cmsApiKey}}
&hotel=SSTGR
&key=abcabc123 > {%
client.test("200 with key", function () {
client.assert(response.status === 200, "expected 200, got " + response.status);
});
%}
### GET hotel data for myep ### GET hotel data for myep
GET https://ep-reisen.ddev.site/api/hotel GET {{host}}/api/hotel?hotel=DPWGRP
?hotel=DPWGRP X-Api-Key: {{cmsApiKey}}
&key=abcabc123
> {%
client.test("200 with key", function () {
client.assert(response.status === 200, "expected 200, got " + response.status);
});
%}
### Transition form: key as query parameter, as MyE&P sends it today (still accepted)
GET {{host}}/api/hotel?hotel=DPWGRP&key={{cmsApiKey}}
> {%
client.test("200 with query key", function () {
client.assert(response.status === 200, "expected 200, got " + response.status);
});
%}
### Any other value is refused now - this used to pass
GET {{host}}/api/hotel?hotel=DPWGRP&key=AbcAbc123
> {%
client.test("403 with the old dummy key", function () {
client.assert(response.status === 403, "expected 403, got " + response.status);
});
%}
### ###
@@ -3,6 +3,7 @@
namespace EP\EpProducts\Middleware; namespace EP\EpProducts\Middleware;
use EP\EpProducts\ContentFeed\ContentFeedApi; use EP\EpProducts\ContentFeed\ContentFeedApi;
use EP\EpProducts\Http\ApiKeyAuthenticator;
use EP\EpProducts\Service\MyEpImageService; use EP\EpProducts\Service\MyEpImageService;
use EP\EpProducts\Utility\HotelIconUtility; use EP\EpProducts\Utility\HotelIconUtility;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@@ -47,13 +48,19 @@ class ApiMiddleware implements MiddlewareInterface
*/ */
private $contentFeedApi; private $contentFeedApi;
/**
* @var ApiKeyAuthenticator
*/
private $authenticator;
public function __construct( public function __construct(
MyEpImageService $hotelImageService, MyEpImageService $hotelImageService,
MyEpImageService $productImageService, MyEpImageService $productImageService,
MyEpImageService $regionImageService, MyEpImageService $regionImageService,
FileRepository $fileRepository, FileRepository $fileRepository,
ImageService $imageService, ImageService $imageService,
ContentFeedApi $contentFeedApi ContentFeedApi $contentFeedApi,
ApiKeyAuthenticator $authenticator
) { ) {
$this->hotelImageService = $hotelImageService; $this->hotelImageService = $hotelImageService;
$this->productImageService = $productImageService; $this->productImageService = $productImageService;
@@ -61,6 +68,7 @@ class ApiMiddleware implements MiddlewareInterface
$this->fileRepository = $fileRepository; $this->fileRepository = $fileRepository;
$this->imageService = $imageService; $this->imageService = $imageService;
$this->contentFeedApi = $contentFeedApi; $this->contentFeedApi = $contentFeedApi;
$this->authenticator = $authenticator;
} }
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
@@ -84,12 +92,8 @@ class ApiMiddleware implements MiddlewareInterface
private function handleHotelRequest(ServerRequestInterface $request): ResponseInterface private function handleHotelRequest(ServerRequestInterface $request): ResponseInterface
{ {
// This is currently fake, we only check for existence. Since we are communicating if (!$this->authenticator->isAuthorized($request)) {
// on the backchannel only it's not much of a problem. return new JsonResponse(['message' => 'Invalid or missing api key'], 403);
$apiKey = $request->getQueryParams()['key'] ?? null;
if (null === $apiKey) {
return new JsonResponse(['message' => 'No api key provided'], 403);
} }
$hotelCode = $request->getQueryParams()['hotel'] ?? null; $hotelCode = $request->getQueryParams()['hotel'] ?? null;
@@ -109,12 +113,8 @@ class ApiMiddleware implements MiddlewareInterface
private function handleProductRequest(ServerRequestInterface $request): ResponseInterface private function handleProductRequest(ServerRequestInterface $request): ResponseInterface
{ {
// This is currently fake, we only check for existence. Since we are communicating if (!$this->authenticator->isAuthorized($request)) {
// on the backchannel only it's not much of a problem. return new JsonResponse(['message' => 'Invalid or missing api key'], 403);
$apiKey = $request->getQueryParams()['key'] ?? null;
if (null === $apiKey) {
return new JsonResponse(['message' => 'No api key provided'], 403);
} }
$productCode = $request->getQueryParams()['product'] ?? null; $productCode = $request->getQueryParams()['product'] ?? null;
@@ -76,7 +76,7 @@ productImportSftpHostKey =
# cat=SftpImport; type=int+; label=SFTP connection timeout in seconds # cat=SftpImport; type=int+; label=SFTP connection timeout in seconds
productImportSftpTimeout = 15 productImportSftpTimeout = 15
# cat=ContentFeed; type=string; label=API key for the CMS backchannel endpoints (chatbot content feed, CMS index); sent as X-Api-Key. Empty denies every request. # cat=ContentFeed; type=string; label=API key for the CMS backchannel endpoints used by MyE&P (/api/hotel, /api/product, /api/content/*); sent as X-Api-Key or ?key=. Empty denies every request.
cmsApiKey = cmsApiKey =
# cat=ContentFeed; type=boolean; label=Serve the chatbot content feed (off: all feed endpoints answer 503) # cat=ContentFeed; type=boolean; label=Serve the chatbot content feed (off: all feed endpoints answer 503)