Skip to content

Commit

Permalink
FEATURE: Introduce RoleId and RoleIds value objects
Browse files Browse the repository at this point in the history
Resolves: #3414
  • Loading branch information
bwaidelich committed Nov 11, 2024
1 parent dfc75e7 commit 1e61872
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 37 deletions.
28 changes: 24 additions & 4 deletions Neos.Flow/Classes/Security/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, Role>
* @throws Exception
* @throws Exception\NoSuchRoleException
* @throws InvalidConfigurationTypeException
Expand All @@ -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();
Expand All @@ -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.
Expand Down
14 changes: 10 additions & 4 deletions Neos.Flow/Classes/Security/Policy/PolicyService.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,28 +215,34 @@ protected function initializePrivilegeTargets(): void
/**
* Checks if a role exists
*
* @param string $roleIdentifier The role identifier, format: (<PackageKey>:)<Role>
* @param RoleId|string $roleIdentifier The role identifier, format: (<PackageKey>:)<Role>
* @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]);
}

/**
* Returns a Role object configured in the PolicyService
*
* @param string $roleIdentifier The role identifier of the role, format: (<PackageKey>:)<Role>
* @param RoleId|string $roleIdentifier The role identifier of the role, format: (<PackageKey>:)<Role>
* @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];
}
Expand Down
43 changes: 14 additions & 29 deletions Neos.Flow/Classes/Security/Policy/Role.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,45 +66,48 @@ 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;
}

/**
* 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();
}

/**
Expand Down
78 changes: 78 additions & 0 deletions Neos.Flow/Classes/Security/Policy/RoleId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);

namespace Neos\Flow\Security\Policy;

/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

/**
* A role identifier in the format <Vendor>(.<Package>):<Role>, 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;
}

}
67 changes: 67 additions & 0 deletions Neos.Flow/Classes/Security/Policy/RoleIds.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);
namespace Neos\Flow\Security\Policy;

/**
* @implements \IteratorAggregate<RoleId>
*/
final readonly class RoleIds implements \IteratorAggregate, \Countable
{

/**
* array<RoleId>
*/
private array $roleIds;

/**
* @param array<RoleId> $roleIds
*/
private function __construct(

Check failure on line 20 in Neos.Flow/Classes/Security/Policy/RoleIds.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test static analysis (deps: highest)

PHPDoc tag @param for parameter $roleIds with type array<Neos\Flow\Security\Policy\RoleId> is incompatible with native type Neos\Flow\Security\Policy\RoleId.
RoleId ...$roleIds
) {
$this->roleIds = $roleIds;
}

public static function forAnonymousUser(): self
{
return self::fromArray([RoleId::everybody(), RoleId::anonymous()]);
}

/**
* @param array<RoleId|string> $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<T>
*/
public function map(callable $callback): array
{
return array_map($callback, $this->roleIds);
}
}

0 comments on commit 1e61872

Please sign in to comment.