Skip to content

Added resource endpoint for limiting access to instant book interactive slide #245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

- [#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.
- [#242](https://github.com/os2display/display-api-service/pull/242)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" "{}" }}
Original file line number Diff line number Diff line change
Expand Up @@ -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" "{}" }}
92 changes: 76 additions & 16 deletions src/InteractiveSlide/InstantBook.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
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;
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;
Expand All @@ -36,6 +36,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-';
Expand All @@ -58,23 +59,29 @@ 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,
'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.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this: 'The key in the KeyVault for the resources endpoint.'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add a comment to the configuration section.

],
];
}

Expand All @@ -83,7 +90,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'),
};
}

Expand All @@ -98,7 +105,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;
Expand All @@ -124,7 +131,7 @@ private function getToken(Tenant $tenant, InteractiveSlide $interactive): string
$configuration = $interactive->getConfiguration();

if (null === $configuration) {
throw new \Exception('InteractiveNoConfiguration');
throw new BadRequestHttpException('InteractiveSlide has no configuration');
}

return $this->interactiveSlideCache->get(
Expand Down Expand Up @@ -161,13 +168,16 @@ 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.
$this->checkPermission($interactive, $resource);

$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'] ?? [])) {
Expand Down Expand Up @@ -247,7 +257,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();
Expand All @@ -273,25 +283,28 @@ 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);

$configuration = $interactive->getConfiguration();

if (null === $configuration) {
throw new \Exception('InteractiveNoConfiguration');
throw new BadRequestHttpException('Interactive no configuration');
}

$username = $this->keyValueService->getValue($configuration['username']);
Expand Down Expand Up @@ -411,13 +424,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;
Expand All @@ -431,4 +444,51 @@ private function getHeaders(string $token): array
'Accept' => 'application/json',
];
}

private function checkPermission(InteractiveSlide $interactive, string $resource): void
{
$configuration = $interactive->getConfiguration();
// Optional limiting of available resources.
if (null !== $configuration && !empty($configuration['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();

$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();

$allowedResources = [];

foreach ($content as $resource) {
if ('True' === $resource['allowInstantBooking']) {
$allowedResources[] = $resource['ResourceMail'];
}
}

return $allowedResources;
});
}
}