Skip to content

Commit

Permalink
Simplify ContentRepositoryAuthorizationService API
Browse files Browse the repository at this point in the history
according to review comments
  • Loading branch information
bwaidelich committed Nov 11, 2024
1 parent 8569804 commit 466c89e
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 174 deletions.
10 changes: 2 additions & 8 deletions Neos.Media.Browser/Classes/Controller/UsageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
use Neos\Flow\Security\Context as SecurityContext;
use Neos\Media\Domain\Model\AssetInterface;
use Neos\Media\Domain\Service\AssetService;
use Neos\Neos\AssetUsage\Dto\AssetUsageReference;
use Neos\Neos\Domain\Repository\SiteRepository;
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\Service\UserService;
use Neos\Neos\AssetUsage\Dto\AssetUsageReference;

/**
* Controller for asset usage handling
Expand Down Expand Up @@ -117,13 +117,7 @@ public function relatedNodesAction(AssetInterface $asset)
);
$nodeType = $nodeAggregate ? $contentRepository->getNodeTypeManager()->getNodeType($nodeAggregate->nodeTypeName) : null;

$authenticatedAccount = $this->securityContext->getAccount();
if ($authenticatedAccount !== null) {
$workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($currentContentRepositoryId, $usage->getWorkspaceName(), $authenticatedAccount);
} else {
$workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAnonymousUser($currentContentRepositoryId, $usage->getWorkspaceName());
}

$workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissions($currentContentRepositoryId, $usage->getWorkspaceName(), $this->securityContext->getRoles(), $this->userService->getBackendUser()?->getId());
$workspace = $contentRepository->findWorkspaceByName($usage->getWorkspaceName());

$inaccessibleRelation['nodeIdentifier'] = $usage->getNodeAggregateId()->value;
Expand Down
7 changes: 1 addition & 6 deletions Neos.Neos/Classes/Controller/Frontend/NodeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,7 @@ public function showAction(string $node): void
}

$contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId);
$authenticatedAccount = $this->securityContext->getAccount();
if ($authenticatedAccount !== null) {
$visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForAccount($contentRepository->id, $authenticatedAccount);
} else {
$visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForAnonymousUser($contentRepository->id);
}
$visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraints($contentRepository->id, $this->securityContext->getRoles());
// By default, the visibility constraints only contain the SubtreeTags the authenticated user has _no_ access to
// Neos backend users have access to the "disabled" SubtreeTag so that they can see/edit disabled nodes.
// In this showAction (= "frontend") we have to explicitly remove those disabled nodes, even if the user was authenticated,
Expand Down
12 changes: 2 additions & 10 deletions Neos.Neos/Classes/Domain/Model/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,13 @@ public function setPreferences(UserPreferences $preferences)
* @api
*/
public function isActive()
{
return $this->getFirstActiveAccount() !== null;
}

/**
* @api
*/
public function getFirstActiveAccount(): ?Account
{
foreach ($this->accounts as $account) {
/** @var Account $account */
if ($account->isActive()) {
return $account;
return true;
}
}
return null;
return false;
}
}
4 changes: 2 additions & 2 deletions Neos.Neos/Classes/Domain/Service/WorkspaceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId,
/**
* Get all role assignments for the specified workspace
*
* NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAccount()} and {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAnonymousUser()} should be used!
* NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissions()} should be used!
*/
public function getWorkspaceRoleAssignments(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspaceRoleAssignments
{
Expand Down Expand Up @@ -272,7 +272,7 @@ public function getWorkspaceRoleAssignments(ContentRepositoryId $contentReposito
/**
* Get the role with the most privileges for the specified {@see WorkspaceRoleSubjects} on workspace $workspaceName
*
* NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAccount()} and {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAnonymousUser()} should be used!
* NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissions()} should be used!
*/
public function getMostPrivilegedWorkspaceRoleForSubjects(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjects $subjects): ?WorkspaceRole
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Security\Account;
use Neos\Flow\Security\Authorization\PrivilegeManagerInterface;
use Neos\Flow\Security\Context;
use Neos\Flow\Security\Policy\PolicyService;
use Neos\Flow\Security\Policy\Role;
use Neos\Neos\Domain\Model\NodePermissions;
use Neos\Neos\Domain\Model\User;
use Neos\Neos\Domain\Model\UserId;
use Neos\Neos\Domain\Model\WorkspacePermissions;
use Neos\Neos\Domain\Model\WorkspaceRole;
use Neos\Neos\Domain\Model\WorkspaceRoleSubject;
Expand All @@ -24,7 +24,6 @@
use Neos\Neos\Security\Authorization\Privilege\EditNodePrivilege;
use Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege;
use Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilegeSubject;
use Neos\Party\Domain\Service\PartyService;

/**
* Central point which does ContentRepository authorization decisions within Neos.
Expand All @@ -34,133 +33,71 @@
#[Flow\Scope('singleton')]
final readonly class ContentRepositoryAuthorizationService
{
private const FLOW_ROLE_EVERYBODY = 'Neos.Flow:Everybody';
private const FLOW_ROLE_ANONYMOUS = 'Neos.Flow:Anonymous';
private const FLOW_ROLE_AUTHENTICATED_USER = 'Neos.Flow:AuthenticatedUser';
private const FLOW_ROLE_NEOS_ADMINISTRATOR = 'Neos.Neos:Administrator';
private const ROLE_NEOS_ADMINISTRATOR = 'Neos.Neos:Administrator';

public function __construct(
private PartyService $partyService,
private WorkspaceService $workspaceService,
private PolicyService $policyService,
private PrivilegeManagerInterface $privilegeManager,
) {
}

/**
* Determines the {@see WorkspacePermissions} an anonymous user has for the specified workspace (aka "public access")
* Determines the {@see WorkspacePermissions} a user with the specified {@see Role}s has for the specified workspace
*
* @param array<Role> $roles The {@see Role} instances to check access for. Note: These have to be the expanded roles auf the authenticated tokens {@see Context::getRoles()}
* @param UserId|null $userId Optional ID of the authenticated Neos user. If set the workspace owner is evaluated since owners always have all permissions on their workspace
*/
public function getWorkspacePermissionsForAnonymousUser(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspacePermissions
{
$subjects = [WorkspaceRoleSubject::createForGroup(self::FLOW_ROLE_EVERYBODY), WorkspaceRoleSubject::createForGroup(self::FLOW_ROLE_ANONYMOUS)];
$userWorkspaceRole = $this->workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects));
if ($userWorkspaceRole === null) {
return WorkspacePermissions::none(sprintf('Anonymous user has no explicit role for workspace "%s"', $workspaceName->value));
}
return WorkspacePermissions::create(
read: $userWorkspaceRole->isAtLeast(WorkspaceRole::VIEWER),
write: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR),
manage: $userWorkspaceRole->isAtLeast(WorkspaceRole::MANAGER),
reason: sprintf('Anonymous user has role "%s" for workspace "%s"', $userWorkspaceRole->value, $workspaceName->value),
);
}

/**
* Determines the {@see WorkspacePermissions} the given user has for the specified workspace
*/
public function getWorkspacePermissionsForAccount(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, Account $account): WorkspacePermissions
public function getWorkspacePermissions(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, array $roles, UserId|null $userId): WorkspacePermissions
{
$workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspaceName);
$neosUser = $this->neosUserFromAccount($account);
if ($workspaceMetadata->ownerUserId !== null && $neosUser !== null && $neosUser->getId()->equals($workspaceMetadata->ownerUserId)) {
return WorkspacePermissions::all(sprintf('User "%s" (id: %s is the owner of workspace "%s"', $neosUser->getLabel(), $neosUser->getId()->value, $workspaceName->value));
if ($userId !== null && $workspaceMetadata->ownerUserId !== null && $userId->equals($workspaceMetadata->ownerUserId)) {
return WorkspacePermissions::all(sprintf('User with id "%s" is the owner of workspace "%s"', $userId->value, $workspaceName->value));
}
$userRoles = $this->expandAccountRoles($account);
$userIsAdministrator = array_key_exists(self::FLOW_ROLE_NEOS_ADMINISTRATOR, $userRoles);
$subjects = array_map(WorkspaceRoleSubject::createForGroup(...), array_keys($userRoles));

if ($neosUser !== null) {
$subjects[] = WorkspaceRoleSubject::createForUser($neosUser->getId());
$roleIdentifiers = array_map(static fn (Role $role) => $role->getIdentifier(), $roles);
$subjects = array_map(WorkspaceRoleSubject::createForGroup(...), $roleIdentifiers);
if ($userId !== null) {
$subjects[] = WorkspaceRoleSubject::createForUser($userId);
}
$userIsAdministrator = array_key_exists(self::ROLE_NEOS_ADMINISTRATOR, $roleIdentifiers);

$userWorkspaceRole = $this->workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects));
if ($userWorkspaceRole === null) {
if ($userIsAdministrator) {
return WorkspacePermissions::manage(sprintf('Account "%s" is a Neos Administrator without explicit role for workspace "%s"', $account->getAccountIdentifier(), $workspaceName->value));
return WorkspacePermissions::manage(sprintf('User is a Neos Administrator without explicit role for workspace "%s"', $workspaceName->value));
}
return WorkspacePermissions::none(sprintf('Account "%s" is no Neos Administrator and has no explicit role for workspace "%s"', $account->getAccountIdentifier(), $workspaceName->value));
return WorkspacePermissions::none(sprintf('User is no Neos Administrator and has no explicit role for workspace "%s"', $workspaceName->value));
}
return WorkspacePermissions::create(
read: $userWorkspaceRole->isAtLeast(WorkspaceRole::VIEWER),
write: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR),
manage: $userIsAdministrator || $userWorkspaceRole->isAtLeast(WorkspaceRole::MANAGER),
reason: sprintf('Account "%s" is %s Neos Administrator and has role "%s" for workspace "%s"', $account->getAccountIdentifier(), $userIsAdministrator ? 'a' : 'no', $userWorkspaceRole->value, $workspaceName->value),
reason: sprintf('User is %s Neos Administrator and has role "%s" for workspace "%s"', $userIsAdministrator ? 'a' : 'no', $userWorkspaceRole->value, $workspaceName->value),
);
}

public function getNodePermissionsForAnonymousUser(Node $node): NodePermissions
{
$roles = $this->rolesOfAnonymousUser();
return $this->nodePermissionsForRoles($node, $roles);
}

public function getNodePermissionsForAccount(Node $node, Account $account): NodePermissions
{
$roles = $this->expandAccountRoles($account);
return $this->nodePermissionsForRoles($node, $roles);
}

/**
* Determines the default {@see VisibilityConstraints} for an anonymous user (aka "public access")
* Determines the {@see NodePermissions} a user with the specified {@see Role}s has on the given {@see Node}
*
* @param array<Role> $roles
*/
public function getVisibilityConstraintsForAnonymousUser(ContentRepositoryId $contentRepositoryId): VisibilityConstraints
public function getNodePermissions(Node $node, array $roles): NodePermissions
{
$roles = $this->rolesOfAnonymousUser();
return VisibilityConstraints::fromTagConstraints($this->tagConstraintsForRoles($contentRepositoryId, $roles));
return $this->nodePermissionsForRoles($node, $roles);
}

/**
* Determines the default {@see VisibilityConstraints} for the specified account
* Determines the default {@see VisibilityConstraints} for the specified {@see Role}s
*
* @param array<Role> $roles
*/
public function getVisibilityConstraintsForAccount(ContentRepositoryId $contentRepositoryId, Account $account): VisibilityConstraints
public function getVisibilityConstraints(ContentRepositoryId $contentRepositoryId, array $roles): VisibilityConstraints
{
$roles = $this->expandAccountRoles($account);
return VisibilityConstraints::fromTagConstraints($this->tagConstraintsForRoles($contentRepositoryId, $roles));
}

// ------------------------------

/**
* @return array<Role>
*/
private function rolesOfAnonymousUser(): array
{
return [
self::FLOW_ROLE_EVERYBODY => $this->policyService->getRole(self::FLOW_ROLE_EVERYBODY),
self::FLOW_ROLE_ANONYMOUS => $this->policyService->getRole(self::FLOW_ROLE_ANONYMOUS),
];
}

/**
* @return array<Role>
*/
private function expandAccountRoles(Account $account): array
{
$roles = [
self::FLOW_ROLE_EVERYBODY => $this->policyService->getRole(self::FLOW_ROLE_EVERYBODY),
self::FLOW_ROLE_AUTHENTICATED_USER => $this->policyService->getRole(self::FLOW_ROLE_AUTHENTICATED_USER),
];
foreach ($account->getRoles() as $currentRole) {
if (!array_key_exists($currentRole->getIdentifier(), $roles)) {
$roles[$currentRole->getIdentifier()] = $currentRole;
}
foreach ($currentRole->getAllParentRoles() as $currentParentRole) {
if (!array_key_exists($currentParentRole->getIdentifier(), $roles)) {
$roles[$currentParentRole->getIdentifier()] = $currentParentRole;
}
}
}
return $roles;
}

/**
* @param array<Role> $roles
Expand All @@ -177,12 +114,6 @@ private function tagConstraintsForRoles(ContentRepositoryId $contentRepositoryId
return $restrictedSubtreeTags;
}

private function neosUserFromAccount(Account $account): ?User
{
$user = $this->partyService->getAssignedPartyOfAccount($account);
return $user instanceof User ? $user : null;
}

/**
* @param array<Role> $roles
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,20 @@ public function getAuthenticatedUserId(): ?UserId

public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints
{
$authenticatedAccount = $this->securityContext->getAccount();
if ($authenticatedAccount) {
return $this->authorizationService->getVisibilityConstraintsForAccount($this->contentRepositoryId, $authenticatedAccount);
}
return $this->authorizationService->getVisibilityConstraintsForAnonymousUser($this->contentRepositoryId);
return $this->authorizationService->getVisibilityConstraints($this->contentRepositoryId, $this->securityContext->getRoles());
}

public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privilege
{
if ($this->securityContext->areAuthorizationChecksDisabled()) {
return Privilege::granted('Authorization checks are disabled');
}
$authenticatedAccount = $this->securityContext->getAccount();
if ($authenticatedAccount === null) {
$workspacePermissions = $this->authorizationService->getWorkspacePermissionsForAnonymousUser($this->contentRepositoryId, $workspaceName);
} else {
$workspacePermissions = $this->authorizationService->getWorkspacePermissionsForAccount($this->contentRepositoryId, $workspaceName, $authenticatedAccount);
}
$workspacePermissions = $this->authorizationService->getWorkspacePermissions(
$this->contentRepositoryId,
$workspaceName,
$this->securityContext->getRoles(),
$this->userService->getCurrentUser()?->getId(),
);
return $workspacePermissions->read ? Privilege::granted($workspacePermissions->getReason()) : Privilege::denied($workspacePermissions->getReason());
}

Expand All @@ -117,7 +113,7 @@ public function canExecuteCommand(CommandInterface $command): Privilege
if ($node === null) {
return Privilege::denied(sprintf('Failed to load node "%s" in workspace "%s"', $nodeThatRequiresEditPrivilege->aggregateId->value, $nodeThatRequiresEditPrivilege->workspaceName->value));
}
$nodePermissions = $this->getNodePermissionsForCurrentUser($node);
$nodePermissions = $this->authorizationService->getNodePermissions($node, $this->securityContext->getRoles());
if (!$nodePermissions->edit) {
return Privilege::denied(sprintf('No edit permissions for node "%s" in workspace "%s": %s', $nodeThatRequiresEditPrivilege->aggregateId->value, $nodeThatRequiresEditPrivilege->workspaceName->value, $nodePermissions->getReason()));
}
Expand Down Expand Up @@ -199,19 +195,11 @@ private function requireWorkspaceManagePermission(WorkspaceName $workspaceName):

private function getWorkspacePermissionsForCurrentUser(WorkspaceName $workspaceName): WorkspacePermissions
{
$authenticatedAccount = $this->securityContext->getAccount();
if ($authenticatedAccount === null) {
return $this->authorizationService->getWorkspacePermissionsForAnonymousUser($this->contentRepositoryId, $workspaceName);
}
return $this->authorizationService->getWorkspacePermissionsForAccount($this->contentRepositoryId, $workspaceName, $authenticatedAccount);
}

private function getNodePermissionsForCurrentUser(Node $node): NodePermissions
{
$authenticatedAccount = $this->securityContext->getAccount();
if ($authenticatedAccount === null) {
return $this->authorizationService->getNodePermissionsForAnonymousUser($node);
}
return $this->authorizationService->getNodePermissionsForAccount($node, $authenticatedAccount);
return $this->authorizationService->getWorkspacePermissions(
$this->contentRepositoryId,
$workspaceName,
$this->securityContext->getRoles(),
$this->userService->getCurrentUser()?->getId(),
);
}
}
Loading

0 comments on commit 466c89e

Please sign in to comment.