From 28346c3c84816341eb6a7d74984595b2dcb1cdb4 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:24:00 +0100 Subject: [PATCH 01/16] 6897: Added authorization voter for screen --- .env | 13 ++++ config/api_platform/screen.yaml | 9 +-- config/services.yaml | 4 ++ src/Security/Voter/AuthorizationVoter.php | 73 +++++++++++++++++++++++ 4 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/Security/Voter/AuthorizationVoter.php diff --git a/.env b/.env index 414a776c5..82d2d2273 100644 --- a/.env +++ b/.env @@ -129,3 +129,16 @@ CLIENT_PULL_STRATEGY_INTERVAL=90000 CLIENT_COLOR_SCHEME='{"type":"library","lat":56.0,"lng":10.0}' CLIENT_DEBUG=false ###< Client configuration ### + +AUTHORIZATION_OVERRIDE='{ + "Screen": { + "EDIT": "ROLE_ADMIN", + "EDIT_OWN": "ROLE_EDITOR", + "VIEW": "ROLE_EDITOR", + "VIEW_OWN": "ROLE_EDITOR", + "CREATE": "ROLE_ADMIN", + "CREATE_OWN": "ROLE_EDITOR", + "DELETE": "ROLE_ADMIN", + "DELETE_OWN": "ROLE_EDITOR" + } +}' diff --git a/config/api_platform/screen.yaml b/config/api_platform/screen.yaml index 3ff6b4f83..1a22301f5 100644 --- a/config/api_platform/screen.yaml +++ b/config/api_platform/screen.yaml @@ -1,7 +1,6 @@ --- resources: App\Entity\Tenant\Screen: - security: 'is_granted("ROLE_SCREEN") or is_granted("ROLE_ADMIN")' input: App\Dto\ScreenInput output: App\Dto\Screen provider: App\State\ScreenProvider @@ -9,6 +8,7 @@ resources: operations: ApiPlatform\Metadata\Get: &get + security: 'is_granted("VIEW", object)' normalizationContext: jsonld_embed_context: true openapiContext: @@ -34,7 +34,7 @@ resources: headers: {} ApiPlatform\Metadata\Put: - security: 'is_granted("ROLE_ADMIN")' + security: 'is_granted("EDIT", object)' openapiContext: description: Update a Screen resource. summary: Update a Screen resource. @@ -51,7 +51,7 @@ resources: required: true ApiPlatform\Metadata\Delete: - security: 'is_granted("ROLE_ADMIN")' + security: 'is_granted("DELETE", object)' openapiContext: description: Delete a Screen resource. summary: Delete a Screen resource. @@ -68,6 +68,7 @@ resources: required: true ApiPlatform\Metadata\GetCollection: + security: 'is_granted("VIEW", object)' filters: - 'App\Filter\MultipleSearchFilter' - 'screen.screen_user_exists_filter' @@ -105,7 +106,7 @@ resources: headers: {} ApiPlatform\Metadata\Post: - security: 'is_granted("ROLE_ADMIN")' + security: 'is_granted("CREATE", object)' normalizationContext: jsonld_embed_context: true openapiContext: diff --git a/config/services.yaml b/config/services.yaml index b62f35f7f..75ed14fba 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -53,6 +53,10 @@ services: Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface: '@Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationFailureHandler' Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface: '@Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler' + App\Security\Voter\AuthorizationVoter: + arguments: + $authorizationOverride: '%env(json:AUTHORIZATION_OVERRIDE)%' + App\EventListener\RelationsChecksumListener: arguments: $enabled: '%env(bool:RELATIONS_CHECKSUM_ENABLED)%' diff --git a/src/Security/Voter/AuthorizationVoter.php b/src/Security/Voter/AuthorizationVoter.php new file mode 100644 index 000000000..974dca07e --- /dev/null +++ b/src/Security/Voter/AuthorizationVoter.php @@ -0,0 +1,73 @@ + [ + self::EDIT => ['ROLE_ADMIN'], + self::EDIT . self::OWN => ['ROLE_ADMIN'], + self::VIEW => ['ROLE_ADMIN'], + self::VIEW . self::OWN => ['ROLE_ADMIN'], + self::CREATE => ['ROLE_ADMIN'], + self::CREATE . self::OWN => ['ROLE_ADMIN'], + self::DELETE => ['ROLE_ADMIN'], + self::DELETE . self::OWN => ['ROLE_ADMIN'], + ] + ]; + + private array $authorization; + + public function __construct(private readonly array $authorizationOverride, private readonly RoleHierarchyInterface $roleHierarchy) + { + $this->authorization = array_replace($this->authorizationDefault, $authorizationOverride); + } + + protected function supports(string $attribute, mixed $subject): bool + { + // https://symfony.com/doc/current/security/voters.html + + return in_array($attribute, [self::EDIT, self::VIEW, self::CREATE, self::DELETE]) + && $subject instanceof Screen; + } + + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + { + $user = $token->getUser(); + + // if the user is anonymous, do not grant access + if (!$user instanceof UserInterface) { + return false; + } + + $userRoles = $user->getRoles(); + + $class = str_replace("App\\Dto\\", "", $subject::class); + + $createdBy = $subject->createdBy; + + $userIdentifier = $user->getUserIdentifier(); + + // Check authorization array for demands for $class, $attribute and $userIdentifier + // Permissions are different if the user is the creator of the object. + $actionKey = $attribute . ($userIdentifier === $createdBy ? self::OWN : ''); + $requiredRole = $this->authorization[$class][$actionKey]; + + $reachableRoles = $this->roleHierarchy->getReachableRoleNames($userRoles); + + return in_array($requiredRole, $reachableRoles); + } +} From f48a5f5ea5ddfc9bf47fed986faa229c490befe0 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:21:50 +0100 Subject: [PATCH 02/16] 6897: Added helper class --- .env | 26 ++++--- src/Security/Voter/AuthorizationVoter.php | 42 +++++----- .../Voter/AuthorizationVoterHelper.php | 77 +++++++++++++++++++ 3 files changed, 113 insertions(+), 32 deletions(-) create mode 100644 src/Security/Voter/AuthorizationVoterHelper.php diff --git a/.env b/.env index 82d2d2273..a611e705a 100644 --- a/.env +++ b/.env @@ -130,15 +130,17 @@ CLIENT_COLOR_SCHEME='{"type":"library","lat":56.0,"lng":10.0}' CLIENT_DEBUG=false ###< Client configuration ### -AUTHORIZATION_OVERRIDE='{ - "Screen": { - "EDIT": "ROLE_ADMIN", - "EDIT_OWN": "ROLE_EDITOR", - "VIEW": "ROLE_EDITOR", - "VIEW_OWN": "ROLE_EDITOR", - "CREATE": "ROLE_ADMIN", - "CREATE_OWN": "ROLE_EDITOR", - "DELETE": "ROLE_ADMIN", - "DELETE_OWN": "ROLE_EDITOR" - } -}' +AUTHORIZATION_OVERRIDE='{}' +# Example value: +#AUTHORIZATION_OVERRIDE='{ +# "Screen": { +# "EDIT": ["ROLE_ADMIN"], +# "EDIT_OWN": ["ROLE_EDITOR"], +# "VIEW": ["ROLE_EDITOR"], +# "VIEW_OWN": ["ROLE_EDITOR"], +# "CREATE": ["ROLE_ADMIN"], +# "CREATE_OWN": ["ROLE_EDITOR"], +# "DELETE": ["ROLE_ADMIN"], +# "DELETE_OWN": ["ROLE_EDITOR"] +# } +#}' diff --git a/src/Security/Voter/AuthorizationVoter.php b/src/Security/Voter/AuthorizationVoter.php index 974dca07e..276401fbf 100644 --- a/src/Security/Voter/AuthorizationVoter.php +++ b/src/Security/Voter/AuthorizationVoter.php @@ -14,34 +14,24 @@ final class AuthorizationVoter extends Voter { public const CREATE = 'CREATE'; public const DELETE = 'DELETE'; public const OWN = '_OWN'; - public const SCREEN = 'Screen'; - - private array $authorizationDefault = [ - self::SCREEN => [ - self::EDIT => ['ROLE_ADMIN'], - self::EDIT . self::OWN => ['ROLE_ADMIN'], - self::VIEW => ['ROLE_ADMIN'], - self::VIEW . self::OWN => ['ROLE_ADMIN'], - self::CREATE => ['ROLE_ADMIN'], - self::CREATE . self::OWN => ['ROLE_ADMIN'], - self::DELETE => ['ROLE_ADMIN'], - self::DELETE . self::OWN => ['ROLE_ADMIN'], - ] - ]; private array $authorization; public function __construct(private readonly array $authorizationOverride, private readonly RoleHierarchyInterface $roleHierarchy) { - $this->authorization = array_replace($this->authorizationDefault, $authorizationOverride); + $authorizationDefaults = AuthorizationVoterHelper::getAuthorizationDefaults(); + $this->authorization = array_replace($authorizationDefaults, $authorizationOverride); } protected function supports(string $attribute, mixed $subject): bool { // https://symfony.com/doc/current/security/voters.html + $dto = str_contains($subject::class, "App\\Dto\\"); + $entity = str_contains($subject::class, "App\\Entity\\Tenant\\"); + return in_array($attribute, [self::EDIT, self::VIEW, self::CREATE, self::DELETE]) - && $subject instanceof Screen; + && $dto || $entity; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool @@ -55,19 +45,31 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter $userRoles = $user->getRoles(); - $class = str_replace("App\\Dto\\", "", $subject::class); + $dto = str_contains($subject::class, "App\\Dto\\"); + $entity = str_contains($subject::class, "App\\Entity\\"); + + $class = ''; + $createdBy = null; - $createdBy = $subject->createdBy; + if ($dto) { + $class = str_replace("App\\Dto\\", "", $subject::class); + $createdBy = $subject->createdBy; + } else if ($entity) { + $class = str_replace("App\\Entity\\Tenant\\", "", $subject::class); + $createdBy = $subject->getCreatedBy(); + } $userIdentifier = $user->getUserIdentifier(); // Check authorization array for demands for $class, $attribute and $userIdentifier // Permissions are different if the user is the creator of the object. $actionKey = $attribute . ($userIdentifier === $createdBy ? self::OWN : ''); - $requiredRole = $this->authorization[$class][$actionKey]; + $requiredRoles = $this->authorization[$class][$actionKey]; $reachableRoles = $this->roleHierarchy->getReachableRoleNames($userRoles); - return in_array($requiredRole, $reachableRoles); + $intersect = array_intersect($requiredRoles, $reachableRoles); + + return !empty($intersect); } } diff --git a/src/Security/Voter/AuthorizationVoterHelper.php b/src/Security/Voter/AuthorizationVoterHelper.php new file mode 100644 index 000000000..15e79f0f2 --- /dev/null +++ b/src/Security/Voter/AuthorizationVoterHelper.php @@ -0,0 +1,77 @@ + ['ROLE_EDITOR'], + AuthorizationVoter::EDIT . AuthorizationVoter::OWN => ['ROLE_EDITOR'], + AuthorizationVoter::VIEW => ['ROLE_EDITOR'], + AuthorizationVoter::VIEW . AuthorizationVoter::OWN => ['ROLE_EDITOR'], + AuthorizationVoter::CREATE => ['ROLE_EDITOR'], + AuthorizationVoter::CREATE . AuthorizationVoter::OWN => ['ROLE_EDITOR'], + AuthorizationVoter::DELETE => ['ROLE_EDITOR'], + AuthorizationVoter::DELETE . AuthorizationVoter::OWN => ['ROLE_EDITOR'], + ]; + } + + foreach (self::adminClasses as $class) { + $defaults[$class] = [ + AuthorizationVoter::EDIT => ['ROLE_ADMIN'], + AuthorizationVoter::EDIT . AuthorizationVoter::OWN => ['ROLE_ADMIN'], + AuthorizationVoter::VIEW => ['ROLE_ADMIN'], + AuthorizationVoter::VIEW . AuthorizationVoter::OWN => ['ROLE_ADMIN'], + AuthorizationVoter::CREATE => ['ROLE_ADMIN'], + AuthorizationVoter::CREATE . AuthorizationVoter::OWN => ['ROLE_ADMIN'], + AuthorizationVoter::DELETE => ['ROLE_ADMIN'], + AuthorizationVoter::DELETE . AuthorizationVoter::OWN => ['ROLE_ADMIN'], + ]; + } + + return $defaults; + } +} From 4a49210c4579c58c8699cde350c57ba135055ca1 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:09:04 +0100 Subject: [PATCH 03/16] 6897: Started work on admin hasPermission function --- .../admin/components/screen/screen-form.jsx | 66 ++++++++++++++----- .../Admin/AdminConfigController.php | 3 + src/Security/Voter/AuthorizationVoter.php | 9 ++- .../Voter/AuthorizationVoterHelper.php | 4 +- 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/assets/admin/components/screen/screen-form.jsx b/assets/admin/components/screen/screen-form.jsx index 8f64516c0..f95bc0687 100644 --- a/assets/admin/components/screen/screen-form.jsx +++ b/assets/admin/components/screen/screen-form.jsx @@ -83,6 +83,32 @@ function ScreenForm({ previewOrientationOptions[0].value, ); + const hasPermission = (entity, action) => { + console.log("hasPermission", entity, action); + + if (config?.authorization) { + const currentTenant = JSON.parse(localStorage.getItem("admin-selected-tenant")); + const userRoles = currentTenant?.roles; + + const authorization = config.authorization; + + console.log("authorization", authorization); + + const requiredRoles = authorization[entity]?.[action]; + + console.log("requiredRoles", requiredRoles); + + const intersect = requiredRoles.filter(value => userRoles.includes(value)); + + console.log("intersect", intersect); + + if (intersect.length > 0) { + return true; + } + } + return false; + } + /** * Check if published is set * @@ -382,24 +408,28 @@ function ScreenForm({ > {t("cancel-button")} - - + {hasPermission("Screen", "EDIT") && ( + <> + + + + )} {config?.enhancedPreview && (