Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8742756

Browse filesBrowse files
natewiebe13Nate Wiebe
authored and
Nate Wiebe
committed
[Security][SecurityBundle] User authorization checker
1 parent 73d4904 commit 8742756
Copy full SHA for 8742756

File tree

13 files changed

+316
-1
lines changed
Filter options

13 files changed

+316
-1
lines changed

‎src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Add `Security::userIsGranted()` to test user authorization without relying on the session. For example, users not currently logged in, or while processing a message from a message queue
8+
49
7.2
510
---
611

‎src/Symfony/Bundle/SecurityBundle/Resources/config/security.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security.php
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
3232
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
3333
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
34+
use Symfony\Component\Security\Core\Authorization\UserAuthorizationChecker;
35+
use Symfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface;
3436
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
3537
use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter;
3638
use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter;
@@ -67,6 +69,12 @@
6769
])
6870
->alias(AuthorizationCheckerInterface::class, 'security.authorization_checker')
6971

72+
->set('security.user_authorization_checker', UserAuthorizationChecker::class)
73+
->args([
74+
service('security.access.decision_manager'),
75+
])
76+
->alias(UserAuthorizationCheckerInterface::class, 'security.user_authorization_checker')
77+
7078
->set('security.token_storage', UsageTrackingTokenStorage::class)
7179
->args([
7280
service('security.untracked_token_storage'),
@@ -85,6 +93,7 @@
8593
service_locator([
8694
'security.token_storage' => service('security.token_storage'),
8795
'security.authorization_checker' => service('security.authorization_checker'),
96+
'security.user_authorization_checker' => service('security.user_authorization_checker'),
8897
'security.authenticator.managers_locator' => service('security.authenticator.managers_locator')->ignoreOnInvalid(),
8998
'request_stack' => service('request_stack'),
9099
'security.firewall.map' => service('security.firewall.map'),

‎src/Symfony/Bundle/SecurityBundle/Security.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Security.php
+34-1Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,27 @@
1313

1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
16+
use Symfony\Component\DependencyInjection\ServiceLocator;
1617
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpFoundation\RequestStack;
1719
use Symfony\Component\HttpFoundation\Response;
1820
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1921
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
2022
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
23+
use Symfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface;
2124
use Symfony\Component\Security\Core\Exception\LogicException;
2225
use Symfony\Component\Security\Core\Exception\LogoutException;
26+
use Symfony\Component\Security\Core\User\UserCheckerInterface;
2327
use Symfony\Component\Security\Core\User\UserInterface;
2428
use Symfony\Component\Security\Csrf\CsrfToken;
29+
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
2530
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
2631
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
2732
use Symfony\Component\Security\Http\Event\LogoutEvent;
33+
use Symfony\Component\Security\Http\FirewallMapInterface;
2834
use Symfony\Component\Security\Http\ParameterBagUtils;
2935
use Symfony\Contracts\Service\ServiceProviderInterface;
36+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
3037

3138
/**
3239
* Helper class for commonly-needed security tasks.
@@ -37,7 +44,7 @@
3744
*
3845
* @final
3946
*/
40-
class Security implements AuthorizationCheckerInterface
47+
class Security implements AuthorizationCheckerInterface, ServiceSubscriberInterface, UserAuthorizationCheckerInterface
4148
{
4249
public function __construct(
4350
private readonly ContainerInterface $container,
@@ -148,6 +155,17 @@ public function logout(bool $validateCsrfToken = true): ?Response
148155
return $logoutEvent->getResponse();
149156
}
150157

158+
/**
159+
* Checks if the attribute is granted against the user and optionally supplied subject.
160+
*
161+
* This should be used over isGranted() when checking permissions against a user that is not currently logged in or while in a CLI context.
162+
*/
163+
public function userIsGranted(UserInterface $user, mixed $attribute, mixed $subject = null): bool
164+
{
165+
return $this->container->get('security.user_authorization_checker')
166+
->userIsGranted($user, $attribute, $subject);
167+
}
168+
151169
private function getAuthenticator(?string $authenticatorName, string $firewallName): AuthenticatorInterface
152170
{
153171
if (!isset($this->authenticators[$firewallName])) {
@@ -182,4 +200,19 @@ private function getAuthenticator(?string $authenticatorName, string $firewallNa
182200

183201
return $firewallAuthenticatorLocator->get($authenticatorId);
184202
}
203+
204+
public static function getSubscribedServices(): array
205+
{
206+
return [
207+
'security.token_storage' => TokenStorageInterface::class,
208+
'security.authorization_checker' => AuthorizationCheckerInterface::class,
209+
'security.user_authorization_checker' => UserAuthorizationCheckerInterface::class,
210+
'security.authenticator.managers_locator' => '?'.ServiceProviderInterface::class,
211+
'request_stack' => RequestStack::class,
212+
'security.firewall.map' => FirewallMapInterface::class,
213+
'security.user_checker' => UserCheckerInterface::class,
214+
'security.firewall.event_dispatcher_locator' => ServiceLocator::class,
215+
'security.csrf.token_manager' => '?'.CsrfTokenManagerInterface::class,
216+
];
217+
}
185218
}

‎src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php
+18Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ public function testServiceIsFunctional()
4747
$this->assertSame('main', $firewallConfig->getName());
4848
}
4949

50+
public function testUserAuthorizationChecker()
51+
{
52+
$kernel = self::createKernel(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
53+
$kernel->boot();
54+
$container = $kernel->getContainer();
55+
56+
$loggedInUser = new InMemoryUser('foo', 'pass', ['ROLE_USER', 'ROLE_FOO']);
57+
$offlineUser = new InMemoryUser('bar', 'pass', ['ROLE_USER', 'ROLE_BAR']);
58+
$token = new UsernamePasswordToken($loggedInUser, 'provider', $loggedInUser->getRoles());
59+
$container->get('functional.test.security.token_storage')->setToken($token);
60+
61+
$security = $container->get('functional_test.security.helper');
62+
$this->assertTrue($security->isGranted('ROLE_FOO'));
63+
$this->assertFalse($security->isGranted('ROLE_BAR'));
64+
$this->assertTrue($security->userIsGranted($offlineUser, 'ROLE_BAR'));
65+
$this->assertFalse($security->userIsGranted($offlineUser, 'ROLE_FOO'));
66+
}
67+
5068
/**
5169
* @dataProvider userWillBeMarkedAsChangedIfRolesHasChangedProvider
5270
*/
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Authentication\Token;
13+
14+
/**
15+
* Interface used for marking tokens that do not represent the currently logged-in user.
16+
*
17+
* @author Nate Wiebe <nate@northern.co>
18+
*/
19+
interface OfflineTokenInterface extends TokenInterface
20+
{
21+
}
+31Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Authentication\Token;
13+
14+
use Symfony\Component\Security\Core\User\UserInterface;
15+
16+
/**
17+
* UserAuthorizationCheckerToken implements a token used for checking authorization.
18+
*
19+
* @author Nate Wiebe <nate@northern.co>
20+
*
21+
* @internal
22+
*/
23+
final class UserAuthorizationCheckerToken extends AbstractToken implements OfflineTokenInterface
24+
{
25+
public function __construct(UserInterface $user)
26+
{
27+
parent::__construct($user->getRoles());
28+
29+
$this->setUser($user);
30+
}
31+
}
+31Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Authorization;
13+
14+
use Symfony\Component\Security\Core\Authentication\Token\UserAuthorizationCheckerToken;
15+
use Symfony\Component\Security\Core\User\UserInterface;
16+
17+
/**
18+
* @author Nate Wiebe <nate@northern.co>
19+
*/
20+
final class UserAuthorizationChecker implements UserAuthorizationCheckerInterface
21+
{
22+
public function __construct(
23+
private readonly AccessDecisionManagerInterface $accessDecisionManager,
24+
) {
25+
}
26+
27+
public function userIsGranted(UserInterface $user, mixed $attribute, mixed $subject = null): bool
28+
{
29+
return $this->accessDecisionManager->decide(new UserAuthorizationCheckerToken($user), [$attribute], $subject);
30+
}
31+
}
+29Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Authorization;
13+
14+
use Symfony\Component\Security\Core\User\UserInterface;
15+
16+
/**
17+
* Interface is used to check user authorization without a session.
18+
*
19+
* @author Nate Wiebe <nate@northern.co>
20+
*/
21+
interface UserAuthorizationCheckerInterface
22+
{
23+
/**
24+
* Checks if the attribute is granted against the user and optionally supplied subject.
25+
*
26+
* @param mixed $attribute A single attribute to vote on (can be of any type, string and instance of Expression are supported by the core)
27+
*/
28+
public function userIsGranted(UserInterface $user, mixed $attribute, mixed $subject = null): bool;
29+
}

‎src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Component\Security\Core\Authorization\Voter;
1313

1414
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
15+
use Symfony\Component\Security\Core\Authentication\Token\OfflineTokenInterface;
1516
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
1617
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
18+
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
1719

1820
/**
1921
* AuthenticatedVoter votes if an attribute like IS_AUTHENTICATED_FULLY,
@@ -54,6 +56,10 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes):
5456
continue;
5557
}
5658

59+
if ($token instanceof OfflineTokenInterface) {
60+
throw new InvalidArgumentException('Cannot decide on authentication attributes when an offline token is used.');
61+
}
62+
5763
$result = VoterInterface::ACCESS_DENIED;
5864

5965
if (self::IS_AUTHENTICATED_FULLY === $attribute

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/CHANGELOG.md
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Add `UserAuthorizationChecker::userIsGranted()` to test user authorization without relying on the session.
8+
For example, users not currently logged in, or while processing a message from a message queue.
9+
* Add `OfflineTokenInterface` to mark tokens that do not represent the currently logged-in user
10+
411
7.2
512
---
613

+26Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Tests\Authentication\Token;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Security\Core\Authentication\Token\UserAuthorizationCheckerToken;
16+
use Symfony\Component\Security\Core\User\InMemoryUser;
17+
18+
class UserAuthorizationCheckerTokenTest extends TestCase
19+
{
20+
public function testConstructor()
21+
{
22+
$token = new UserAuthorizationCheckerToken($user = new InMemoryUser('foo', 'bar', ['ROLE_FOO']));
23+
$this->assertSame(['ROLE_FOO'], $token->getRoleNames());
24+
$this->assertSame($user, $token->getUser());
25+
}
26+
}
+70Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Tests\Authorization;
13+
14+
use PHPUnit\Framework\MockObject\MockObject;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Security\Core\Authentication\Token\UserAuthorizationCheckerToken;
17+
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
18+
use Symfony\Component\Security\Core\Authorization\UserAuthorizationChecker;
19+
use Symfony\Component\Security\Core\User\InMemoryUser;
20+
21+
class UserAuthorizationCheckerTest extends TestCase
22+
{
23+
private AccessDecisionManagerInterface&MockObject $accessDecisionManager;
24+
private UserAuthorizationChecker $authorizationChecker;
25+
26+
protected function setUp(): void
27+
{
28+
$this->accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class);
29+
30+
$this->authorizationChecker = new UserAuthorizationChecker($this->accessDecisionManager);
31+
}
32+
33+
/**
34+
* @dataProvider isGrantedProvider
35+
*/
36+
public function testIsGranted(bool $decide, array $roles)
37+
{
38+
$user = new InMemoryUser('username', 'password', $roles);
39+
40+
$this->accessDecisionManager
41+
->expects($this->once())
42+
->method('decide')
43+
->with($this->callback(fn (UserAuthorizationCheckerToken $token): bool => $user === $token->getUser()), $this->identicalTo(['ROLE_FOO']))
44+
->willReturn($decide);
45+
46+
$this->assertSame($decide, $this->authorizationChecker->userIsGranted($user, 'ROLE_FOO'));
47+
}
48+
49+
public static function isGrantedProvider(): array
50+
{
51+
return [
52+
[false, ['ROLE_USER']],
53+
[true, ['ROLE_USER', 'ROLE_FOO']],
54+
];
55+
}
56+
57+
public function testIsGrantedWithObjectAttribute()
58+
{
59+
$attribute = new \stdClass();
60+
61+
$token = new UserAuthorizationCheckerToken(new InMemoryUser('username', 'password', ['ROLE_USER']));
62+
63+
$this->accessDecisionManager
64+
->expects($this->once())
65+
->method('decide')
66+
->with($this->isInstanceOf($token::class), $this->identicalTo([$attribute]))
67+
->willReturn(true);
68+
$this->assertTrue($this->authorizationChecker->userIsGranted($token->getUser(), $attribute));
69+
}
70+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.