Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -475,10 +475,28 @@ private function addSubtreeTagConstraints(QueryBuilder $queryBuilder, string $hi
{
$hierarchyRelationTablePrefix = $hierarchyRelationTableAlias === '' ? '' : $hierarchyRelationTableAlias . '.';
$i = 0;

foreach ($this->visibilityConstraints->excludedSubtreeTags as $excludedTag) {
$queryBuilder->andWhere('NOT JSON_CONTAINS_PATH(' . $hierarchyRelationTablePrefix . 'subtreetags, \'one\', :tagPath' . $i . ')')->setParameter('tagPath' . $i, '$."' . $excludedTag->value . '"');
$queryBuilder->andWhere(
'NOT JSON_CONTAINS_PATH(' . $hierarchyRelationTablePrefix . 'subtreetags, \'one\', :tagPath' . $i . ')')
->setParameter('tagPath' . $i, '$."' . $excludedTag->value . '"');
$i++;
}

if (!$this->visibilityConstraints->includedSubtreeTags->isEmpty()) {
$includedParts = [];
$expression = $queryBuilder->expr();
foreach ($this->visibilityConstraints->includedSubtreeTags as $includedTag) {
$includedParts[] = $expression->eq(
'JSON_CONTAINS_PATH(' . $hierarchyRelationTablePrefix . 'subtreetags, \'one\', :tagPath' . $i . ')',
$expression->literal(1)
);
$queryBuilder->setParameter('tagPath' . $i, '$."' . $includedTag->value . '"');
$i++;
}
$queryBuilder->andWhere($expression->or(...$includedParts));
}
}

private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter|CountChildNodesFilter $filter): QueryBuilder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@contentrepository @adapters=DoctrineDBAL
Feature: Filter nodes based on their subtree tags

Background:
Given using no content dimensions
And using the following node types:
"""yaml
'Neos.ContentRepository.Testing:AbstractPage':
abstract: true
'Neos.ContentRepository.Testing:Homepage':
superTypes:
'Neos.ContentRepository.Testing:AbstractPage': true
'Neos.ContentRepository.Testing:Page':
superTypes:
'Neos.ContentRepository.Testing:AbstractPage': true
"""
And using identifier "default", I define a content repository
And I am in content repository "default"
And I am user identified by "initiating-user-identifier"
And the command CreateRootWorkspace is executed with payload:
| Key | Value |
| workspaceName | "live" |
| newContentStreamId | "cs-identifier" |
And I am in workspace "live" and dimension space point {}
And the command CreateRootNodeAggregateWithNode is executed with payload:
| Key | Value |
| nodeAggregateId | "lady-eleonode-rootford" |
| nodeTypeName | "Neos.ContentRepository:Root" |
And the following CreateNodeAggregateWithNode commands are executed:
| nodeAggregateId | nodeName | nodeTypeName | parentNodeAggregateId | initialPropertyValues | tetheredDescendantNodeAggregateIds |
| home | home | Neos.ContentRepository.Testing:Homepage | lady-eleonode-rootford | {} | {"terms": "terms", "contact": "contact"} |
| a | a | Neos.ContentRepository.Testing:Page | home | {} | {} |
| a1 | a1 | Neos.ContentRepository.Testing:Page | a | {} | {} |
| a2 | a2 | Neos.ContentRepository.Testing:Page | a | {} | {} |
| a2a | a2a | Neos.ContentRepository.Testing:Page | a2 | {} | {} |
| a2a1 | a2a1 | Neos.ContentRepository.Testing:Page | a2a | {} | {} |
| a2a2 | a2a2 | Neos.ContentRepository.Testing:Page | a2a | {} | {} |
| a2a2a | a2a2a | Neos.ContentRepository.Testing:Page | a2a2 | {} | {} |
| a2a2b | a2a2b | Neos.ContentRepository.Testing:Page | a2a2 | {} | {} |
| a2a2c | a2a2c | Neos.ContentRepository.Testing:Page | a2a2 | {} | {} |
| a2a2d | a2a2d | Neos.ContentRepository.Testing:Page | a2a2 | {} | {} |
| a2b | a2b | Neos.ContentRepository.Testing:Page | a2 | {} | {} |
| a2b1 | a2b1 | Neos.ContentRepository.Testing:Page | a2b | {} | {} |
| b | b | Neos.ContentRepository.Testing:Page | home | {} | {} |
And the command TagSubtree is executed with payload:
| Key | Value |
| nodeAggregateId | "a2a2c" |
| workspaceName | "live" |
| nodeVariantSelectionStrategy | "allSpecializations" |
| tag | "mytag" |
And the command TagSubtree is executed with payload:
| Key | Value |
| nodeAggregateId | "a2a2d" |
| workspaceName | "live" |
| nodeVariantSelectionStrategy | "allSpecializations" |
| tag | "mysecondtag" |
Scenario: Exclude Subtree Tag
When VisibilityConstraints are set to "exclude" "mytag"
And I execute the findDescendantNodes query for entry node aggregate id "a2a2" I expect the nodes "a2a2a,a2a2b,a2a2d" to be returned
Scenario: Include Subtree Tag
When VisibilityConstraints are set to "include" "mytag"
And I execute the findDescendantNodes query for entry node aggregate id "a2a2" I expect the nodes "a2a2c" to be returned
When VisibilityConstraints are set to "include" "nonexistenttag"
And I execute the findDescendantNodes query for entry node aggregate id "a2a2" I expect no nodes to be returned
When VisibilityConstraints are set to "include" "mytag,mysecondtag"
And I execute the findDescendantNodes query for entry node aggregate id "a2a2" I expect the nodes "a2a2c,a2a2d" to be returned
Scenario: Include and exclude Subtree Tag
When VisibilityConstraints are set to "exclude and include" "mysecondtag" "mytag"
And I execute the findDescendantNodes query for entry node aggregate id "a2a2" I expect the nodes "a2a2c" to be returned
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@
{
/**
* @param SubtreeTags $excludedSubtreeTags A set of {@see SubtreeTag} instances that will be _excluded_ from the results of any content graph query
* @param SubtreeTags $includedSubtreeTags A set of {@see SubtreeTag} instances that will be _included_ in the results of any content graph query
*/
private function __construct(
public SubtreeTags $excludedSubtreeTags,
public SubtreeTags $includedSubtreeTags,
) {
}

Expand All @@ -53,25 +55,49 @@ private function __construct(
*/
public static function createEmpty(): self
{
return new self(SubtreeTags::createEmpty());
return new self(SubtreeTags::createEmpty(), SubtreeTags::createEmpty());
}

/**
* @param SubtreeTags $excluded A set of {@see SubtreeTag} instances that will be _excluded_ from the results of any content graph query
* @param SubtreeTags $included A set of {@see SubtreeTag} instances that will be _included_ in the results of any content graph query
*/
public static function excludeAndIncludeSubtreeTags(SubtreeTags $excluded, SubtreeTags $included): self
{
return new self($excluded, $included);
}

/**
* @param SubtreeTags $subtreeTags A set of {@see SubtreeTag} instances that will be _excluded_ from the results of any content graph query
*/
public static function excludeSubtreeTags(SubtreeTags $subtreeTags): self
{
return new self($subtreeTags);
return new self($subtreeTags, SubtreeTags::createEmpty());
}

/**
* @param SubtreeTags $subtreeTags A set of {@see SubtreeTag} instances that will be _included_ in the results of any content graph query
*/
public static function includeSubtreeTags(SubtreeTags $subtreeTags): self
{
return new self(SubtreeTags::createEmpty(), $subtreeTags);
}

public function getHash(): string
{
return md5(implode('|', $this->excludedSubtreeTags->toStringArray()));
return md5(
implode('|', $this->excludedSubtreeTags->toStringArray())
. '|' .
implode('|', $this->includedSubtreeTags->toStringArray())
);
}

public function merge(VisibilityConstraints $other): self
{
return new self($this->excludedSubtreeTags->merge($other->excludedSubtreeTags));
return new self(
$this->excludedSubtreeTags->merge($other->excludedSubtreeTags),
$this->includedSubtreeTags->merge($other->includedSubtreeTags)
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;
use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag;
use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate;
Expand Down Expand Up @@ -110,15 +112,51 @@ public function iAmInWorkspaceAndDimensionSpacePoint(string $workspaceName, stri
}

/**
* @When /^VisibilityConstraints are set to "(withoutRestrictions|default)"$/
* @When /^VisibilityConstraints are set to "(withoutRestrictions|default|empty|exclude|include|exclude and include)"(?: "([^"]*)")?(?: "([^"]*)")?$/
*/
public function visibilityConstraintsAreSetTo(string $restrictionType): void
public function visibilityConstraintsAreSetTo(string $type, ?string $excludeTags = null, ?string $includeTags = null): void
{
$this->currentVisibilityConstraints = match ($restrictionType) {
'withoutRestrictions' => VisibilityConstraints::withoutRestrictions(),
'default' => VisibilityConstraints::default(),
default => throw new \InvalidArgumentException('Visibility constraint "' . $restrictionType . '" not supported.'),
};
switch ($type) {
case 'withoutRestrictions':
$this->currentVisibilityConstraints = VisibilityConstraints::withoutRestrictions();
return;
case 'default':
$this->currentVisibilityConstraints = VisibilityConstraints::default();
return;
case 'empty':
$this->currentVisibilityConstraints = VisibilityConstraints::createEmpty();
return;

case 'exclude':
$excluded = $excludeTags ? $this->parseTags($excludeTags) : SubtreeTags::createEmpty();
$this->currentVisibilityConstraints = VisibilityConstraints::excludeSubtreeTags($excluded);
return;

case 'include':
$included = $excludeTags ? $this->parseTags($excludeTags) : SubtreeTags::createEmpty();
$this->currentVisibilityConstraints = VisibilityConstraints::includeSubtreeTags($included);
return;

case 'exclude and include':
$excluded = $excludeTags ? $this->parseTags($excludeTags) : SubtreeTags::createEmpty();
$included = $includeTags ? $this->parseTags($includeTags) : SubtreeTags::createEmpty();
$this->currentVisibilityConstraints = VisibilityConstraints::excludeAndIncludeSubtreeTags($excluded, $included);
return;
}

throw new \InvalidArgumentException("Visibility constraint '$type' not supported.");
}

private function parseTags(string $tags): SubtreeTags
{
$tags = array_filter(array_map(
fn(string $s) => SubtreeTag::fromString(trim($s)),
explode(',', $tags)
), fn(string $s) => $s !== '');

return empty($tags)
? SubtreeTags::createEmpty()
: SubtreeTags::create(...$tags);
}

public function getCurrentSubgraph(): ContentSubgraphInterface
Expand Down
4 changes: 2 additions & 2 deletions Neos.Media.Browser/Classes/Controller/UsageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
use Neos\Neos\Domain\Service\NodeTypeNameFactory;
use Neos\Neos\Domain\Service\WorkspaceService;
use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationInterface;
use Neos\Neos\Service\UserService;

/**
Expand Down Expand Up @@ -77,7 +77,7 @@ class UsageController extends ActionController

/**
* @Flow\Inject
* @var ContentRepositoryAuthorizationService
* @var ContentRepositoryAuthorizationInterface
*/
protected $contentRepositoryAuthorizationService;

Expand Down
4 changes: 2 additions & 2 deletions Neos.Neos/Classes/Controller/Frontend/NodeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
use Neos\Neos\FrontendRouting\NodeShortcutResolver;
use Neos\Neos\FrontendRouting\NodeUriBuilderFactory;
use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationInterface;
use Neos\Neos\Utility\NodeTypeWithFallbackProvider;
use Neos\Neos\View\FusionView;

Expand Down Expand Up @@ -106,7 +106,7 @@ class NodeController extends ActionController
protected NodeUriBuilderFactory $nodeUriBuilderFactory;

#[Flow\Inject]
protected ContentRepositoryAuthorizationService $contentRepositoryAuthorizationService;
protected ContentRepositoryAuthorizationInterface $contentRepositoryAuthorizationService;

#[Flow\Inject]
protected ContentSubgraphCacheWarmup|null $contentSubgraphCacheWarmup = null;
Expand Down
14 changes: 7 additions & 7 deletions Neos.Neos/Classes/Domain/Model/NodePermissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
namespace Neos\Neos\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationInterface;

/**
* Evaluated permissions a specific user has on a node, usually evaluated by the {@see ContentRepositoryAuthorizationService}
* Evaluated permissions a specific user has on a node, usually evaluated by the {@see ContentRepositoryAuthorizationInterface}
*
* - read: Permission to read the node and its properties and references
* - edit: Permission to change the node
* - remove: Permission to remove the node
*
* @api because it is returned by the {@see ContentRepositoryAuthorizationService}
* @api because it is returned by the {@see ContentRepositoryAuthorizationInterface}
*/
#[Flow\Proxy(false)]
final readonly class NodePermissions
Expand All @@ -23,23 +24,22 @@
* @param bool $edit Permission to edit the corresponding node
*/
private function __construct(
public bool $read,
public bool $edit,
public bool $remove,
private string $reason,
) {
}

/**
* @param bool $read Permission to read data from the corresponding node
* @param bool $edit Permission to edit the corresponding node
* @param string $reason Human-readable explanation for why this permission was evaluated {@see getReason()}
*/
public static function create(
bool $read,
bool $edit,
bool $remove,
string $reason,
): self {
return new self($read, $edit, $reason);
return new self($edit, $remove, $reason);
}

public static function all(string $reason): self
Expand Down
6 changes: 3 additions & 3 deletions Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
namespace Neos\Neos\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationInterface;

/**
* Evaluated permissions a specific user has on a workspace, usually evaluated by the {@see ContentRepositoryAuthorizationService}
* Evaluated permissions a specific user has on a workspace, usually evaluated by the {@see ContentRepositoryAuthorizationInterface}
*
* - read: Permission to read data from the corresponding workspace (e.g. get hold of and traverse the content graph)
* - write: Permission to write to the corresponding workspace, including publishing a derived workspace to it
* - manage: Permission to change the metadata and roles of the corresponding workspace (e.g. change description/title or add/remove workspace roles)
*
* @api because it is returned by the {@see ContentRepositoryAuthorizationService}
* @api because it is returned by the {@see ContentRepositoryAuthorizationInterface}
*/
#[Flow\Proxy(false)]
final readonly class WorkspacePermissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
use Neos\Neos\Domain\Model\WorkspaceRoleSubjectType;
use Neos\Neos\Domain\Model\WorkspaceTitle;
use Neos\Neos\Domain\Service\WorkspaceService;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationInterface;

/**
* Implementation detail of {@see WorkspaceService} and {@see ContentRepositoryAuthorizationService}
* Implementation detail of {@see WorkspaceService} and {@see ContentRepositoryAuthorizationInterface}
*
* @internal Neos users should not need to deal with this low level repository. No security is imposed here. Please use the {@see WorkspaceService}!
*/
Expand Down
6 changes: 3 additions & 3 deletions Neos.Neos/Classes/Domain/Service/WorkspaceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
use Neos\Neos\Domain\Model\WorkspaceTitle;
use Neos\Neos\Domain\Repository\WorkspaceMetadataAndRoleRepository;
use Neos\Neos\Domain\SubtreeTagging\SoftRemoval\SoftRemovalGarbageCollector;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationInterface;

/**
* Central authority to interact with Content Repository Workspaces within Neos
Expand All @@ -53,7 +53,7 @@ public function __construct(
private ContentRepositoryRegistry $contentRepositoryRegistry,
private WorkspaceMetadataAndRoleRepository $metadataAndRoleRepository,
private UserService $userService,
private ContentRepositoryAuthorizationService $authorizationService,
private ContentRepositoryAuthorizationInterface $authorizationService,
private SecurityContext $securityContext,
private SoftRemovalGarbageCollector $softRemovalGarbageCollector,
) {
Expand Down Expand Up @@ -265,7 +265,7 @@ public function deleteWorkspace(ContentRepositoryId $contentRepositoryId, Worksp
/**
* Get all role assignments for the specified workspace
*
* NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissions()} should be used!
* NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationInterface::getWorkspacePermissions()} should be used!
*/
public function getWorkspaceRoleAssignments(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspaceRoleAssignments
{
Expand Down
Loading
Loading