Skip to content
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

WIP - page tree restrictions #42

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
30 changes: 30 additions & 0 deletions Classes/Domain/Dto/MatcherConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 = [];
Expand Down
13 changes: 13 additions & 0 deletions Classes/Domain/Dto/MatcherConfigurationSelectedNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
21 changes: 18 additions & 3 deletions Classes/DynamicRoleEnforcement/DynamicPolicyRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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'
];

/**
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -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, [])
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion Classes/Service/ACLCheckerService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
17 changes: 14 additions & 3 deletions Classes/Service/DynamicRoleGeneratorService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
];
}
Expand Down
16 changes: 16 additions & 0 deletions Configuration/Policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -54,3 +65,8 @@ roles:
-
privilegeTarget: 'Sandstorm.NeosAcl:RemoveAllNodes'
permission: GRANT
# TODO: ENABLED
-
privilegeTarget: 'Sandstorm.NeosAcl:NodeTreeAllNodes'
permission: GRANT
'Sandstorm.NeosAcl:NodeTreeRestricted': {}
51 changes: 51 additions & 0 deletions Documentation/2023_12_06_PageTreeRestrictions.md
Original file line number Diff line number Diff line change
@@ -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