Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Introduce RoleId and RoleIds value objects #3415

Open
wants to merge 7 commits into
base: 9.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()->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();
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
33 changes: 20 additions & 13 deletions Neos.Flow/Classes/Security/Policy/PolicyService.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,42 +108,43 @@ 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));
}

if (isset($roleConfiguration['privileges'])) {
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;
}
}

Expand Down Expand Up @@ -215,28 +216,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
50 changes: 19 additions & 31 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 @@ -77,52 +59,58 @@ 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;
}

Copy link
Member

@kitsunet kitsunet Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static function fromRoleId(RoleId $id, array $parentRoles = [], string $label = '', string $description = ''): self
{
return new ($id->value, array $parentRoles = [], string $label = '', string $description = '');
}
public static function fromIdentifierString(string $identifier, array $parentRoles = [], string $label = '', string $description = ''): self
{
return new ($identifier, array $parentRoles = [], string $label = '', string $description = '');
}

And then deprecate the __construct as it is so we can at least open a window for refactoring internally?

Copy link
Member Author

@bwaidelich bwaidelich Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kitsunet mh, Role::fromRoleId() and Role::fromIdentifierString() sounds wrong to me.. But what do you think of

public function __construct(RoleId|string $id, array $parentRoles = [], string $label = '', string $description = '') {
  if (is_string($id)) {
    $id = RoleId::fromString($id);
  }
  // ...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, sounds good! My thought train was not finished really. I was thinking / hoping how we can refactor towards something like RoleIds $parentRoles or somthing, but the static constructors would not necessarily make that easier :D

Copy link
Member Author

@bwaidelich bwaidelich Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree – there is a lot of potential to improve things, but I wanted to avoid another rabbit hole (like #3410)
Adjusted with 32bbd2d

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, totally fair

/**
* 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 Expand Up @@ -281,7 +269,7 @@ public function addPrivilege(PrivilegeInterface $privilege): void
*/
public function __toString()
{
return $this->identifier;
return $this->id->value;
}

/**
Expand Down
76 changes: 76 additions & 0 deletions Neos.Flow/Classes/Security/Policy/RoleId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?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;
}
}
Loading
Loading