Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
bwaidelich committed Oct 8, 2024
1 parent 8bafb5a commit f68300a
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 25 deletions.
139 changes: 137 additions & 2 deletions Neos.Neos/Classes/Command/WorkspaceCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ public function rebaseCommand(string $workspace, string $contentRepository = 'de
}

/**
* Create a new root workspace for a content repository.
* Create a new root workspace for a content repository
*
* NOTE: By default, only administrators can access workspaces without role assignments. Use <i>workspace:assignrole</i> to add workspace permissions
*
* @param string $name Name of the new root
* @param string $contentRepository Identifier of the content repository. (Default: 'default')
Expand Down Expand Up @@ -162,7 +164,8 @@ public function createRootCommand(string $name, string $contentRepository = 'def
* @param string $contentRepository Identifier of the content repository. (Default: 'default')
* @throws StopCommandException
*/
public function createPersonalCommand(string $workspace, string $owner, string $baseWorkspace = 'live', string $title = null, string $description = null, string $contentRepository = 'default'): void {
public function createPersonalCommand(string $workspace, string $owner, string $baseWorkspace = 'live', string $title = null, string $description = null, string $contentRepository = 'default'): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$workspaceOwner = $this->userService->getUser($owner);
if ($workspaceOwner === null) {
Expand All @@ -184,6 +187,8 @@ public function createPersonalCommand(string $workspace, string $owner, string $
/**
* Create a new shared workspace
*
* NOTE: By default, only administrators can access workspaces without role assignments. Use <i>workspace:assignrole</i> to add workspace permissions
*
* @param string $workspace Name of the workspace, for example "christmas-campaign"
* @param string $baseWorkspace Name of the base workspace. If none is specified, "live" is assumed.
* @param string|null $title Human friendly title of the workspace, for example "Christmas Campaign"
Expand All @@ -205,6 +210,136 @@ public function createSharedCommand(string $workspace, string $baseWorkspace = '
$this->outputLine('<success>Created shared workspace "%s"</success>', [$workspaceName->value]);
}

/**
* Set/change the title of a workspace
*
* @param string $workspace Name of the workspace, for example "some-workspace"
* @param string $newTitle Human friendly title of the workspace, for example "Some workspace"
* @param string $contentRepository Identifier of the content repository. (Default: 'default')
* @throws StopCommandException
*/
public function setTitleCommand(string $workspace, string $newTitle, string $contentRepository = 'default'): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$workspaceName = WorkspaceName::fromString($workspace);
$this->workspaceService->setWorkspaceTitle(
$contentRepositoryId,
$workspaceName,
WorkspaceTitle::fromString($newTitle),
);
$this->outputLine('<success>Set title of workspace "%s" to "%s"</success>', [$workspaceName->value, $newTitle]);
}

/**
* Set/change the description of a workspace
*
* @param string $workspace Name of the workspace, for example "some-workspace"
* @param string $newDescription Human friendly description of the workspace
* @param string $contentRepository Identifier of the content repository. (Default: 'default')
* @throws StopCommandException
*/
public function setDescriptionCommand(string $workspace, string $newDescription, string $contentRepository = 'default'): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$workspaceName = WorkspaceName::fromString($workspace);
$this->workspaceService->setWorkspaceDescription(
$contentRepositoryId,
$workspaceName,
WorkspaceDescription::fromString($newDescription),
);
$this->outputLine('<success>Set description of workspace "%s"</success>', [$workspaceName->value]);
}

/**
* Assign a workspace role to the given user/user group
*
* Without explicit workspace roles, only administrators can change the corresponding workspace.
* With this command, a user or group (represented by a Flow role identifier) can be granted one of the two roles:
* - collaborator: Can read from and write to the workspace
* - manager: Can read from and write to the workspace and manage it (i.e. change metadata & role assignments)
*
* Examples:
*
* To grant editors read and write access to a (shared) workspace: <i>./flow workspace:assignrole some-workspace "Neos.Neos:AbstractEditor" collaborator</i>
*
* To grant a specific user read, write and manage access to a workspace: <i>./flow workspace:assignrole some-workspace admin manager --type user</i>
*
* {@see WorkspaceRole}
*
* @param string $workspace Name of the workspace, for example "some-workspace"
* @param string $subject The user/group that should be assigned. By default, this is expected to be a Flow role identifier (e.g. 'Neos.Neos:AbstractEditor') – if $type is 'user', this is the username (aka account identifier) of a Neos user
* @param string $role Role to assign, either 'collaborator' or 'manager' – a collaborator can read and write from/to the workspace. A manager can _on top_ change the workspace metadata & roles itself
* @param string $contentRepository Identifier of the content repository. (Default: 'default')
* @param string $type Type of role, either 'group' (default) or 'user' – if 'group', $subject is expected to be a Flow role identifier, otherwise the username (aka account identifier) of a Neos user
* @throws StopCommandException
*/
public function assignRoleCommand(string $workspace, string $subject, string $role, string $contentRepository = 'default', string $type = 'group'): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$workspaceName = WorkspaceName::fromString($workspace);

$subjectType = match ($type) {
'group' => WorkspaceRoleSubjectType::GROUP,
'user' => WorkspaceRoleSubjectType::USER,
default => throw new \InvalidArgumentException(sprintf('type must be "group" or "user", given "%s"', $type), 1728398802),
};
$workspaceRole = match ($role) {
'collaborator' => WorkspaceRole::COLLABORATOR,
'manager' => WorkspaceRole::MANAGER,
default => throw new \InvalidArgumentException(sprintf('role must be "collaborator" or "manager", given "%s"', $role), 1728398880),
};
if ($subjectType === WorkspaceRoleSubjectType::USER) {
$neosUser = $this->userService->getUser($subject);
if ($neosUser === null) {
$this->outputLine('<error>The user "%s" specified as subject does not exist</error>', [$subject]);
$this->quit(1);
}
$roleSubject = WorkspaceRoleSubject::fromString($neosUser->getId()->value);
} else {
$roleSubject = WorkspaceRoleSubject::fromString($subject);
}
$this->workspaceService->assignWorkspaceRole(
$contentRepositoryId,
$workspaceName,
$subjectType,
$roleSubject,
$workspaceRole,
);
$this->outputLine('<success>Assigned role "%s" to subject "%s" for workspace "%s"</success>', [$workspaceRole->value, $roleSubject->value, $workspaceName->value]);
}

/**
* Unassign a workspace role from the given user/user group
*
* @see assignRoleCommand()
*
* @param string $workspace Name of the workspace, for example "some-workspace"
* @param string $subject The user/group that should be unassigned. By default, this is expected to be a Flow role identifier (e.g. 'Neos.Neos:AbstractEditor') – if $type is 'user', this is the username (aka account identifier) of a Neos user
* @param string $contentRepository Identifier of the content repository. (Default: 'default')
* @param string $type Type of role, either 'group' (default) or 'user' – if 'group', $subject is expected to be a Flow role identifier, otherwise the username (aka account identifier) of a Neos user
* @throws StopCommandException
*/
public function unassignRoleCommand(string $workspace, string $subject, string $contentRepository = 'default', string $type = 'group'): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$workspaceName = WorkspaceName::fromString($workspace);

$subjectType = match ($type) {
'group' => WorkspaceRoleSubjectType::GROUP,
'user' => WorkspaceRoleSubjectType::USER,
default => throw new \InvalidArgumentException(sprintf('type must be "group" or "user", given "%s"', $type), 1728398802),
};
$roleSubject = WorkspaceRoleSubject::fromString($subject);
$this->workspaceService->unassignWorkspaceRole(
$contentRepositoryId,
$workspaceName,
$subjectType,
$roleSubject,
);
$this->outputLine('<success>Removed role assignment from subject "%s" for workspace "%s"</success>', [$roleSubject->value, $workspaceName->value]);
}


/**
* Deletes a workspace
*
Expand Down
4 changes: 2 additions & 2 deletions Neos.Neos/Classes/Domain/Model/WorkspaceRole.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
*/
enum WorkspaceRole : string
{
case COLLABORATOR = 'COLLABORATOR';
case MANAGER = 'MANAGER';
case COLLABORATOR = 'COLLABORATOR'; // Can read from and write to the workspace
case MANAGER = 'MANAGER'; // Can read from and write to the workspace and manage it (i.e. change metadata & role assignments)

public function isAtLeast(self $role): bool
{
Expand Down
87 changes: 69 additions & 18 deletions Neos.Neos/Classes/Domain/Service/WorkspaceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
use Neos\Neos\Domain\Model\WorkspaceTitle;

/**
* Central authority to interact with Content Repository Workspaces within Neos
*
* @api
*/
#[Flow\Scope('singleton')]
Expand All @@ -58,6 +60,8 @@ public function __construct(

/**
* Load metadata for the specified workspace
*
* Note: If no metadata exists for the specified workspace, an instance with classification {@see WorkspaceClassification::UNKNOWN} is returned!
*/
public function getWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspaceMetadata
{
Expand All @@ -73,13 +77,23 @@ public function getWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, W
}

/**
* Change title and/or description metadata for the specified workspace
* Update/set title metadata for the specified workspace
*/
public function updateWorkspaceTitleAndDescription(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $newWorkspaceTitle, WorkspaceDescription $newWorkspaceDescription): void
public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $newWorkspaceTitle): void
{
$this->requireWorkspace($contentRepositoryId, $workspaceName);
$this->updateWorkspaceMetadata($contentRepositoryId, $workspaceName, [
'title' => $newWorkspaceTitle->value,
]);
}

/**
* Update/set description metadata for the specified workspace
*/
public function setWorkspaceDescription(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceDescription $newWorkspaceDescription): void
{
$this->requireWorkspace($contentRepositoryId, $workspaceName);
$this->updateWorkspaceMetadata($contentRepositoryId, $workspaceName, [
'description' => $newWorkspaceDescription->value,
]);
}
Expand Down Expand Up @@ -154,6 +168,48 @@ public function createPersonalWorkspaceForUserIfMissing(ContentRepositoryId $con
);
}

/**
* Assign a workspace role to the given user/user group
*
* Without explicit workspace roles, only administrators can change the corresponding workspace.
* With this method, the subject (i.e. a Neos user or group represented by a Flow role identifier) can be granted a {@see WorkspaceRole} for the specified workspace
*/
public function assignWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjectType $subjectType, WorkspaceRoleSubject $subject, WorkspaceRole $role): void
{
$this->requireWorkspace($contentRepositoryId, $workspaceName);
try {
$this->dbal->insert(self::TABLE_NAME_WORKSPACE_ROLE, [
'content_repository_id' => $contentRepositoryId->value,
'workspace_name' => $workspaceName->value,
'subject_type' => $subjectType->value,
'subject' => $subject->value,
'role' => $role->value,
]);
} catch (DbalException $e) {
throw new \RuntimeException(sprintf('Failed to assign role for workspace "%s" to subject "%s" (Content Repository "%s"): %s', $workspaceName->value, $subject->value, $contentRepositoryId->value, $e->getMessage()), 1728396138, $e);
}
}

/**
* Remove a workspace role assignment for the given subject
*
* @see self::assignWorkspaceRole()
*/
public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjectType $subjectType, WorkspaceRoleSubject $subject): void
{
$this->requireWorkspace($contentRepositoryId, $workspaceName);
try {
$this->dbal->delete(self::TABLE_NAME_WORKSPACE_ROLE, [
'content_repository_id' => $contentRepositoryId->value,
'workspace_name' => $workspaceName->value,
'subject_type' => $subjectType->value,
'subject' => $subject->value,
]);
} catch (DbalException $e) {
throw new \RuntimeException(sprintf('Failed to unassign role for subject "%s" from workspace "%s" (Content Repository "%s"): %s', $subject->value, $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1728396169, $e);
}
}

/**
* Determines the permission the given user has for the specified workspace {@see WorkspacePermissions}
*/
Expand Down Expand Up @@ -247,10 +303,20 @@ private function loadWorkspaceMetadata(ContentRepositoryId $contentRepositoryId,
private function updateWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, array $data): void
{
try {
$this->dbal->update(self::TABLE_NAME_WORKSPACE_METADATA, $data, [
$affectedRows = $this->dbal->update(self::TABLE_NAME_WORKSPACE_METADATA, $data, [
'content_repository_id' => $contentRepositoryId->value,
'workspace_name' => $workspaceName->value,
]);
if ($affectedRows === 0) {
$this->dbal->insert(self::TABLE_NAME_WORKSPACE_METADATA, [
'content_repository_id' => $contentRepositoryId->value,
'workspace_name' => $workspaceName->value,
'description' => '',
'title' => '',
'classification' => WorkspaceClassification::UNKNOWN->value,
...$data,
]);
}
} catch (DbalException $e) {
throw new \RuntimeException(sprintf('Failed to update metadata for workspace "%s" (Content Repository "%s"): %s', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1726821159, $e);
}
Expand Down Expand Up @@ -308,21 +374,6 @@ private function findPrimaryWorkspaceNameForUser(ContentRepositoryId $contentRep
return $workspaceName === false ? null : WorkspaceName::fromString($workspaceName);
}

public function addWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjectType $subjectType, WorkspaceRoleSubject $subject, WorkspaceRole $role): void
{
try {
$this->dbal->insert(self::TABLE_NAME_WORKSPACE_ROLE, [
'content_repository_id' => $contentRepositoryId->value,
'workspace_name' => $workspaceName->value,
'subject_type' => $subjectType->value,
'subject' => $subject->value,
'role' => $role->value,
]);
} catch (DbalException $e) {
throw new \RuntimeException(sprintf('Failed to add metadata for workspace "%s" (Content Repository "%s"): %s', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1727084068, $e);
}
}

private function loadWorkspaceRoleOfUser(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, User $user): ?WorkspaceRole
{
try {
Expand Down
10 changes: 7 additions & 3 deletions Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,14 @@ public function createAction(
);
$this->redirect('new');
}
$this->workspaceService->addWorkspaceRole(
$this->workspaceService->assignWorkspaceRole(
$contentRepositoryId,
$workspaceName,
WorkspaceRoleSubjectType::USER,
WorkspaceRoleSubject::fromString($currentUser->getId()->value),
WorkspaceRole::MANAGER,
);
$this->workspaceService->addWorkspaceRole(
$this->workspaceService->assignWorkspaceRole(
$contentRepositoryId,
$workspaceName,
WorkspaceRoleSubjectType::GROUP,
Expand Down Expand Up @@ -297,10 +297,14 @@ public function updateAction(
);
$this->redirect('index');
}
$this->workspaceService->updateWorkspaceTitleAndDescription(
$this->workspaceService->setWorkspaceTitle(
$contentRepositoryId,
$workspaceName,
$title,
);
$this->workspaceService->setWorkspaceDescription(
$contentRepositoryId,
$workspaceName,
$description,
);
$this->addFlashMessage($this->translator->translateById(
Expand Down

0 comments on commit f68300a

Please sign in to comment.