Skip to content

Commit ae44f50

Browse files
committed
[Security] Handle placeholders in role hierarchy
1 parent abe5555 commit ae44f50

File tree

4 files changed

+103
-14
lines changed

4 files changed

+103
-14
lines changed

src/Symfony/Component/Security/Core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
6.4
55
---
66

7+
* Allow using wildcards as placeholders in `RoleHierarchy` map's keys
78
* Make `PersistentToken` immutable
89
* Deprecate accepting only `DateTime` for `TokenProviderInterface::updateToken()`, use `DateTimeInterface` instead
910

src/Symfony/Component/Security/Core/Role/RoleHierarchy.php

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
class RoleHierarchy implements RoleHierarchyInterface
2020
{
2121
private array $hierarchy;
22+
23+
/**
24+
* Map role placeholders with their regex pattern.
25+
*
26+
* @var array<string,string>
27+
*/
28+
private array $rolePlaceholdersPatterns;
29+
2230
/** @var array<string, list<string>> */
2331
protected $map;
2432

@@ -34,19 +42,7 @@ public function __construct(array $hierarchy)
3442

3543
public function getReachableRoleNames(array $roles): array
3644
{
37-
$reachableRoles = $roles;
38-
39-
foreach ($roles as $role) {
40-
if (!isset($this->map[$role])) {
41-
continue;
42-
}
43-
44-
foreach ($this->map[$role] as $r) {
45-
$reachableRoles[] = $r;
46-
}
47-
}
48-
49-
return array_values(array_unique($reachableRoles));
45+
return $this->resolveReachableRoleNames($roles);
5046
}
5147

5248
/**
@@ -55,6 +51,8 @@ public function getReachableRoleNames(array $roles): array
5551
protected function buildRoleMap()
5652
{
5753
$this->map = [];
54+
$this->rolePlaceholdersPatterns = [];
55+
5856
foreach ($this->hierarchy as $main => $roles) {
5957
$this->map[$main] = $roles;
6058
$visited = [];
@@ -76,6 +74,49 @@ protected function buildRoleMap()
7674
}
7775

7876
$this->map[$main] = array_unique($this->map[$main]);
77+
78+
if (str_contains($main, '*')) {
79+
$this->rolePlaceholdersPatterns[$main] = sprintf('/%s/', strtr($main, ['*' => '[^\*]+']));
80+
}
7981
}
8082
}
83+
84+
private function resolveReachableRoleNames(array $roles, array &$visitedPlaceholders = []): array
85+
{
86+
$reachableRoles = $roles;
87+
88+
foreach ($roles as $role) {
89+
if (!isset($this->map[$role])) {
90+
continue;
91+
}
92+
93+
foreach ($this->map[$role] as $r) {
94+
$reachableRoles[] = $r;
95+
}
96+
}
97+
98+
$placeholderRoles = array_diff($this->getMatchingPlaceholders($reachableRoles), $visitedPlaceholders);
99+
if (!empty($placeholderRoles)) {
100+
array_push($visitedPlaceholders, ...$placeholderRoles);
101+
$resolvedPlaceholderRoles = $this->resolveReachableRoleNames($placeholderRoles, $visitedPlaceholders);
102+
foreach (array_diff($resolvedPlaceholderRoles, $placeholderRoles) as $r) {
103+
$reachableRoles[] = $r;
104+
}
105+
}
106+
107+
return array_values(array_unique($reachableRoles));
108+
}
109+
110+
private function getMatchingPlaceholders(array $roles): array
111+
{
112+
$resolved = [];
113+
114+
foreach ($this->rolePlaceholdersPatterns as $placeholder => $pattern) {
115+
if (!\in_array($placeholder, $resolved) && \count(preg_grep($pattern, $roles) ?? null)) {
116+
$resolved[] = $placeholder;
117+
}
118+
}
119+
120+
return $resolved;
121+
}
81122
}

src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ class RoleHierarchyVoterTest extends RoleVoterTest
2222
*/
2323
public function testVoteUsingTokenThatReturnsRoleNames($roles, $attributes, $expected)
2424
{
25-
$voter = new RoleHierarchyVoter(new RoleHierarchy(['ROLE_FOO' => ['ROLE_FOOBAR']]));
25+
$voter = new RoleHierarchyVoter(new RoleHierarchy([
26+
'ROLE_FOO' => ['ROLE_FOOBAR'],
27+
'ROLE_FOO_*' => ['ROLE_BAR_A', 'ROLE_FOO'],
28+
'ROLE_BAR_*' => ['ROLE_BAZ'],
29+
]));
2630

2731
$this->assertSame($expected, $voter->vote($this->getTokenWithRoleNames($roles), null, $attributes));
2832
}
@@ -31,6 +35,9 @@ public static function getVoteTests()
3135
{
3236
return array_merge(parent::getVoteTests(), [
3337
[['ROLE_FOO'], ['ROLE_FOOBAR'], VoterInterface::ACCESS_GRANTED],
38+
[['ROLE_FOO_A'], ['ROLE_BAR_A'], VoterInterface::ACCESS_GRANTED], // ROLE_FOO_A => ROLE_FOO_* => ROLE_BAR_A
39+
[['ROLE_FOO_A'], ['ROLE_FOOBAR'], VoterInterface::ACCESS_GRANTED], // ROLE_FOO_A => ROLE_FOO_* => ROLE_FOO => ROLE_FOOBAR
40+
[['ROLE_FOO_A'], ['ROLE_BAZ'], VoterInterface::ACCESS_GRANTED], // ROLE_FOO_A => ROLE_FOO_* => ROLE_BAR_A => ROLE_BAR_* => ROLE_BAZ
3441
]);
3542
}
3643

src/Symfony/Component/Security/Core/Tests/Role/RoleHierarchyTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,44 @@ public function testGetReachableRoleNames()
3030
$this->assertEquals(['ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_FOO', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_SUPER_ADMIN']));
3131
$this->assertEquals(['ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_FOO', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_SUPER_ADMIN', 'ROLE_SUPER_ADMIN']));
3232
}
33+
34+
public function testGetReachableRoleNamesWithPlaceholders()
35+
{
36+
$role = new RoleHierarchy([
37+
'ROLE_BAZ_*' => ['ROLE_USER'],
38+
'ROLE_FOO_*' => ['ROLE_BAZ_FOO'],
39+
'ROLE_BAR_*' => ['ROLE_BAZ_BAR'],
40+
]);
41+
42+
$this->assertEquals(['ROLE_BAZ_A', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_BAZ_A']));
43+
$this->assertEquals(['ROLE_FOO_A', 'ROLE_BAZ_FOO', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_FOO_A']));
44+
45+
// Multiple roles matching the same placeholder
46+
$this->assertEquals(['ROLE_FOO_A', 'ROLE_FOO_B', 'ROLE_BAZ_FOO', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_FOO_A', 'ROLE_FOO_B']));
47+
48+
// Multiple roles matching multiple placeholders
49+
$this->assertEquals(['ROLE_FOO_A', 'ROLE_BAR_A', 'ROLE_BAZ_FOO', 'ROLE_BAZ_BAR', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_FOO_A', 'ROLE_BAR_A']));
50+
}
51+
52+
public function testGetReachableRoleNamesWithRecursivePlaceholders()
53+
{
54+
$role = new RoleHierarchy([
55+
'ROLE_FOO_*' => ['ROLE_BAR_BAZ'],
56+
'ROLE_BAR_*' => ['ROLE_FOO_BAZ'],
57+
'ROLE_QUX_*' => ['ROLE_QUX_BAZ'],
58+
]);
59+
60+
// ROLE_FOO_* expanded once
61+
$this->assertEquals(['ROLE_FOO_A', 'ROLE_BAR_BAZ', 'ROLE_FOO_BAZ'], $role->getReachableRoleNames(['ROLE_FOO_A']));
62+
63+
// ROLE_FOO_* expanded once even with multiple ROLE_FOO_* input roles
64+
$this->assertEquals(['ROLE_FOO_A', 'ROLE_FOO_B', 'ROLE_BAR_BAZ', 'ROLE_FOO_BAZ'], $role->getReachableRoleNames(['ROLE_FOO_A', 'ROLE_FOO_B']));
65+
66+
// ROLE_BAR_* expanded once with ROLE_FOO_A => ROLE_FOO_* => ROLE_BAR_BAZ => ROLE_BAR_* => ROLE_FOO_BAZ
67+
$this->assertEquals(['ROLE_FOO_A', 'ROLE_BAR_A', 'ROLE_BAR_BAZ', 'ROLE_FOO_BAZ'], $role->getReachableRoleNames(['ROLE_FOO_A', 'ROLE_BAR_A']));
68+
69+
// Self matching placeholder
70+
$this->assertEquals(['ROLE_QUX_A', 'ROLE_QUX_BAZ'], $role->getReachableRoleNames(['ROLE_QUX_A']));
71+
$this->assertEquals(['ROLE_QUX_BAZ'], $role->getReachableRoleNames(['ROLE_QUX_BAZ']));
72+
}
3373
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy