diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index a4166df2054..fcab9065674 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -55,6 +55,7 @@ use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\SharedModel\Privilege\PrivilegeProviderInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\EventStore\CatchUp\CatchUp; use Neos\EventStore\DoctrineAdapter\DoctrineCheckpointStorage; @@ -95,6 +96,7 @@ public function __construct( private readonly ProjectionContentGraph $projectionContentGraph, private readonly CatchUpHookFactoryInterface $catchUpHookFactory, private readonly string $tableNamePrefix, + private readonly PrivilegeProviderInterface $privilegeProvider, ) { $this->checkpointStorage = new DoctrineCheckpointStorage( $this->dbalClient->getConnection(), @@ -261,7 +263,8 @@ public function getState(): ContentGraph $this->dbalClient, $this->nodeFactory, $this->nodeTypeManager, - $this->tableNamePrefix + $this->tableNamePrefix, + $this->privilegeProvider, ); } return $this->contentGraph; diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php index f9bc58758ea..924ae2761a8 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php @@ -61,7 +61,8 @@ public function build( $tableNamePrefix ), $catchUpHookFactory, - $tableNamePrefix + $tableNamePrefix, + $projectionFactoryDependencies->privilegeProvider, ) ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 18df5147ac2..6b79992ab35 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -33,6 +33,8 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; +use Neos\ContentRepository\Core\SharedModel\Privilege\PrivilegeProviderInterface; +use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; @@ -56,7 +58,8 @@ public function __construct( private readonly DbalClientInterface $client, private readonly NodeFactory $nodeFactory, private readonly NodeTypeManager $nodeTypeManager, - private readonly string $tableNamePrefix + private readonly string $tableNamePrefix, + private readonly PrivilegeProviderInterface $privilegeProvider, ) { } @@ -67,6 +70,10 @@ final public function getSubgraph( ): ContentSubgraphInterface { $index = $contentStreamId->value . '-' . $dimensionSpacePoint->hash . '-' . $visibilityConstraints->getHash(); if (!isset($this->subgraphs[$index])) { + $privileges = $this->privilegeProvider->getPrivileges($visibilityConstraints); + if (!$privileges->isContentStreamAllowed($contentStreamId)) { + throw new \RuntimeException(sprintf('No access to content stream "%s"', $contentStreamId->value), 1681306937); + } $this->subgraphs[$index] = new ContentSubgraphWithRuntimeCaches( new ContentSubgraph( $contentStreamId, @@ -75,7 +82,8 @@ final public function getSubgraph( $this->client, $this->nodeFactory, $this->nodeTypeManager, - $this->tableNamePrefix + $this->tableNamePrefix, + $this->privilegeProvider, ) ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index b81ea9094a0..7cc13ad72f7 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -63,6 +63,7 @@ use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; +use Neos\ContentRepository\Core\SharedModel\Privilege\PrivilegeProviderInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** @@ -102,7 +103,8 @@ public function __construct( private readonly DbalClientInterface $client, private readonly NodeFactory $nodeFactory, private readonly NodeTypeManager $nodeTypeManager, - private readonly string $tableNamePrefix + private readonly string $tableNamePrefix, + private readonly PrivilegeProviderInterface $privilegeProvider, ) { } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index e1ab60571a0..e9325bcb914 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -32,6 +32,7 @@ use Neos\ContentRepository\Core\Projection\Projections; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceFinder; +use Neos\ContentRepository\Core\SharedModel\Privilege\PrivilegeProviderInterface; use Neos\ContentRepository\Core\SharedModel\User\StaticUserIdProvider; use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; @@ -67,6 +68,7 @@ public function __construct( private readonly InterDimensionalVariationGraph $variationGraph, private readonly ContentDimensionSourceInterface $contentDimensionSource, private readonly UserIdProviderInterface $userIdProvider, + private readonly PrivilegeProviderInterface $privilegeProvider, private readonly ClockInterface $clock, ) { } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index 10a87327f57..88219a8108f 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -30,6 +30,7 @@ use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\ProjectionCatchUpTriggerInterface; use Neos\ContentRepository\Core\Projection\Projections; +use Neos\ContentRepository\Core\SharedModel\Privilege\PrivilegeProviderInterface; use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; use Neos\EventStore\EventStoreInterface; use Psr\Clock\ClockInterface; @@ -54,6 +55,7 @@ public function __construct( ProjectionsFactory $projectionsFactory, private readonly ProjectionCatchUpTriggerInterface $projectionCatchUpTrigger, private readonly UserIdProviderInterface $userIdProvider, + private readonly PrivilegeProviderInterface $privilegeProvider, private readonly ClockInterface $clock, ) { $contentDimensionZookeeper = new ContentDimensionZookeeper($contentDimensionSource); @@ -70,7 +72,8 @@ public function __construct( $contentDimensionSource, $contentDimensionZookeeper, $interDimensionalVariationGraph, - new PropertyConverter($propertySerializer) + new PropertyConverter($propertySerializer), + $this->privilegeProvider, ); $this->projections = $projectionsFactory->build($this->projectionFactoryDependencies); @@ -99,6 +102,7 @@ public function build(): ContentRepository $this->projectionFactoryDependencies->interDimensionalVariationGraph, $this->projectionFactoryDependencies->contentDimensionSource, $this->userIdProvider, + $this->privilegeProvider, $this->clock, ); } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php index 606d8576c97..e674e8d2cb4 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php @@ -21,6 +21,7 @@ use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Factory\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Privilege\PrivilegeProviderInterface; use Neos\EventStore\EventStoreInterface; /** @@ -37,6 +38,7 @@ public function __construct( public readonly ContentDimensionZookeeper $contentDimensionZookeeper, public readonly InterDimensionalVariationGraph $interDimensionalVariationGraph, public readonly PropertyConverter $propertyConverter, + public readonly PrivilegeProviderInterface $privilegeProvider, ) { } } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Privilege/ContentStreamPrivilege.php b/Neos.ContentRepository.Core/Classes/SharedModel/Privilege/ContentStreamPrivilege.php new file mode 100644 index 00000000000..305dbfc1085 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Privilege/ContentStreamPrivilege.php @@ -0,0 +1,35 @@ +allowedContentStreamIds, + $disallowedContentStreamIds ?? $this->disallowedContentStreamIds, + ); + } +} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Privilege/PrivilegeProviderInterface.php b/Neos.ContentRepository.Core/Classes/SharedModel/Privilege/PrivilegeProviderInterface.php new file mode 100644 index 00000000000..939d33b26f4 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Privilege/PrivilegeProviderInterface.php @@ -0,0 +1,15 @@ +contentStreamPrivilege, + ); + } + + public function isContentStreamAllowed(ContentStreamId $contentStreamId): bool + { + if ($this->contentStreamPrivilege === null) { + return true; + } + return $this->contentStreamPrivilege->allowedContentStreamIds->contain($contentStreamId); + } +} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Workspace/ContentStreamIds.php b/Neos.ContentRepository.Core/Classes/SharedModel/Workspace/ContentStreamIds.php new file mode 100644 index 00000000000..7c6734fbc86 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Workspace/ContentStreamIds.php @@ -0,0 +1,54 @@ + + */ +final class ContentStreamIds implements \IteratorAggregate +{ + /** + * @param ContentStreamId[] $contentStreamIds + */ + private function __construct( + private readonly array $contentStreamIds, + ) { + if ($this->contentStreamIds === []) { + throw new \InvalidArgumentException('ContentStreamIds must not be empty', 1681306355); + } + } + + public static function fromContentStreamIds(ContentStreamId ...$contentStreamIds): self + { + return new self($contentStreamIds); + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->contentStreamIds); + } + + public function contain(ContentStreamId $contentStreamId): bool + { + foreach ($this->contentStreamIds as $id) { + if ($id->equals($contentStreamId)) { + return true; + } + } + return false; + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index b62ffff43bc..141dd32092a 100644 --- a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php +++ b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php @@ -15,12 +15,14 @@ use Neos\ContentRepository\Core\Projection\ProjectionCatchUpTriggerInterface; use Neos\ContentRepository\Core\Projection\ProjectionFactoryInterface; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\SharedModel\Privilege\PrivilegeProviderInterface; use Neos\ContentRepositoryRegistry\Exception\ContentRepositoryNotFound; use Neos\ContentRepositoryRegistry\Exception\InvalidConfigurationException; use Neos\ContentRepositoryRegistry\Factory\Clock\ClockFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\ContentDimensionSource\ContentDimensionSourceFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\EventStore\EventStoreFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\NodeTypeManager\NodeTypeManagerFactoryInterface; +use Neos\ContentRepositoryRegistry\Factory\PrivilegeProvider\PrivilegeProviderFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\ProjectionCatchUpTrigger\ProjectionCatchUpTriggerFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepositoryRegistry\Factory\UserIdProvider\UserIdProviderFactoryInterface; @@ -138,6 +140,7 @@ private function buildFactory(ContentRepositoryId $contentRepositoryId): Content } try { $clock = $this->buildClock($contentRepositoryId, $contentRepositorySettings); + $userIdProvider = $this->buildUserIdProvider($contentRepositoryId, $contentRepositorySettings); return new ContentRepositoryFactory( $contentRepositoryId, $this->buildEventStore($contentRepositoryId, $contentRepositorySettings, $clock), @@ -146,7 +149,8 @@ private function buildFactory(ContentRepositoryId $contentRepositoryId): Content $this->buildPropertySerializer($contentRepositoryId, $contentRepositorySettings), $this->buildProjectionsFactory($contentRepositoryId, $contentRepositorySettings), $this->buildProjectionCatchUpTrigger($contentRepositoryId, $contentRepositorySettings), - $this->buildUserIdProvider($contentRepositoryId, $contentRepositorySettings), + $userIdProvider, + $this->buildPrivilegeProvider($contentRepositoryId, $contentRepositorySettings, $userIdProvider, $this), $clock, ); } catch (\Exception $exception) { @@ -248,6 +252,16 @@ private function buildUserIdProvider(ContentRepositoryId $contentRepositoryId, a return $userIdProviderFactory->build($contentRepositoryId, $contentRepositorySettings['userIdProvider']['options'] ?? []); } + private function buildPrivilegeProvider(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, UserIdProviderInterface $userIdProvider, self $contentRepositoryRegistry): PrivilegeProviderInterface + { + isset($contentRepositorySettings['privilegeProvider']['factoryObjectName']) || throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have privilegeProvider.factoryObjectName configured.', $contentRepositoryId->value); + $privilegeProviderFactory = $this->objectManager->get($contentRepositorySettings['privilegeProvider']['factoryObjectName']); + if (!$privilegeProviderFactory instanceof PrivilegeProviderFactoryInterface) { + throw InvalidConfigurationException::fromMessage('privilegeProvider.factoryObjectName for content repository "%s" is not an instance of %s but %s.', $contentRepositoryId->value, PrivilegeProviderFactoryInterface::class, get_debug_type($privilegeProviderFactory)); + } + return $privilegeProviderFactory->build($contentRepositoryId, $contentRepositorySettings['userIdProvider']['options'] ?? [], $userIdProvider, $contentRepositoryRegistry); + } + private function buildClock(ContentRepositoryId $contentRepositoryIdentifier, array $contentRepositorySettings): ClockInterface { isset($contentRepositorySettings['clock']['factoryObjectName']) || throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have clock.factoryObjectName configured.', $contentRepositoryIdentifier->value); diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/PrivilegeProvider/FakePrivilegeProvider.php b/Neos.ContentRepositoryRegistry/Classes/Factory/PrivilegeProvider/FakePrivilegeProvider.php new file mode 100644 index 00000000000..185b357bec4 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/PrivilegeProvider/FakePrivilegeProvider.php @@ -0,0 +1,40 @@ +userIdProvider->getUserId(); + $contentRepository = $this->contentRepositoryRegistry->get($this->contentRepositoryId); + + $privileges = Privileges::create(); + + $userWorkspace = $contentRepository->getWorkspaceFinder()->findOneByWorkspaceOwner($userId->value); + if ($userWorkspace === null) { + return $privileges; + } + return $privileges->with( + contentStreamPrivilege: ContentStreamPrivilege::create()->with(allowedContentStreamIds: ContentStreamIds::fromContentStreamIds($userWorkspace->currentContentStreamId)) + ); + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/PrivilegeProvider/FakePrivilegeProviderFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/PrivilegeProvider/FakePrivilegeProviderFactory.php new file mode 100644 index 00000000000..339ad1c5ad5 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/PrivilegeProvider/FakePrivilegeProviderFactory.php @@ -0,0 +1,18 @@ +