diff --git a/Classes/Domain/Dto/MatcherConfiguration.php b/Classes/Domain/Dto/MatcherConfiguration.php index b85b294..311c155 100644 --- a/Classes/Domain/Dto/MatcherConfiguration.php +++ b/Classes/Domain/Dto/MatcherConfiguration.php @@ -86,6 +86,20 @@ public function toPolicyMatcherString(): string return implode(' && ', $matcherParts); } + public function toPolicyMatcherStringForAncestorNodesAndChildren(): string + { + $matcherParts = []; + + $matcherParts[] = self::generatePolicyMatcherStringForSelectedWorkspaces($this->selectedWorkspaces); + $matcherParts[] = self::generatePolicyMatcherStringForSelectedDimensions($this->selectedDimensionPresets); + $nodeMatcherParts = []; + $nodeMatcherParts[] = self::generatePolicyMatcherStringForSelectedNodes($this->selectedNodes); + $nodeMatcherParts[] = self::generatePolicyMatcherStringForSelectedNodesAncestors($this->selectedNodes); + $matcherParts[] = '(' . implode(' || ', $nodeMatcherParts) . ')'; + + return implode(' && ', $matcherParts); + } + private static function generatePolicyMatcherStringForSelectedWorkspaces(array $selectedWorkspaces): string { if (empty($selectedWorkspaces)) { @@ -137,6 +151,22 @@ private static function generatePolicyMatcherStringForSelectedNodes(array $selec return '(' . implode(' || ', $matcherParts) . ')'; } + private static function generatePolicyMatcherStringForSelectedNodesAncestors(array $selectedNodesConfig) + { + $matcherParts = []; + + foreach ($selectedNodesConfig as $nodeConfig) { + /* @var $nodeConfig \Sandstorm\NeosAcl\Domain\Dto\MatcherConfigurationSelectedNode */ + $matcherParts[] = $nodeConfig->toAncestorPolicyMatcherString(); + } + + if (empty($matcherParts)) { + return 'true'; + } + + return '(' . implode(' || ', $matcherParts) . ')'; + } + public function renderExplanationParts(): array { $explanation = []; diff --git a/Classes/Domain/Dto/MatcherConfigurationSelectedNode.php b/Classes/Domain/Dto/MatcherConfigurationSelectedNode.php index 2ef62bd..1574fbd 100644 --- a/Classes/Domain/Dto/MatcherConfigurationSelectedNode.php +++ b/Classes/Domain/Dto/MatcherConfigurationSelectedNode.php @@ -69,6 +69,19 @@ public function toPolicyMatcherString(): string return sprintf('(%s && %s)', $nodeIdentifierMatcher, $nodeTypeMatcher); } + public function toAncestorPolicyMatcherString(): string + { + $nodeIdentifierMatcher = sprintf('isAncestorNodeOf("%s")', $this->nodeIdentifier); + + if (empty($this->whitelistedNodeTypes)) { + return $nodeIdentifierMatcher; + } + + $nodeTypeMatcher = self::generatePolicyMatcherStringForNodeTypes($this->whitelistedNodeTypes); + + return sprintf('(%s && %s)', $nodeIdentifierMatcher, $nodeTypeMatcher); + } + private static function generatePolicyMatcherStringForNodeTypes(array $nodeTypes) { $matcherParts = []; diff --git a/Classes/DynamicRoleEnforcement/DynamicPolicyRegistry.php b/Classes/DynamicRoleEnforcement/DynamicPolicyRegistry.php index 3beb8f6..fa53baa 100644 --- a/Classes/DynamicRoleEnforcement/DynamicPolicyRegistry.php +++ b/Classes/DynamicRoleEnforcement/DynamicPolicyRegistry.php @@ -12,6 +12,7 @@ use Neos\Flow\Security\Authorization\Privilege\PrivilegeInterface; use Neos\Flow\Security\Authorization\Privilege\PrivilegeTarget; use Neos\Flow\Security\Policy\PolicyService; +use Neos\Neos\Security\Authorization\Privilege\ReadNodeTreePrivilege; use Neos\Utility\Arrays; /** @@ -34,7 +35,8 @@ final class DynamicPolicyRegistry const ALLOWED_PRIVILEGE_TARGET_TYPES = [ 'Neos\ContentRepository\Security\Authorization\Privilege\Node\EditNodePrivilege' => 'Sandstorm.NeosAcl:EditAllNodes', 'Neos\ContentRepository\Security\Authorization\Privilege\Node\CreateNodePrivilege' => 'Sandstorm.NeosAcl:CreateAllNodes', - 'Neos\ContentRepository\Security\Authorization\Privilege\Node\RemoveNodePrivilege' => 'Sandstorm.NeosAcl:RemoveAllNodes' + 'Neos\ContentRepository\Security\Authorization\Privilege\Node\RemoveNodePrivilege' => 'Sandstorm.NeosAcl:RemoveAllNodes', + //ReadNodeTreePrivilege::class => 'Sandstorm.NeosAcl:NodeTreeRestricted' ]; /** @@ -103,6 +105,9 @@ public function registerDynamicPolicyAndMergeThemWithOriginal(array $dynamicPoli private static function ensurePrivilegeTargetIsInDynamicWhitelist(string $privilegeTargetType) { + if ($privilegeTargetType === ReadNodeTreePrivilege::class) { + return true; + } if (!isset(self::ALLOWED_PRIVILEGE_TARGET_TYPES[$privilegeTargetType])) { throw new \RuntimeException('the privilege target type "' . $privilegeTargetType . '" is not allowed to be registered dynamically.'); } @@ -176,8 +181,15 @@ private function initializeDynamicPrivilegeMapping(): void $dynamicPrivilegeMapping = []; $dynamicPrivilegeToCatchAllMapping = []; foreach ($this->dynamicPrivilegeTargetsPerType as $privilegeTargetType => $dynamicPrivilegeTargets) { + if (!isset(self::ALLOWED_PRIVILEGE_TARGET_TYPES[$privilegeTargetType])) { + continue; + } $catchAllPrivilegeTargetForType = self::ALLOWED_PRIVILEGE_TARGET_TYPES[$privilegeTargetType]; - $matcherForCatchAllPrivilegeTarget = static::getMatcherForCatchAllPrivilegeTargets($this->objectManager)[$catchAllPrivilegeTargetForType]; + $x = static::getMatcherForCatchAllPrivilegeTargets($this->objectManager); + if (!isset($x[$catchAllPrivilegeTargetForType])) { + continue; // TODO WHY? + } + $matcherForCatchAllPrivilegeTarget = $x[$catchAllPrivilegeTargetForType]; $cacheIdentifierForCatchAllPrivilegeTarget = (new PrivilegeTarget($catchAllPrivilegeTargetForType, $privilegeTargetType, $matcherForCatchAllPrivilegeTarget, [])) ->createPrivilege(PrivilegeInterface::GRANT, []) @@ -212,7 +224,10 @@ public static function getMatcherForCatchAllPrivilegeTargets($objectManager): ar $configurationManager = $objectManager->get(ConfigurationManager::class); $policyConfiguration = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_POLICY); foreach (self::ALLOWED_PRIVILEGE_TARGET_TYPES as $catchAllPrivilegeTargetType => $catchAllPrivilegeTargetName) { - $catchAllPrivilegeTargetMatchers[$catchAllPrivilegeTargetName] = $policyConfiguration['privilegeTargets'][$catchAllPrivilegeTargetType][$catchAllPrivilegeTargetName]['matcher']; + if (!isset($policyConfiguration['privilegeTargets'][$catchAllPrivilegeTargetType][$catchAllPrivilegeTargetName]['matcher'])) { // TODO: WHY IF? + $catchAllPrivilegeTargetMatchers[$catchAllPrivilegeTargetName] = $policyConfiguration['privilegeTargets'][$catchAllPrivilegeTargetType][$catchAllPrivilegeTargetName]['matcher']; + } + } return $catchAllPrivilegeTargetMatchers; diff --git a/Classes/Service/ACLCheckerService.php b/Classes/Service/ACLCheckerService.php index 1cc24e3..c19d4eb 100644 --- a/Classes/Service/ACLCheckerService.php +++ b/Classes/Service/ACLCheckerService.php @@ -22,6 +22,7 @@ use Neos\ContentRepository\Security\Authorization\Privilege\Node\NodePrivilegeSubject; use Neos\ContentRepository\Security\Authorization\Privilege\Node\ReadNodePrivilege; use Neos\ContentRepository\Security\Authorization\Privilege\Node\RemoveNodePrivilege; +use Neos\Neos\Security\Authorization\Privilege\ReadNodeTreePrivilege; use Sandstorm\NeosAcl\Dto\ACLCheckerDto; class ACLCheckerService @@ -80,7 +81,8 @@ public function checkNodeForRoles(NodeInterface $node, array $roles) 'editNode' => $this->privilegeManager->isGrantedForRoles([$role], EditNodePrivilege::class, new NodePrivilegeSubject($node)), 'removeNode' => $this->privilegeManager->isGrantedForRoles([$role], RemoveNodePrivilege::class, new NodePrivilegeSubject($node)), 'createNodeOfType' => $this->privilegeManager->isGrantedForRoles([$role], CreateNodePrivilege::class, new CreateNodePrivilegeSubject($node)), - 'showInTree' => $this->privilegeManager->isGrantedForRoles([$role], NodeTreePrivilege::class, new NodePrivilegeSubject($node)) + 'showInTree' => $this->privilegeManager->isGrantedForRoles([$role], NodeTreePrivilege::class, new NodePrivilegeSubject($node)) || + $this->privilegeManager->isGrantedForRoles([$role], ReadNodeTreePrivilege::class, new NodePrivilegeSubject($node)) ]; } return $checkedNodes; diff --git a/Classes/Service/DynamicRoleGeneratorService.php b/Classes/Service/DynamicRoleGeneratorService.php index 1e87c73..4d56492 100644 --- a/Classes/Service/DynamicRoleGeneratorService.php +++ b/Classes/Service/DynamicRoleGeneratorService.php @@ -56,11 +56,22 @@ public function onConfigurationLoaded(&$configuration) $connection = $this->entityManager->getConnection(); $rows = $connection->executeQuery('SELECT name, abstract, parentrolenames, matcher, privilege FROM sandstorm_neosacl_domain_model_dynamicrole')->fetchAll(); foreach ($rows as $row) { - + $parentRoles = json_decode($row['parentrolenames'], true); $matcherConfig = json_decode($row['matcher'], true); - $matcher = MatcherConfiguration::fromJson($matcherConfig)->toPolicyMatcherString(); $privileges = []; + if (in_array('Sandstorm.NeosAcl:NodeTreeRestricted', $parentRoles) && ($row['privilege'] === DynamicRole::PRIVILEGE_VIEW || $row['privilege'] === DynamicRole::PRIVILEGE_VIEW_EDIT || $row['privilege'] === DynamicRole::PRIVILEGE_VIEW_EDIT_CREATE_DELETE)) { + $customConfiguration['privilegeTargets']['Neos\Neos\Security\Authorization\Privilege\ReadNodeTreePrivilege']['Dynamic:' . $row['name'] . '.ReadNodeTree'] = [ + 'matcher' => MatcherConfiguration::fromJson($matcherConfig)->toPolicyMatcherStringForAncestorNodesAndChildren(), + ]; + + $privileges[] = [ + 'privilegeTarget' => 'Dynamic:' . $row['name'] . '.ReadNodeTree', + 'permission' => 'GRANT' + ]; + } + + $matcher = MatcherConfiguration::fromJson($matcherConfig)->toPolicyMatcherString(); if ($row['privilege'] === DynamicRole::PRIVILEGE_VIEW_EDIT || $row['privilege'] === DynamicRole::PRIVILEGE_VIEW_EDIT_CREATE_DELETE) { $customConfiguration['privilegeTargets']['Neos\ContentRepository\Security\Authorization\Privilege\Node\EditNodePrivilege']['Dynamic:' . $row['name'] . '.EditNode'] = [ 'matcher' => $matcher @@ -92,7 +103,7 @@ public function onConfigurationLoaded(&$configuration) $customConfiguration['roles']['Dynamic:' . $row['name']] = [ 'abstract' => intval($row['abstract']) === 1, - 'parentRoles' => json_decode($row['parentrolenames'], true), + 'parentRoles' => $parentRoles, 'privileges' => $privileges ]; } diff --git a/Configuration/Policy.yaml b/Configuration/Policy.yaml index ea560cd..bfb7356 100644 --- a/Configuration/Policy.yaml +++ b/Configuration/Policy.yaml @@ -23,6 +23,12 @@ privilegeTargets: 'Sandstorm.NeosAcl:RemoveAllNodes': matcher: 'TRUE' + # TODO: BREAKING!!! + 'Neos\Neos\Security\Authorization\Privilege\ReadNodeTreePrivilege': + # this privilegeTarget is defined to switch to a "whitelist" approach + 'Sandstorm.NeosAcl:NodeTreeAllNodes': + matcher: 'TRUE' + roles: 'Neos.Neos:Administrator': privileges: @@ -42,6 +48,11 @@ roles: - privilegeTarget: 'Sandstorm.NeosAcl:RemoveAllNodes' permission: GRANT + # TODO: ENABLED + - + privilegeTarget: 'Sandstorm.NeosAcl:NodeTreeAllNodes' + permission: GRANT + 'Neos.Neos:Editor': # Admins and unrestricted editors can still do everything. privileges: @@ -54,3 +65,8 @@ roles: - privilegeTarget: 'Sandstorm.NeosAcl:RemoveAllNodes' permission: GRANT + # TODO: ENABLED + - + privilegeTarget: 'Sandstorm.NeosAcl:NodeTreeAllNodes' + permission: GRANT + 'Sandstorm.NeosAcl:NodeTreeRestricted': {} diff --git a/Documentation/2023_12_06_PageTreeRestrictions.md b/Documentation/2023_12_06_PageTreeRestrictions.md new file mode 100644 index 0000000..eebf120 --- /dev/null +++ b/Documentation/2023_12_06_PageTreeRestrictions.md @@ -0,0 +1,51 @@ +## Ziel + +- Seitenbaum - Einschränkung... + + +## Mögliche Solutions + +- irgendwie über Policy.yaml gehen um Seitenbaum einzuschränken +- irgendwie neue Config Mount Point für Seitenbaum + - ACHTUNG: User/Gruppenspezifisch. +- ggf. Tabs für versch. Seitenbäume...? + + +### UX + +Was die Redakteurinnen verwirrt: Sehen links den ganzen Seitenbaum; und nicht ersichtlich "ab wo darf ich was editieren"? + - [ ] !!! Idee 1: Visuelle Unterscheidung der Seiten nach Rechten, die ich habe + - SCHÄTZUNG: 1 PT + - Idee 2: Disablen des Auswählens der Seiten ohne Rechte? + - Idee 3: Was ausblenden vom Seitenbaum? + - [ ] !!! Idee 4: Wenn ich mich einlogge, springe ich direkt zur 1. obersten Seite, wo ich Zugriff habe. + - SCHÄTZUNG: 0.5 PT + - Depth First Search + - Cache (pro User; oder ggf. pro Rollenkombo); wenn cached node nicht mehr da -> neu aufbauen + - leichte Inkonsistenz (wir zeigen nicht in *allen* Fällen die 1. Seite auf die man Rechte hat) + - Idee 5: Bookmarks? + - klares Konzept; NACHTEIL: Neues UI Element. + - [x] !!! Idee 6: Parents bis zur Seite mit Access bleiben eingeblendet. + - SCHÄTZUNG: 2 PT + - mit isAncestorNodeOf() mgl. + - [x] Prototyp + - [ ] noch Crash in Neos UI, wenn ich versuche Seite zu laden auf die ich keine Seitenbaum-Rechte habe. + - [ ] wenn ich im Content auf eine Seite ohne Rechte navigiere, dann disabled page tree nicht // inspector nicht. + - !!!!!! Non breaking ness. + - SCHÄTZUNG: 2 PT + - ggf. neue Basisrolle... + +=> 5 PT. + +- Ausblenden: + - Was mache ich mit der Verbindung zur Wurzel? + - Mount Points -> Rafft auch keiner (Seiten potentiell mehrfach dargestellt, .... -> Rabbit Hole) + - + + +### TODO + +!!!! NodeTreePrivilege + +- +- TODO: Prioblem with OPageTree Privilege