diff --git a/Neos.Flow/Classes/Security/Context.php b/Neos.Flow/Classes/Security/Context.php index 8957fed657..a2c5ae0a73 100644 --- a/Neos.Flow/Classes/Security/Context.php +++ b/Neos.Flow/Classes/Security/Context.php @@ -21,6 +21,8 @@ use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException; use Neos\Flow\Security\Policy\Role; use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Security\Policy\RoleId; +use Neos\Flow\Security\Policy\RoleIds; use Neos\Flow\Session\SessionManagerInterface; use Neos\Flow\Utility\Algorithms; use Neos\Utility\TypeHandling; @@ -390,7 +392,9 @@ public function getAuthenticationTokensOfType($className) * * The "Neos.Flow:Everybody" roles is always returned. * - * @return Role[] + * Consider using {@see self::getExpandedRoleIds()} instead + * + * @return array * @throws Exception * @throws Exception\NoSuchRoleException * @throws InvalidConfigurationTypeException @@ -405,18 +409,18 @@ public function getRoles() return $this->roles; } - $this->roles = ['Neos.Flow:Everybody' => $this->policyService->getRole('Neos.Flow:Everybody')]; + $this->roles = [RoleId::everybody()->value => $this->policyService->getRole(RoleId::everybody()->value)]; $authenticatedTokens = array_filter($this->getAuthenticationTokens(), static function (TokenInterface $token) { return $token->isAuthenticated(); }); if (empty($authenticatedTokens)) { - $this->roles['Neos.Flow:Anonymous'] = $this->policyService->getRole('Neos.Flow:Anonymous'); + $this->roles[RoleId::anonymous()->value] = $this->policyService->getRole(RoleId::anonymous()->value); return $this->roles; } - $this->roles['Neos.Flow:AuthenticatedUser'] = $this->policyService->getRole('Neos.Flow:AuthenticatedUser'); + $this->roles[RoleId::authenticatedUser()->value] = $this->policyService->getRole(RoleId::authenticatedUser()->value); foreach ($authenticatedTokens as $token) { $account = $token->getAccount(); @@ -430,6 +434,22 @@ public function getRoles() return $this->roles; } + /** + * Returns the role ids of all authenticated accounts, including inherited roles. + * + * If no authenticated roles could be found the "Anonymous" role is returned. + * + * The "Neos.Flow:Everybody" roles is always returned. + **/ + public function getExpandedRoleIds(): RoleIds + { + try { + return RoleIds::fromArray(array_keys($this->getRoles())); + } catch (InvalidConfigurationTypeException | Exception\NoSuchRoleException | Exception $e) { + throw new \RuntimeException(sprintf('Failed to get ids of authenticated accounts: %s', $e->getMessage()), 1731337723, $e); + } + } + /** * Returns true, if at least one of the currently authenticated accounts holds * a role with the given identifier, also recursively. diff --git a/Neos.Flow/Classes/Security/Policy/PolicyService.php b/Neos.Flow/Classes/Security/Policy/PolicyService.php index f2e8228ccb..1aa8127ca0 100644 --- a/Neos.Flow/Classes/Security/Policy/PolicyService.php +++ b/Neos.Flow/Classes/Security/Policy/PolicyService.php @@ -108,14 +108,15 @@ protected function initialize(): void $privilegeTargetsForEverybody = $this->privilegeTargets; $this->roles = []; - $everybodyRole = new Role('Neos.Flow:Everybody', [], (string)($this->policyConfiguration['roles']['Neos.Flow:Everybody']['label'] ?? ''), (string)($this->policyConfiguration['roles']['Neos.Flow:Everybody']['description'] ?? '')); + $everybodyRole = new Role(RoleId::everybody(), [], (string)($this->policyConfiguration['roles']['Neos.Flow:Everybody']['label'] ?? ''), (string)($this->policyConfiguration['roles']['Neos.Flow:Everybody']['description'] ?? '')); $everybodyRole->setAbstract(true); if (isset($this->policyConfiguration['roles'])) { - foreach ($this->policyConfiguration['roles'] as $roleIdentifier => $roleConfiguration) { - if ($roleIdentifier === 'Neos.Flow:Everybody') { + foreach ($this->policyConfiguration['roles'] as $roleIdString => $roleConfiguration) { + $roleId = RoleId::fromString($roleIdString); + if ($roleId->equals(RoleId::everybody())) { $role = $everybodyRole; } else { - $role = new Role($roleIdentifier, [], (string)($roleConfiguration['label'] ?? ''), (string)($roleConfiguration['description'] ?? '')); + $role = new Role($roleId, [], (string)($roleConfiguration['label'] ?? ''), (string)($roleConfiguration['description'] ?? '')); $role->setAbstract((bool)($roleConfiguration['abstract'] ?? false)); } @@ -123,27 +124,27 @@ protected function initialize(): void foreach ($roleConfiguration['privileges'] as $privilegeConfiguration) { $privilegeTargetIdentifier = $privilegeConfiguration['privilegeTarget']; if (!isset($this->privilegeTargets[$privilegeTargetIdentifier])) { - throw new SecurityException(sprintf('privilege target "%s", referenced in role configuration "%s" is not defined!', $privilegeTargetIdentifier, $roleIdentifier), 1395869320); + throw new SecurityException(sprintf('privilege target "%s", referenced in role configuration "%s" is not defined!', $privilegeTargetIdentifier, $roleId->value), 1395869320); } $privilegeTarget = $this->privilegeTargets[$privilegeTargetIdentifier]; if (!isset($privilegeConfiguration['permission'])) { - throw new SecurityException(sprintf('No permission set for privilegeTarget "%s" in Role "%s"', $privilegeTargetIdentifier, $roleIdentifier), 1395869331); + throw new SecurityException(sprintf('No permission set for privilegeTarget "%s" in Role "%s"', $privilegeTargetIdentifier, $roleId->value), 1395869331); } $privilegeParameters = $privilegeConfiguration['parameters'] ?? []; try { $privilege = $privilegeTarget->createPrivilege($privilegeConfiguration['permission'], $privilegeParameters); } catch (\Exception $exception) { - throw new SecurityException(sprintf('Error for privilegeTarget "%s" in Role "%s": %s', $privilegeTargetIdentifier, $roleIdentifier, $exception->getMessage()), 1401886654, $exception); + throw new SecurityException(sprintf('Error for privilegeTarget "%s" in Role "%s": %s', $privilegeTargetIdentifier, $roleId->value, $exception->getMessage()), 1401886654, $exception); } $role->addPrivilege($privilege); - if ($roleIdentifier === 'Neos.Flow:Everybody') { + if ($roleId->equals(RoleId::everybody())) { unset($privilegeTargetsForEverybody[$privilegeTargetIdentifier]); } } } - $this->roles[$roleIdentifier] = $role; + $this->roles[$roleId->value] = $role; } } @@ -215,13 +216,16 @@ protected function initializePrivilegeTargets(): void /** * Checks if a role exists * - * @param string $roleIdentifier The role identifier, format: (:) + * @param RoleId|string $roleIdentifier The role identifier, format: (:) * @return bool * @throws InvalidConfigurationTypeException * @throws SecurityException */ - public function hasRole(string $roleIdentifier): bool + public function hasRole(RoleId|string $roleIdentifier): bool { + if ($roleIdentifier instanceof RoleId) { + $roleIdentifier = $roleIdentifier->value; + } $this->initialize(); return isset($this->roles[$roleIdentifier]); } @@ -229,14 +233,17 @@ public function hasRole(string $roleIdentifier): bool /** * Returns a Role object configured in the PolicyService * - * @param string $roleIdentifier The role identifier of the role, format: (:) + * @param RoleId|string $roleIdentifier The role identifier of the role, format: (:) * @return Role * @throws InvalidConfigurationTypeException * @throws NoSuchRoleException * @throws SecurityException */ - public function getRole(string $roleIdentifier): Role + public function getRole(RoleId|string $roleIdentifier): Role { + if ($roleIdentifier instanceof RoleId) { + $roleIdentifier = $roleIdentifier->value; + } if ($this->hasRole($roleIdentifier)) { return $this->roles[$roleIdentifier]; } diff --git a/Neos.Flow/Classes/Security/Policy/Role.php b/Neos.Flow/Classes/Security/Policy/Role.php index e4ae2790ac..2d1b13a572 100644 --- a/Neos.Flow/Classes/Security/Policy/Role.php +++ b/Neos.Flow/Classes/Security/Policy/Role.php @@ -21,28 +21,10 @@ */ class Role { - private const ROLE_IDENTIFIER_PATTERN = '/^(\w+(?:\.\w+)*)\:(\w+)$/'; // Vendor(.Package)?:RoleName - /** * The identifier of this role - * - * @var string - */ - protected $identifier; - - /** - * The name of this role (without package key) - * - * @var string */ - protected $name; - - /** - * The package key this role belongs to (extracted from the identifier) - * - * @var string - */ - protected $packageKey; + protected RoleId $id; /** * Whether or not the role is "abstract", meaning it can't be assigned to accounts directly but only serves as a "template role" for other roles to inherit from @@ -77,20 +59,18 @@ class Role protected $privileges = []; /** - * @param string $identifier The fully qualified identifier of this role (Vendor.Package:Role) + * @param RoleId|string $id The fully qualified identifier of this role (Vendor.Package:Role) * @param Role[] $parentRoles * @param string $label A label for this role * @param string $description A description on this role */ - public function __construct(string $identifier, array $parentRoles = [], string $label = '', string $description = '') + public function __construct(RoleId|string $id, array $parentRoles = [], string $label = '', string $description = '') { - if (preg_match(self::ROLE_IDENTIFIER_PATTERN, $identifier, $matches) !== 1) { - throw new \InvalidArgumentException('The role identifier must follow the pattern "Vendor.Package:RoleName", but "' . $identifier . '" was given. Please check the code or policy configuration creating or defining this role.', 1365446549); + if (is_string($id)) { + $id = RoleId::fromString($id); } - $this->identifier = $identifier; - $this->packageKey = $matches[1]; - $this->name = $matches[2]; - $this->label = $label ?: $matches[2]; + $this->id = $id; + $this->label = $label ?: $this->id->getName(); $this->description = $description; $this->parentRoles = $parentRoles; } @@ -98,31 +78,39 @@ public function __construct(string $identifier, array $parentRoles = [], string /** * Returns the fully qualified identifier of this role * + * @deprecated with Flow 9.0 – use {@see self::getId()} instead * @return string */ public function getIdentifier(): string { - return $this->identifier; + return $this->id->value; + } + + public function getId(): RoleId + { + return $this->id; } /** * The key of the package that defines this role. * * @return string + * @deprecated with Neos 9.0 – use {@see RoleId::getPackageKey()} instead */ public function getPackageKey(): string { - return $this->packageKey; + return $this->id->getPackageKey(); } /** * The name of this role, being the identifier without the package key. * * @return string + * @deprecated with Neos 9.0 – use {@see RoleId::getName()} instead */ public function getName(): string { - return $this->name; + return $this->id->getName(); } /** @@ -281,7 +269,7 @@ public function addPrivilege(PrivilegeInterface $privilege): void */ public function __toString() { - return $this->identifier; + return $this->id->value; } /** diff --git a/Neos.Flow/Classes/Security/Policy/RoleId.php b/Neos.Flow/Classes/Security/Policy/RoleId.php new file mode 100644 index 0000000000..5e77498c38 --- /dev/null +++ b/Neos.Flow/Classes/Security/Policy/RoleId.php @@ -0,0 +1,76 @@ +(.):, for example "Some.Package:SomeRole" + */ +final readonly class RoleId +{ + private const ROLE_IDENTIFIER_PATTERN = '/^(\w+(?:\.\w+)*)\:(\w+)$/'; // Vendor(.Package)?:RoleName + + private string $packageKey; + private string $name; + + private function __construct( + public string $value, + ) { + if (preg_match(self::ROLE_IDENTIFIER_PATTERN, $value, $matches) !== 1) { + throw new \InvalidArgumentException('The role id must follow the pattern "Vendor.Package:RoleName", but "' . $value . '" was given. Please check the code or policy configuration creating or defining this role.', 1365446549); + } + $this->packageKey = $matches[1]; + $this->name = $matches[2]; + } + + public static function fromString(string $value): self + { + return new self($value); + } + + public static function everybody(): self + { + return new self('Neos.Flow:Everybody'); + } + + public static function anonymous(): self + { + return new self('Neos.Flow:Anonymous'); + } + + public static function authenticatedUser(): self + { + return new self('Neos.Flow:AuthenticatedUser'); + } + + /** + * The package key prefix of the id, e.g. "Some.Package" + */ + public function getPackageKey(): string + { + return $this->packageKey; + } + + /** + * The name suffix of the id without its package key prefix, e.g. "SomeRole" + */ + public function getName(): string + { + return $this->name; + } + + public function equals(self $other): bool + { + return $other->value === $this->value; + } +} diff --git a/Neos.Flow/Classes/Security/Policy/RoleIds.php b/Neos.Flow/Classes/Security/Policy/RoleIds.php new file mode 100644 index 0000000000..a7ae95e117 --- /dev/null +++ b/Neos.Flow/Classes/Security/Policy/RoleIds.php @@ -0,0 +1,63 @@ + + */ +final readonly class RoleIds implements \IteratorAggregate, \Countable +{ + /** + * array + */ + private array $roleIds; + + private function __construct( + RoleId ...$roleIds + ) { + $this->roleIds = $roleIds; + } + + public static function forAnonymousUser(): self + { + return self::fromArray([RoleId::everybody(), RoleId::anonymous()]); + } + + /** + * @param array $roleIds + */ + public static function fromArray(array $roleIds): self + { + $processedIds = []; + foreach ($roleIds as $roleId) { + if (is_string($roleId)) { + $roleId = RoleId::fromString($roleId); + } elseif (!$roleId instanceof RoleId) { + throw new \InvalidArgumentException(sprintf('Expected string or instance of %s, got: %s', RoleId::class, get_debug_type($roleId)), 1731338164); + } + $processedIds[] = $roleId; + } + return new self(...$processedIds); + } + + public function getIterator(): \Traversable + { + yield from $this->roleIds; + } + + public function count(): int + { + return count($this->roleIds); + } + + /** + * @template T + * @param callable(RoleId): T $callback + * @return array + */ + public function map(callable $callback): array + { + return array_map($callback, $this->roleIds); + } +}