diff --git a/Neos.Flow/Classes/Security/Context.php b/Neos.Flow/Classes/Security/Context.php index 8957fed657..45344188a6 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())]; $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()); return $this->roles; } - $this->roles['Neos.Flow:AuthenticatedUser'] = $this->policyService->getRole('Neos.Flow:AuthenticatedUser'); + $this->roles[RoleId::authenticatedUser()->value] = $this->policyService->getRole(RoleId::authenticatedUser()); 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..ae2a5f65d7 100644 --- a/Neos.Flow/Classes/Security/Policy/PolicyService.php +++ b/Neos.Flow/Classes/Security/Policy/PolicyService.php @@ -215,13 +215,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 +232,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..28c1ee7949 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 @@ -84,13 +66,8 @@ class Role */ public function __construct(string $identifier, 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); - } - $this->identifier = $identifier; - $this->packageKey = $matches[1]; - $this->name = $matches[2]; - $this->label = $label ?: $matches[2]; + $this->id = RoleId::fromString($identifier); + $this->label = $label ?: $this->id->getName(); $this->description = $description; $this->parentRoles = $parentRoles; } @@ -98,31 +75,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(); } /** diff --git a/Neos.Flow/Classes/Security/Policy/RoleId.php b/Neos.Flow/Classes/Security/Policy/RoleId.php new file mode 100644 index 0000000000..716af7503e --- /dev/null +++ b/Neos.Flow/Classes/Security/Policy/RoleId.php @@ -0,0 +1,78 @@ +(.):, 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..16d79da032 --- /dev/null +++ b/Neos.Flow/Classes/Security/Policy/RoleIds.php @@ -0,0 +1,67 @@ + + */ +final readonly class RoleIds implements \IteratorAggregate, \Countable +{ + + /** + * array + */ + private array $roleIds; + + /** + * @param 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); + } +}