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

BUGFIX: WorkspaceService adjust to real world use cases #5334

Merged
merged 25 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d5300e2
TASK: Introduce utility methods for #5132
mhsdesign Oct 31, 2024
1404d37
TASK: Introduce `WorkspaceService::deleteWorkspace`
mhsdesign Dec 5, 2024
909223e
TASK: Test `ChangeBaseWorkspace` behaviour with security
mhsdesign Dec 6, 2024
9755c9d
TASK: Ensure the live workspace is created by the WorkspaceService fo…
mhsdesign Dec 6, 2024
0257b28
TASK: Ensure the live workspace is created by the WorkspaceService fo…
mhsdesign Dec 6, 2024
d514e61
TASK: Allow workspace creation with only base workspace read privilege
mhsdesign Dec 6, 2024
484c1ab
BUGFIX: Only allow to publish if the current user can write to the base
mhsdesign Dec 6, 2024
74cfaaa
TASK: Simplify test to only have one `uninvolved_editor`
mhsdesign Dec 6, 2024
a7d887f
BUGFIX: Ensure that the live workspace is created WITH public viewing…
mhsdesign Dec 6, 2024
28b98c5
TASK: Add test for not authenticated case
mhsdesign Dec 7, 2024
8ee8521
TASK: Fix workspace permission test to no be in authorizationChecksDi…
mhsdesign Dec 7, 2024
6351ce6
TASK: Use `deleteWorkspace` from service in cli command and test
mhsdesign Dec 9, 2024
06348d5
TASK: Throw specific `WorkspaceDoesNotExist` in workspace service to …
mhsdesign Dec 9, 2024
6d0a149
BUGFIX: Allow to specify `WorkspaceRoleAssignments` when creating a n…
mhsdesign Dec 9, 2024
3c678dd
TASK: Document how Neos.Neos:LivePublisher and Neos.Neos:AbstractEdit…
mhsdesign Dec 9, 2024
1e5dae6
BUGFIX: Fix root workspace creation to specify assignments
mhsdesign Dec 9, 2024
a36e564
TASK: Test current state of ordering if multiple workspaces exist for…
mhsdesign Dec 9, 2024
94c4065
TASK: Add test to ensure personal workspace names are always unique
mhsdesign Dec 9, 2024
099274a
TASK: Document and cleanup `createRootWorkspace` and `createSharedWor…
mhsdesign Dec 9, 2024
31473fb
BUGFIX: Ensure for now that the api does not allow two personal works…
mhsdesign Dec 9, 2024
5cc7e0f
TASK: Apply suggestions from review
mhsdesign Dec 10, 2024
3f65a74
BUGFIX: Use correct error code to assert
dlubitz Dec 10, 2024
29a9b6c
Merge remote-tracking branch 'origin/9.0' into bugfix/workspace-servi…
mhsdesign Dec 12, 2024
ddccba1
TASK: Ensure that the workspace owner is unique via sql index
mhsdesign Dec 12, 2024
08d01e1
TASK: Make workspace role and metadata creation atomic
mhsdesign Dec 12, 2024
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 @@ -14,18 +14,28 @@

namespace Neos\ContentRepository\Core\SharedModel\Exception;

use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;

/**
* @api because exception is thrown during invariant checks on command execution
* @api because exception is thrown during invariant checks on command execution or when attempting to query a non-existing workspace
*/
final class WorkspaceDoesNotExist extends \DomainException
{
public static function butWasSupposedTo(WorkspaceName $name): self
{
return new self(sprintf(
'The source workspace %s does not exist',
'The workspace "%s" does not exist',
$name->value
), 1513924741);
}

public static function butWasSupposedToInContentRepository(WorkspaceName $name, ContentRepositoryId $contentRepositoryId): self
{
return new self(sprintf(
'The workspace "%s" does not exist in content repository "%s"',
$name->value,
$contentRepositoryId->value
), 1733737361);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ public function migrateWorkspaceMetadataToWorkspaceService(\Closure $outputFn):
];
$roleAssignments[] = [
'subject_type' => WorkspaceRoleSubjectType::GROUP->value,
'subject' => 'Neos.Neos:Everybody',
'subject' => 'Neos.Flow:Everybody',
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
'role' => WorkspaceRole::VIEWER->value,
];
} elseif ($isInternalWorkspace) {
Expand Down
12 changes: 5 additions & 7 deletions Neos.Neos/Classes/Command/WorkspaceCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
namespace Neos\Neos\Command;

use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Exception\WorkspaceAlreadyExists;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\DeleteWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed;
use Neos\ContentRepository\Core\Service\WorkspaceMaintenanceServiceFactory;
Expand All @@ -31,6 +30,7 @@
use Neos\Neos\Domain\Model\WorkspaceDescription;
use Neos\Neos\Domain\Model\WorkspaceRole;
use Neos\Neos\Domain\Model\WorkspaceRoleAssignment;
use Neos\Neos\Domain\Model\WorkspaceRoleAssignments;
use Neos\Neos\Domain\Model\WorkspaceRoleSubject;
use Neos\Neos\Domain\Model\WorkspaceRoleSubjectType;
use Neos\Neos\Domain\Model\WorkspaceTitle;
Expand Down Expand Up @@ -148,7 +148,8 @@ public function createRootCommand(string $name, string $contentRepository = 'def
$contentRepositoryId,
$workspaceName,
WorkspaceTitle::fromString($title ?? $name),
WorkspaceDescription::fromString($description ?? '')
WorkspaceDescription::fromString($description ?? ''),
WorkspaceRoleAssignments::createEmpty()
);
$this->outputLine('<success>Created root workspace "%s" in content repository "%s"</success>', [$workspaceName->value, $contentRepositoryId->value]);
}
Expand Down Expand Up @@ -206,6 +207,7 @@ public function createSharedCommand(string $workspace, string $baseWorkspace = '
WorkspaceTitle::fromString($title ?? $workspaceName->value),
WorkspaceDescription::fromString($description ?? ''),
WorkspaceName::fromString($baseWorkspace),
WorkspaceRoleAssignments::createEmpty()
);
$this->outputLine('<success>Created shared workspace "%s"</success>', [$workspaceName->value]);
}
Expand Down Expand Up @@ -400,11 +402,7 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
$this->workspacePublishingService->discardAllWorkspaceChanges($contentRepositoryId, $workspaceName);
}

$contentRepositoryInstance->handle(
DeleteWorkspace::create(
$workspaceName
)
);
$this->workspaceService->deleteWorkspace($contentRepositoryId, $workspaceName);
$this->outputLine('Deleted workspace "%s"', [$workspaceName->value]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
use Neos\ContentRepository\Export\ProcessorInterface;
use Neos\ContentRepository\Export\Severity;
use Neos\Neos\Domain\Model\WorkspaceDescription;
use Neos\Neos\Domain\Model\WorkspaceRole;
use Neos\Neos\Domain\Model\WorkspaceRoleAssignment;
use Neos\Neos\Domain\Model\WorkspaceRoleAssignments;
use Neos\Neos\Domain\Model\WorkspaceTitle;
use Neos\Neos\Domain\Service\WorkspaceService;

Expand All @@ -44,7 +43,12 @@ public function run(ProcessingContext $context): void
$context->dispatch(Severity::NOTICE, 'Workspace already exists, skipping');
return;
}
$this->workspaceService->createRootWorkspace($this->contentRepository->id, WorkspaceName::forLive(), WorkspaceTitle::fromString('Live workspace'), WorkspaceDescription::fromString(''));
$this->workspaceService->assignWorkspaceRole($this->contentRepository->id, WorkspaceName::forLive(), WorkspaceRoleAssignment::createForGroup('Neos.Neos:LivePublisher', WorkspaceRole::COLLABORATOR));
$this->workspaceService->createRootWorkspace(
$this->contentRepository->id,
WorkspaceName::forLive(),
WorkspaceTitle::fromString('Public live workspace'),
WorkspaceDescription::empty(),
WorkspaceRoleAssignments::createForLiveWorkspace()
);
}
}
6 changes: 6 additions & 0 deletions Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignment.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,10 @@ public static function createForGroup(string $flowRoleIdentifier, WorkspaceRole
$role
);
}

public function equals(WorkspaceRoleAssignment $other): bool
{
return $this->subject->equals($other->subject)
&& $this->role === $other->role;
}
}
64 changes: 64 additions & 0 deletions Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignments.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Neos\Neos\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Neos\Neos\Domain\Service\WorkspaceService;

/**
* A set of {@see WorkspaceRoleAssignment} instances
Expand All @@ -25,6 +26,16 @@ private function __construct(WorkspaceRoleAssignment ...$assignments)
$this->assignments = $assignments;
}

public static function createEmpty(): self
{
return new self();
}

public static function create(WorkspaceRoleAssignment ...$assignments): self
{
return new self(...$assignments);
}

/**
* @param array<WorkspaceRoleAssignment> $assignments
*/
Expand All @@ -33,6 +44,44 @@ public static function fromArray(array $assignments): self
return new self(...$assignments);
}

/**
* Default role assignment to be specified at creation via {@see WorkspaceService::createRootWorkspace()}
*
* Users with the role "Neos.Neos:LivePublisher" are collaborators and everybody can read.
*/
public static function createForLiveWorkspace(): self
{
return new self(
WorkspaceRoleAssignment::createForGroup(
'Neos.Neos:LivePublisher',
WorkspaceRole::COLLABORATOR
),
WorkspaceRoleAssignment::createForGroup(
'Neos.Flow:Everybody',
WorkspaceRole::VIEWER
)
);
}
bwaidelich marked this conversation as resolved.
Show resolved Hide resolved

/**
* Default role assignment to be specified at creation via {@see WorkspaceService::createSharedWorkspace()}
*
* Users with the role "Neos.Neos:AbstractEditor" are collaborators and the specified user is manager
*/
public static function createForSharedWorkspace(UserId $userId): self
{
return new self(
WorkspaceRoleAssignment::createForUser(
$userId,
WorkspaceRole::MANAGER,
),
WorkspaceRoleAssignment::createForGroup(
'Neos.Neos:AbstractEditor',
WorkspaceRole::COLLABORATOR,
)
);
}

public function isEmpty(): bool
{
return $this->assignments === [];
Expand All @@ -47,4 +96,19 @@ public function count(): int
{
return count($this->assignments);
}

public function contains(WorkspaceRoleAssignment $assignment): bool
{
foreach ($this->assignments as $existingAssignment) {
if ($existingAssignment->equals($assignment)) {
return true;
}
}
return false;
}

public function withAssignment(WorkspaceRoleAssignment $assignment): self
{
return new self(...[...$this->assignments, $assignment]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,40 @@ public function getMostPrivilegedWorkspaceRoleForSubjects(ContentRepositoryId $c
return WorkspaceRole::from($role);
}

public function deleteWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): void
{
try {
$this->dbal->delete(self::TABLE_NAME_WORKSPACE_METADATA, [
'content_repository_id' => $contentRepositoryId->value,
'workspace_name' => $workspaceName->value,
]);
} catch (DbalException $e) {
throw new \RuntimeException(sprintf(
'Failed to delete metadata for workspace "%s" (Content Repository "%s"): %s',
$workspaceName->value,
$contentRepositoryId->value,
$e->getMessage()
), 1726821159, $e);
}
}

public function deleteWorkspaceRoleAssignments(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): void
{
try {
$this->dbal->delete(self::TABLE_NAME_WORKSPACE_ROLE, [
'content_repository_id' => $contentRepositoryId->value,
'workspace_name' => $workspaceName->value,
]);
} catch (DbalException $e) {
throw new \RuntimeException(sprintf(
'Failed to delete role assignments for workspace "%s" (Content Repository "%s"): %s',
$workspaceName->value,
$contentRepositoryId->value,
$e->getMessage()
), 1726821159, $e);
}
}

/**
* Removes all workspace metadata records for the specified content repository id
*/
Expand Down Expand Up @@ -306,7 +340,7 @@ public function addWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, W
}
}

public function findPrimaryWorkspaceNameForUser(ContentRepositoryId $contentRepositoryId, UserId $userId): ?WorkspaceName
public function findWorkspaceNameByUser(ContentRepositoryId $contentRepositoryId, UserId $userId): ?WorkspaceName
{
$tableMetadata = self::TABLE_NAME_WORKSPACE_METADATA;
$query = <<<SQL
Expand All @@ -326,4 +360,13 @@ public function findPrimaryWorkspaceNameForUser(ContentRepositoryId $contentRepo
]);
return $workspaceName === false ? null : WorkspaceName::fromString($workspaceName);
}

/**
* @param \Closure(): void $fn
* @return void
*/
public function transactional(\Closure $fn): void
{
$this->dbal->transactional($fn);
}
}
14 changes: 13 additions & 1 deletion Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
use Neos\Neos\Domain\Exception\SiteNodeTypeIsInvalid;
use Neos\Neos\Domain\Model\Site;
use Neos\Neos\Domain\Model\SiteNodeName;
use Neos\Neos\Domain\Model\WorkspaceDescription;
use Neos\Neos\Domain\Model\WorkspaceRoleAssignments;
use Neos\Neos\Domain\Model\WorkspaceTitle;

/**
* @internal FIXME refactor and incorporate into SiteService
Expand Down Expand Up @@ -89,7 +92,16 @@ public function removeSiteNode(SiteNodeName $siteNodeName): void

public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): void
{
$this->workspaceService->createLiveWorkspaceIfMissing($this->contentRepository->id);
$liveWorkspace = $this->contentRepository->findWorkspaceByName(WorkspaceName::forLive());
if ($liveWorkspace === null) {
$this->workspaceService->createRootWorkspace(
$this->contentRepository->id,
WorkspaceName::forLive(),
WorkspaceTitle::fromString('Public live workspace'),
WorkspaceDescription::empty(),
WorkspaceRoleAssignments::createForLiveWorkspace()
);
}

$sitesNodeIdentifier = $this->getOrCreateRootNodeAggregate();
$siteNodeType = $this->nodeTypeManager->getNodeType($nodeTypeName);
Expand Down
Loading
Loading