8000 feature #48142 [Security][SecurityBundle] User authorization checker … · symfony/symfony@4612ff2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4612ff2

Browse files
committed
feature #48142 [Security][SecurityBundle] User authorization checker (natewiebe13)
This PR was merged into the 7.3 branch. Discussion ---------- [Security][SecurityBundle] User authorization checker | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #43372 | License | MIT | Doc PR | symfony/symfony-docs#18033 `isGranted()` assumes that it's checking against the currently logged in user. This provides the same functionality to check against a user during times when there isn't a session (cronjobs/commands, message queue, etc.) or for a different user than the one logged in. Having this functionality allows for removing the dependency on sessions entirely for services reducing the number of issues that come up during a project because some underlying function was session dependent. Commits ------- 096bfaa [Security][SecurityBundle] User authorization checker
2 parents 5c7dac2 + 096bfaa commit 4612ff2

File tree

14 files changed

+331
-2
lines changed

14 files changed

+331
-2
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines 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

Lines 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

Lines changed: 34 additions & 1 deletion
< 10000 td data-grid-cell-id="diff-ab336e5d9ea4cade28c1dce3c8f01a63badb7f8de187acbaac8b8de58c820d40-41-48-0" data-selected="false" role="gridcell" style="background-color:var(--bgColor-default);text-align:center" tabindex="-1" valign="top" class="focusable-grid-cell diff-line-number position-relative diff-line-number-neutral left-side">41
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
48
{
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

Lines 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
*/

src/Symfony/Bundle/SecurityBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"symfony/http-kernel": "^6.4|^7.0",
2727
"symfony/http-foundation": "^6.4|^7.0",
2828
"symfony/password-hasher": "^6.4|^7.0",
29-
"symfony/security-core": "^7.2",
29+
"symfony/security-core": "^7.3",
3030
"symfony/security-csrf": "^6.4|^7.0",
3131
"symfony/security-http": "^7.2",
3232
"symfony/service-contracts": "^2.5|^3"
Lines 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+
}
Lines 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+
}
Lines 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+
}
Lines 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

Lines 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

Lines 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

Lines 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+
}

0 commit comments

Comments
 (0)
0