From 459dfb36138a48ca1905f23e031dc358c034b018 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:17:02 +0200 Subject: [PATCH 1/6] 4680: Added resource endpoint for limiting access to instant book interactive slide --- CHANGELOG.md | 2 + src/InteractiveSlide/InstantBook.php | 65 ++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc5cbfef4..341657125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- [#244](https://github.com/os2display/display-api-service/pull/244) + - Added resource endpoint for limiting access to instant book interactive slide. - [#243](https://github.com/os2display/display-api-service/pull/243) - Changed resource name in calendar api feed type resource selector. - [#242](https://github.com/os2display/display-api-service/pull/242) diff --git a/src/InteractiveSlide/InstantBook.php b/src/InteractiveSlide/InstantBook.php index 0ec19002d..234dc8019 100644 --- a/src/InteractiveSlide/InstantBook.php +++ b/src/InteractiveSlide/InstantBook.php @@ -15,6 +15,7 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\InvalidArgumentException; use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; use Symfony\Component\Security\Core\User\UserInterface; @@ -36,6 +37,7 @@ class InstantBook implements InteractiveSlideInterface private const string SCOPE = 'https://graph.microsoft.com/.default'; private const string GRANT_TYPE = 'password'; private const string CACHE_PREFIX = 'MS-INSTANT-BOOK'; + private const string CACHE_ALLOWED_RESOURCES_PREFIX = 'INSTANT-BOOK-ALLOWED-RESOURCES-'; private const string CACHE_KEY_TOKEN_PREFIX = self::CACHE_PREFIX.'-TOKEN-'; private const string CACHE_KEY_OPTIONS_PREFIX = self::CACHE_PREFIX.'-OPTIONS-'; private const string CACHE_PREFIX_SPAM_PROTECT_PREFIX = self::CACHE_PREFIX.'-SPAM-PROTECT-'; @@ -75,6 +77,10 @@ public function getConfigOptions(): array 'required' => true, 'description' => 'The key in the KeyVault for the password of the user.', ], + 'resourceEndpoint' => [ + 'required' => false, + 'description' => 'The key in the KeyVault for the resources endpoint. This should supply a json list of resources that can be booked. The resources should have ResourceMail and allowInstantBooking ("True"/"False") properties set.', + ], ]; } @@ -83,7 +89,7 @@ public function performAction(UserInterface $user, Slide $slide, InteractionSlid return match ($interactionRequest->action) { self::ACTION_GET_QUICK_BOOK_OPTIONS => $this->getQuickBookOptions($slide, $interactionRequest), self::ACTION_QUICK_BOOK => $this->quickBook($slide, $interactionRequest), - default => throw new InteractiveSlideException('Action not allowed'), + default => throw new BadRequestHttpException('Action not allowed'), }; } @@ -98,7 +104,7 @@ private function authenticate(array $configuration): array $password = $this->keyValueService->getValue($configuration['password']); if (4 !== count(array_filter([$tenantId, $clientId, $username, $password]))) { - throw new \Exception('tenantId, clientId, username, password must all be set.'); + throw new BadRequestHttpException('tenantId, clientId, username, password must all be set.'); } $url = self::LOGIN_ENDPOINT.$tenantId.self::OAUTH_PATH; @@ -124,7 +130,7 @@ private function getToken(Tenant $tenant, InteractiveSlide $interactive): string $configuration = $interactive->getConfiguration(); if (null === $configuration) { - throw new \Exception('InteractiveNoConfiguration'); + throw new BadRequestHttpException('Interactive no configuration'); } return $this->interactiveSlideCache->get( @@ -164,6 +170,9 @@ function (CacheItemInterface $item) use ($slide, $resource, $interactionRequest) throw new \Exception('InteractiveNotFound'); } + // Optional limiting of available resources. + $this->checkPermission($interactive, $resource); + $feed = $slide->getFeed(); if (null === $feed) { @@ -273,17 +282,20 @@ function (CacheItemInterface $item) use ($now): \DateTime { $interactive = $this->interactiveService->getInteractiveSlide($tenant, $interactionRequest->implementationClass); if (null === $interactive) { - throw new \Exception('InteractiveNotFound'); + throw new BadRequestHttpException('Interactive not found'); } + // Optional limiting of available resources. + $this->checkPermission($interactive, $resource); + $feed = $slide->getFeed(); if (null === $feed) { - throw new \Exception('Slide.feed not set.'); + throw new BadRequestHttpException('Slide.feed not set.'); } if (!in_array($resource, $feed->getConfiguration()['resources'] ?? [])) { - throw new \Exception('Resource not in feed resources'); + throw new BadRequestHttpException('Resource not in feed resources'); } $token = $this->getToken($tenant, $interactive); @@ -291,7 +303,7 @@ function (CacheItemInterface $item) use ($now): \DateTime { $configuration = $interactive->getConfiguration(); if (null === $configuration) { - throw new \Exception('InteractiveNoConfiguration'); + throw new BadRequestHttpException('InteractiveNoConfiguration'); } $username = $this->keyValueService->getValue($configuration['username']); @@ -411,13 +423,13 @@ private function getValueFromInterval(string $key, InteractionSlideRequest $inte $interval = $interactionRequest->data['interval'] ?? null; if (null === $interval) { - throw new \Exception('interval not set.'); + throw new BadRequestHttpException('interval not set.'); } $value = $interval[$key] ?? null; if (null === $value) { - throw new \Exception("interval.'.$key.' not set."); + throw new BadRequestHttpException("interval.'.$key.' not set."); } return $value; @@ -431,4 +443,39 @@ private function getHeaders(string $token): array 'Accept' => 'application/json', ]; } + + private function checkPermission(InteractiveSlide $interactive, string $resource): void + { + // Optional limiting of available resources. + if (!empty($interactive->getConfiguration()['resourceEndpoint'])) { + $allowedResources = $this->getAllowedResources($interactive); + + if (!in_array($resource, $allowedResources)) { + throw new \Exception('Not allowed'); + } + } + } + + private function getAllowedResources(InteractiveSlide $interactive): array + { + return $this->interactiveSlideCache->get(self::CACHE_ALLOWED_RESOURCES_PREFIX . $interactive->getId(), function (CacheItemInterface $item) use ($interactive) { + $item->expiresAfter(60 * 60); + + $configuration = $interactive->getConfiguration(); + $resourceEndpoint = $this->keyValueService->getValue($configuration['resourceEndpoint']); + + $response = $this->client->request('GET', $resourceEndpoint); + $content = $response->toArray(); + + $allowedResources = []; + + foreach ($content as $resource) { + if ($resource["allowInstantBooking"] === 'True') { + $allowedResources[] = $resource["ResourceMail"]; + } + } + + return $allowedResources; + }); + } } From a0ecaab585b6265fa3f9dc842bd83c3881d3a9f9 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:41:03 +0200 Subject: [PATCH 2/6] 4680: Applied coding standards --- CHANGELOG.md | 2 +- src/InteractiveSlide/InstantBook.php | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 341657125..8eca366ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -- [#244](https://github.com/os2display/display-api-service/pull/244) +- [#245](https://github.com/os2display/display-api-service/pull/245) - Added resource endpoint for limiting access to instant book interactive slide. - [#243](https://github.com/os2display/display-api-service/pull/243) - Changed resource name in calendar api feed type resource selector. diff --git a/src/InteractiveSlide/InstantBook.php b/src/InteractiveSlide/InstantBook.php index 234dc8019..c4b58ad18 100644 --- a/src/InteractiveSlide/InstantBook.php +++ b/src/InteractiveSlide/InstantBook.php @@ -9,7 +9,6 @@ use App\Entity\Tenant\InteractiveSlide; use App\Entity\Tenant\Slide; use App\Entity\User; -use App\Exceptions\InteractiveSlideException; use App\Service\InteractiveSlideService; use App\Service\KeyVaultService; use Psr\Cache\CacheItemInterface; @@ -256,7 +255,7 @@ private function createEntry(string $resource, array $schedules, string $startFo */ private function quickBook(Slide $slide, InteractionSlideRequest $interactionRequest): array { - $resource = $this->getValueFromInterval('resource', $interactionRequest); + $resource = (string) $this->getValueFromInterval('resource', $interactionRequest); $durationMinutes = $this->getValueFromInterval('durationMinutes', $interactionRequest); $now = new \DateTime(); @@ -446,8 +445,9 @@ private function getHeaders(string $token): array private function checkPermission(InteractiveSlide $interactive, string $resource): void { + $configuration = $interactive->getConfiguration(); // Optional limiting of available resources. - if (!empty($interactive->getConfiguration()['resourceEndpoint'])) { + if (null !== $configuration && !empty($configuration['resourceEndpoint'])) { $allowedResources = $this->getAllowedResources($interactive); if (!in_array($resource, $allowedResources)) { @@ -458,11 +458,22 @@ private function checkPermission(InteractiveSlide $interactive, string $resource private function getAllowedResources(InteractiveSlide $interactive): array { - return $this->interactiveSlideCache->get(self::CACHE_ALLOWED_RESOURCES_PREFIX . $interactive->getId(), function (CacheItemInterface $item) use ($interactive) { + return $this->interactiveSlideCache->get(self::CACHE_ALLOWED_RESOURCES_PREFIX.$interactive->getId(), function (CacheItemInterface $item) use ($interactive) { $item->expiresAfter(60 * 60); $configuration = $interactive->getConfiguration(); - $resourceEndpoint = $this->keyValueService->getValue($configuration['resourceEndpoint']); + + $key = $configuration['resourceEndpoint'] ?? null; + + if (null === $key) { + throw new \Exception('resourceEndpoint not set'); + } + + $resourceEndpoint = $this->keyValueService->getValue($key); + + if (null === $resourceEndpoint) { + throw new \Exception('resourceEndpoint value not set'); + } $response = $this->client->request('GET', $resourceEndpoint); $content = $response->toArray(); @@ -470,8 +481,8 @@ private function getAllowedResources(InteractiveSlide $interactive): array $allowedResources = []; foreach ($content as $resource) { - if ($resource["allowInstantBooking"] === 'True') { - $allowedResources[] = $resource["ResourceMail"]; + if ('True' === $resource['allowInstantBooking']) { + $allowedResources[] = $resource['ResourceMail']; } } From 1d92381d3095f9c874e79ca6352e4e126b5dd9a4 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:49:49 +0200 Subject: [PATCH 3/6] 4680: Added APP_KEY_VAULT_JSON to infrastructure --- .../display-api-service/etc/confd/templates/env.local.tmpl | 2 ++ .../display-api-service/etc/confd/templates/env.local.tmpl | 2 ++ public/fixture/resources.json | 6 ++++++ 3 files changed, 10 insertions(+) create mode 100644 public/fixture/resources.json diff --git a/infrastructure/itkdev/display-api-service/etc/confd/templates/env.local.tmpl b/infrastructure/itkdev/display-api-service/etc/confd/templates/env.local.tmpl index 7312e7cdc..4491a4df3 100644 --- a/infrastructure/itkdev/display-api-service/etc/confd/templates/env.local.tmpl +++ b/infrastructure/itkdev/display-api-service/etc/confd/templates/env.local.tmpl @@ -67,3 +67,5 @@ EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS={{ getenv "APP_EVENTDATABASE_API_V2_CA TRACK_SCREEN_INFO={{ getenv "APP_TRACK_SCREEN_INFO" "false" }} TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS={{ getenv "APP_TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS" "300" }} + +APP_KEY_VAULT_JSON={{ getenv "APP_KEY_VAULT_JSON" "{}" }} diff --git a/infrastructure/os2display/display-api-service/etc/confd/templates/env.local.tmpl b/infrastructure/os2display/display-api-service/etc/confd/templates/env.local.tmpl index 2b23b8e25..1fa420ab2 100644 --- a/infrastructure/os2display/display-api-service/etc/confd/templates/env.local.tmpl +++ b/infrastructure/os2display/display-api-service/etc/confd/templates/env.local.tmpl @@ -67,3 +67,5 @@ EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS={{ getenv "APP_EVENTDATABASE_API_V2_CA TRACK_SCREEN_INFO={{ getenv "APP_TRACK_SCREEN_INFO" "false" }} TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS={{ getenv "APP_TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS" "300" }} + +APP_KEY_VAULT_JSON={{ getenv "APP_KEY_VAULT_JSON" "{}" }} diff --git a/public/fixture/resources.json b/public/fixture/resources.json new file mode 100644 index 000000000..48f467875 --- /dev/null +++ b/public/fixture/resources.json @@ -0,0 +1,6 @@ +[ + { + "ResourceMail": "DOKK1-Lokale-Test1@aarhus.dk", + "allowInstantBooking": "True" + } +] From 858e6686bd5bfff357421ecae2161406f3a6ae14 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:17:38 +0200 Subject: [PATCH 4/6] Update src/InteractiveSlide/InstantBook.php Co-authored-by: Sine Jespersen --- src/InteractiveSlide/InstantBook.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InteractiveSlide/InstantBook.php b/src/InteractiveSlide/InstantBook.php index c4b58ad18..3a1046a4e 100644 --- a/src/InteractiveSlide/InstantBook.php +++ b/src/InteractiveSlide/InstantBook.php @@ -302,7 +302,7 @@ function (CacheItemInterface $item) use ($now): \DateTime { $configuration = $interactive->getConfiguration(); if (null === $configuration) { - throw new BadRequestHttpException('InteractiveNoConfiguration'); + throw new BadRequestHttpException('Interactive no configuration'); } $username = $this->keyValueService->getValue($configuration['username']); From 778410084acb3dff88eba98beec8c8686058762c Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:17:50 +0200 Subject: [PATCH 5/6] Update src/InteractiveSlide/InstantBook.php Co-authored-by: Sine Jespersen --- src/InteractiveSlide/InstantBook.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InteractiveSlide/InstantBook.php b/src/InteractiveSlide/InstantBook.php index 3a1046a4e..3e935d29b 100644 --- a/src/InteractiveSlide/InstantBook.php +++ b/src/InteractiveSlide/InstantBook.php @@ -290,7 +290,7 @@ function (CacheItemInterface $item) use ($now): \DateTime { $feed = $slide->getFeed(); if (null === $feed) { - throw new BadRequestHttpException('Slide.feed not set.'); + throw new BadRequestHttpException('Slide feed not set.'); } if (!in_array($resource, $feed->getConfiguration()['resources'] ?? [])) { From 0ec4ee424cbaf3ed2e5d6cc2140e14103470eed6 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:27:57 +0200 Subject: [PATCH 6/6] 4680: Fixed documentation and exception messages --- public/fixture/resources.json | 6 ------ src/InteractiveSlide/InstantBook.php | 14 ++++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 public/fixture/resources.json diff --git a/public/fixture/resources.json b/public/fixture/resources.json deleted file mode 100644 index 48f467875..000000000 --- a/public/fixture/resources.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "ResourceMail": "DOKK1-Lokale-Test1@aarhus.dk", - "allowInstantBooking": "True" - } -] diff --git a/src/InteractiveSlide/InstantBook.php b/src/InteractiveSlide/InstantBook.php index 3e935d29b..62f2f2855 100644 --- a/src/InteractiveSlide/InstantBook.php +++ b/src/InteractiveSlide/InstantBook.php @@ -59,18 +59,20 @@ public function __construct( public function getConfigOptions(): array { + // All secrets are retrieved from the KeyVault. Therefore, the input for the different configurations are the + // keys into the KeyVault where the values can be retrieved. return [ 'tenantId' => [ 'required' => true, - 'description' => 'The key in the KeyVault for the tenant id of the App', + 'description' => 'The key in the KeyVault for the tenant id of the Microsoft Graph App', ], 'clientId' => [ 'required' => true, - 'description' => 'The key in the KeyVault for the client id of the App', + 'description' => 'The key in the KeyVault for the client id of the Microsoft Graph App', ], 'username' => [ 'required' => true, - 'description' => 'The key in the KeyVault for the Microsoft Graph username that should perform the action.', + 'description' => 'The key in the KeyVault for the username that should perform the action.', ], 'password' => [ 'required' => true, @@ -129,7 +131,7 @@ private function getToken(Tenant $tenant, InteractiveSlide $interactive): string $configuration = $interactive->getConfiguration(); if (null === $configuration) { - throw new BadRequestHttpException('Interactive no configuration'); + throw new BadRequestHttpException('InteractiveSlide has no configuration'); } return $this->interactiveSlideCache->get( @@ -166,7 +168,7 @@ function (CacheItemInterface $item) use ($slide, $resource, $interactionRequest) $interactive = $this->interactiveService->getInteractiveSlide($tenant, $interactionRequest->implementationClass); if (null === $interactive) { - throw new \Exception('InteractiveNotFound'); + throw new \Exception('InteractiveSlide not found'); } // Optional limiting of available resources. @@ -175,7 +177,7 @@ function (CacheItemInterface $item) use ($slide, $resource, $interactionRequest) $feed = $slide->getFeed(); if (null === $feed) { - throw new \Exception('Slide.feed not set.'); + throw new \Exception('Slide feed not set.'); } if (!in_array($resource, $feed->getConfiguration()['resources'] ?? [])) {