From 4e3c72fd587413cad57718ec203bb1b587bd2e93 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 11 Oct 2024 13:59:09 +0200 Subject: [PATCH] BUGFIX: Fix workspace role assignment after migration, import and site creation With #5146 a basic workspace access control was implemented. With that, users won't have access to workspaces unless they have a role assigned. This can be achieved via ```shell ./flow workspace:assignrole ``` This bugfix makes sure, that the default behavior (users with the role `Neos.Neos:LivePublisher` can collaborate on the `live` workspace) is automatically ensured when creating/importing a site. Furthermore, the migration command has been fixed to add metadata & role assignments even if no workspace title was set: ```shell ./flow migrateevents:migrateWorkspaceMetadataToWorkspaceService ``` Related: #4726 --- .../Service/ContentRepositoryBootstrapper.php | 86 --------------- .../Classes/Service/EventMigrationService.php | 100 +++++++----------- .../Classes/Command/CrCommandController.php | 8 ++ .../Classes/Domain/Service/SiteService.php | 10 +- .../Domain/Service/SiteServiceInternals.php | 41 +++++-- .../Service/SiteServiceInternalsFactory.php | 6 ++ .../Domain/Service/WorkspaceService.php | 16 +++ 7 files changed, 108 insertions(+), 159 deletions(-) delete mode 100644 Neos.ContentRepository.Core/Classes/Service/ContentRepositoryBootstrapper.php diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryBootstrapper.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryBootstrapper.php deleted file mode 100644 index 3a835b5cb77..00000000000 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryBootstrapper.php +++ /dev/null @@ -1,86 +0,0 @@ -contentRepository->getWorkspaceFinder()->findOneByName($liveWorkspaceName); - if ($liveWorkspace instanceof Workspace) { - return $liveWorkspace; - } - - $this->contentRepository->handle( - CreateRootWorkspace::create( - $liveWorkspaceName, - WorkspaceTitle::fromString('Live'), - WorkspaceDescription::fromString('Public live workspace'), - ContentStreamId::create() - ) - ); - $liveWorkspace = $this->contentRepository->getWorkspaceFinder()->findOneByName($liveWorkspaceName); - if (!$liveWorkspace) { - throw new \Exception('Live workspace creation failed', 1699002435); - } - - return $liveWorkspace; - } - - /** - * Retrieve the root Node Aggregate ID for the specified $workspace - * If no root node of the specified $rootNodeTypeName exist, it will be created - */ - public function getOrCreateRootNodeAggregate( - Workspace $workspace, - NodeTypeName $rootNodeTypeName - ): NodeAggregateId { - $rootNodeAggregate = $this->contentRepository->getContentGraph($workspace->workspaceName)->findRootNodeAggregateByType( - $rootNodeTypeName - ); - if ($rootNodeAggregate !== null) { - return $rootNodeAggregate->nodeAggregateId; - } - $rootNodeAggregateId = NodeAggregateId::create(); - $this->contentRepository->handle(CreateRootNodeAggregateWithNode::create( - $workspace->workspaceName, - $rootNodeAggregateId, - $rootNodeTypeName, - )); - return $rootNodeAggregateId; - } -} diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php index 32dbba18330..9cd7d315f80 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepositoryRegistry\Service; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; @@ -498,31 +499,14 @@ public function migrateWorkspaceMetadataToWorkspaceService(\Closure $outputFn): switch ($eventType) { case 'RootWorkspaceWasCreated': - $eventData = self::decodeEventPayload($eventEnvelope); - if (!isset($eventData['workspaceTitle'])) { - // without the field it's not a legacy workspace creation event - continue 2; - } - $workspaces[$eventData['workspaceName']] = [ - 'workspaceName' => $eventData['workspaceName'], - 'workspaceTitle' => $eventData['workspaceTitle'], - 'workspaceDescription' => $eventData['workspaceDescription'], - 'baseWorkspaceName' => null, - 'workspaceOwner' => null - ]; - break; case 'WorkspaceWasCreated': $eventData = self::decodeEventPayload($eventEnvelope); - if (!isset($eventData['workspaceTitle'])) { - // without the field it's not a legacy workspace creation event - continue 2; - } $workspaces[$eventData['workspaceName']] = [ 'workspaceName' => $eventData['workspaceName'], - 'baseWorkspaceName' => $eventData['baseWorkspaceName'], - 'workspaceTitle' => $eventData['workspaceTitle'], - 'workspaceDescription' => $eventData['workspaceDescription'], - 'workspaceOwner' => $eventData['workspaceOwner'] ?? null + 'workspaceTitle' => !empty($eventData['workspaceTitle']) ? $eventData['workspaceTitle'] : $eventData['workspaceName'], + 'workspaceDescription' => $eventData['workspaceDescription'] ?? '', + 'baseWorkspaceName' => $eventData['baseWorkspaceName'] ?? null, + 'workspaceOwner' => $eventData['workspaceOwner'] ?? null, ]; break; case 'WorkspaceBaseWorkspaceWasChanged': @@ -574,29 +558,17 @@ public function migrateWorkspaceMetadataToWorkspaceService(\Closure $outputFn): $outputFn(sprintf('Found %d legacy workspace events resulting in %d workspaces.', $numberOfHandledWorkspaceEvents, count($workspaces))); - // adding metadata + // adding metadata & role assignments $addedWorkspaceMetadata = 0; foreach ($workspaces as $workspaceRow) { $workspaceName = WorkspaceName::fromString($workspaceRow['workspaceName']); + $outputFn(sprintf('Workspace "%s"', $workspaceName->value)); $baseWorkspaceName = isset($workspaceRow['baseWorkspaceName']) ? WorkspaceName::fromString($workspaceRow['baseWorkspaceName']) : null; $workspaceOwner = $workspaceRow['workspaceOwner'] ?? null; $isPersonalWorkspace = str_starts_with($workspaceName->value, 'user-'); $isPrivateWorkspace = $workspaceOwner !== null && !$isPersonalWorkspace; $isInternalWorkspace = $baseWorkspaceName !== null && $workspaceOwner === null; - $query = <<connection->fetchOne($query, [ - 'contentRepositoryId' => $this->contentRepositoryId->value, - 'workspaceName' => $workspaceName->value, - ]); - if ($metadataExists !== 0) { - $outputFn(sprintf('Metadata for "%s" exists already.', $workspaceName->value)); - continue; - } if ($baseWorkspaceName === null) { $classification = WorkspaceClassification::ROOT; } elseif ($isPersonalWorkspace) { @@ -604,48 +576,54 @@ public function migrateWorkspaceMetadataToWorkspaceService(\Closure $outputFn): } else { $classification = WorkspaceClassification::SHARED; } - $this->connection->insert('neos_neos_workspace_metadata', [ - 'content_repository_id' => $this->contentRepositoryId->value, - 'workspace_name' => $workspaceName->value, - 'title' => $workspaceRow['workspaceTitle'], - 'description' => $workspaceRow['workspaceDescription'], - 'classification' => $classification->value, - 'owner_user_id' => $isPersonalWorkspace ? $workspaceOwner : null, - ]); - if ($workspaceName->isLive()) { - $this->connection->insert('neos_neos_workspace_role', [ + try { + $this->connection->insert('neos_neos_workspace_metadata', [ 'content_repository_id' => $this->contentRepositoryId->value, 'workspace_name' => $workspaceName->value, + 'title' => $workspaceRow['workspaceTitle'], + 'description' => $workspaceRow['workspaceDescription'], + 'classification' => $classification->value, + 'owner_user_id' => $isPersonalWorkspace ? $workspaceOwner : null, + ]); + $outputFn(' Added metadata'); + } catch (UniqueConstraintViolationException) { + $outputFn(' Metadata already exists'); + } + $roleAssignment = []; + if ($workspaceName->isLive()) { + $roleAssignment = [ 'subject_type' => WorkspaceRoleSubjectType::GROUP->value, 'subject' => 'Neos.Neos:LivePublisher', 'role' => WorkspaceRole::COLLABORATOR->value, - ]); + ]; } elseif ($isInternalWorkspace) { - $this->connection->insert('neos_neos_workspace_role', [ - 'content_repository_id' => $this->contentRepositoryId->value, - 'workspace_name' => $workspaceName->value, + $roleAssignment = [ 'subject_type' => WorkspaceRoleSubjectType::GROUP->value, 'subject' => 'Neos.Neos:AbstractEditor', 'role' => WorkspaceRole::COLLABORATOR->value, - ]); + ]; } elseif ($isPrivateWorkspace) { - $this->connection->insert('neos_neos_workspace_role', [ - 'content_repository_id' => $this->contentRepositoryId->value, - 'workspace_name' => $workspaceName->value, + $roleAssignment = [ 'subject_type' => WorkspaceRoleSubjectType::USER->value, 'subject' => $workspaceOwner, 'role' => WorkspaceRole::COLLABORATOR->value, - ]); + ]; + } + if ($roleAssignment !== []) { + try { + $this->connection->insert('neos_neos_workspace_role', [ + 'content_repository_id' => $this->contentRepositoryId->value, + 'workspace_name' => $workspaceName->value, + ...$roleAssignment, + ]); + $outputFn(' Role assignment added'); + } catch (UniqueConstraintViolationException) { + $outputFn(' Role assignment already exists'); + } } - $outputFn(sprintf('Added metadata for "%s".', $workspaceName->value)); $addedWorkspaceMetadata++; } - if ($addedWorkspaceMetadata === 0) { - $outputFn('Migration was not necessary.'); - return; - } - - $outputFn(sprintf('Added metadata for %d workspaces.', $addedWorkspaceMetadata)); + $outputFn(sprintf('Added metadata & role assignments for %d workspaces.', $addedWorkspaceMetadata)); } /** ------------------------ */ diff --git a/Neos.Neos/Classes/Command/CrCommandController.php b/Neos.Neos/Classes/Command/CrCommandController.php index ba7bb7e9b27..e7e70822eb3 100644 --- a/Neos.Neos/Classes/Command/CrCommandController.php +++ b/Neos.Neos/Classes/Command/CrCommandController.php @@ -9,6 +9,7 @@ use Neos\ContentRepository\Core\Projection\CatchUpOptions; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\ExportService; use Neos\ContentRepository\Export\ExportServiceFactory; use Neos\ContentRepository\Export\ImportService; @@ -22,6 +23,9 @@ use Neos\Flow\ResourceManagement\ResourceRepository; use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\AssetUsage\AssetUsageService; +use Neos\Neos\Domain\Model\WorkspaceRole; +use Neos\Neos\Domain\Model\WorkspaceRoleAssignment; +use Neos\Neos\Domain\Service\WorkspaceService; use Neos\Utility\Files; class CrCommandController extends CommandController @@ -40,6 +44,7 @@ public function __construct( private readonly ContentRepositoryRegistry $contentRepositoryRegistry, private readonly ProjectionReplayServiceFactory $projectionReplayServiceFactory, private readonly AssetUsageService $assetUsageService, + private readonly WorkspaceService $workspaceService, ) { parent::__construct(); } @@ -114,6 +119,9 @@ public function importCommand(string $path, string $contentRepository = 'default $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory); $projectionService->replayAllProjections(CatchUpOptions::create()); + $this->outputLine('Assigning live workspace role'); + $this->workspaceService->assignWorkspaceRole($contentRepositoryId, WorkspaceName::forLive(), WorkspaceRoleAssignment::createForGroup('Neos.Neos:LivePublisher', WorkspaceRole::COLLABORATOR)); + $this->outputLine('Done'); } } diff --git a/Neos.Neos/Classes/Domain/Service/SiteService.php b/Neos.Neos/Classes/Domain/Service/SiteService.php index c437b57b219..66bcb9fd356 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteService.php @@ -64,6 +64,12 @@ class SiteService */ protected $assetCollectionRepository; + /** + * @Flow\Inject + * @var WorkspaceService + */ + protected $workspaceService; + /** * Remove given site all nodes for that site and all domains associated. */ @@ -71,7 +77,7 @@ public function pruneSite(Site $site): void { $siteServiceInternals = $this->contentRepositoryRegistry->buildService( $site->getConfiguration()->contentRepositoryId, - new SiteServiceInternalsFactory() + new SiteServiceInternalsFactory($this->workspaceService) ); try { @@ -176,7 +182,7 @@ public function createSite( $siteServiceInternals = $this->contentRepositoryRegistry->buildService( $site->getConfiguration()->contentRepositoryId, - new SiteServiceInternalsFactory() + new SiteServiceInternalsFactory($this->workspaceService) ); $siteServiceInternals->createSiteNodeIfNotExists($site, $nodeTypeName); diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php index 3337686ebeb..9fa6e4a8201 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php @@ -23,13 +23,14 @@ use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant; +use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; -use Neos\ContentRepository\Core\Service\ContentRepositoryBootstrapper; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Neos\Domain\Exception\SiteNodeTypeIsInvalid; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Model\SiteNodeName; @@ -40,6 +41,7 @@ public function __construct( private ContentRepository $contentRepository, private InterDimensionalVariationGraph $interDimensionalVariationGraph, private NodeTypeManager $nodeTypeManager, + private WorkspaceService $workspaceService, ) { } @@ -81,12 +83,9 @@ public function removeSiteNode(SiteNodeName $siteNodeName): void public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): void { - $bootstrapper = ContentRepositoryBootstrapper::create($this->contentRepository); - $liveWorkspace = $bootstrapper->getOrCreateLiveWorkspace(); - $sitesNodeIdentifier = $bootstrapper->getOrCreateRootNodeAggregate( - $liveWorkspace, - NodeTypeNameFactory::forSites() - ); + $this->workspaceService->createLiveWorkspaceIfMissing($this->contentRepository->id); + + $sitesNodeIdentifier = $this->getOrCreateRootNodeAggregate(); $siteNodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); if (!$siteNodeType) { throw new NodeTypeNotFound( @@ -99,7 +98,7 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi throw SiteNodeTypeIsInvalid::becauseItIsNotOfTypeSite(NodeTypeName::fromString($nodeTypeName)); } - $siteNodeAggregate = $this->contentRepository->getContentGraph($liveWorkspace->workspaceName) + $siteNodeAggregate = $this->contentRepository->getContentGraph(WorkspaceName::forLive()) ->findChildNodeAggregateByName( $sitesNodeIdentifier, $site->getNodeName()->toNodeName(), @@ -114,7 +113,7 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi $siteNodeAggregateId = NodeAggregateId::create(); $this->contentRepository->handle(CreateNodeAggregateWithNode::create( - $liveWorkspace->workspaceName, + WorkspaceName::forLive(), $siteNodeAggregateId, NodeTypeName::fromString($nodeTypeName), OriginDimensionSpacePoint::fromDimensionSpacePoint($arbitraryRootDimensionSpacePoint), @@ -128,11 +127,33 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi // Handle remaining root dimension space points by creating peer variants foreach ($rootDimensionSpacePoints as $rootDimensionSpacePoint) { $this->contentRepository->handle(CreateNodeVariant::create( - $liveWorkspace->workspaceName, + WorkspaceName::forLive(), $siteNodeAggregateId, OriginDimensionSpacePoint::fromDimensionSpacePoint($arbitraryRootDimensionSpacePoint), OriginDimensionSpacePoint::fromDimensionSpacePoint($rootDimensionSpacePoint), )); } } + + /** + * Retrieve the root Node Aggregate ID for the specified $workspace + * If no root node of the specified $rootNodeTypeName exist, it will be created + */ + private function getOrCreateRootNodeAggregate(): NodeAggregateId + { + $rootNodeTypeName = NodeTypeNameFactory::forSites(); + $rootNodeAggregate = $this->contentRepository->getContentGraph(WorkspaceName::forLive())->findRootNodeAggregateByType( + $rootNodeTypeName + ); + if ($rootNodeAggregate !== null) { + return $rootNodeAggregate->nodeAggregateId; + } + $rootNodeAggregateId = NodeAggregateId::create(); + $this->contentRepository->handle(CreateRootNodeAggregateWithNode::create( + WorkspaceName::forLive(), + $rootNodeAggregateId, + $rootNodeTypeName, + )); + return $rootNodeAggregateId; + } } diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php index 123b324f754..a73348d24af 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php @@ -22,12 +22,18 @@ */ class SiteServiceInternalsFactory implements ContentRepositoryServiceFactoryInterface { + public function __construct( + private readonly WorkspaceService $workspaceService, + ) { + } + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): SiteServiceInternals { return new SiteServiceInternals( $serviceFactoryDependencies->contentRepository, $serviceFactoryDependencies->interDimensionalVariationGraph, $serviceFactoryDependencies->nodeTypeManager, + $this->workspaceService, ); } } diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 5f854bc2689..3948d95b82b 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -133,6 +133,22 @@ public function createRootWorkspace(ContentRepositoryId $contentRepositoryId, Wo $this->addWorkspaceMetadata($contentRepositoryId, $workspaceName, $title, $description, WorkspaceClassification::ROOT, null); } + /** + * Create the "live" root workspace with the default role assignment (users with the role "Neos.Neos:LivePublisher" are collaborators) + */ + public function createLiveWorkspaceIfMissing(ContentRepositoryId $contentRepositoryId): void + { + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + $workspaceName = WorkspaceName::forLive(); + $liveWorkspace = $contentRepository->findWorkspaceByName($workspaceName); + if ($liveWorkspace !== null) { + // live workspace already exists + return; + } + $this->createRootWorkspace($contentRepositoryId, $workspaceName, WorkspaceTitle::fromString('Public live workspace'), WorkspaceDescription::empty()); + $this->assignWorkspaceRole($contentRepositoryId, $workspaceName, WorkspaceRoleAssignment::createForGroup('Neos.Neos:LivePublisher', WorkspaceRole::COLLABORATOR)); + } + /** * Create a new, personal, workspace for the specified user */