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 && (