Skip to content

Commit 3edca67

Browse files
feature #46907 [Security] Add #[IsGranted()] (nicolas-grekas)
This PR was merged into the 6.2 branch. Discussion ---------- [Security] Add `#[IsGranted()]` | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Part of #44705 | License | MIT | Doc PR | - Extracted from #45415 (and modernized a lot). I did not implement the proposals from Stof to keep this first iteration simple. I'd appreciate help to improve the attribute in a follow up PR 🙏 Commits ------- bf8d75e [Security] Add `#[IsGranted()]`
2 parents f89876e + bf8d75e commit 3edca67

File tree

9 files changed

+523
-4
lines changed

9 files changed

+523
-4
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator;
4343
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
4444
use Symfony\Component\Security\Http\Controller\UserValueResolver;
45+
use Symfony\Component\Security\Http\EventListener\IsGrantedAttributeListener;
4546
use Symfony\Component\Security\Http\Firewall;
4647
use Symfony\Component\Security\Http\FirewallMapInterface;
4748
use Symfony\Component\Security\Http\HttpUtils;
@@ -269,5 +270,9 @@
269270
service('security.expression_language'),
270271
])
271272
->tag('kernel.cache_warmer')
273+
274+
->set('controller.is_granted_attribute_listener', IsGrantedAttributeListener::class)
275+
->args([service('security.authorization_checker')])
276+
->tag('kernel.event_subscriber')
272277
;
273278
};

src/Symfony/Bundle/SecurityBundle/composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
"php": ">=8.1",
2020
"composer-runtime-api": ">=2.1",
2121
"ext-xml": "*",
22-
"symfony/config": "^5.4|^6.0",
23-
"symfony/dependency-injection": "^5.4|^6.0",
22+
"symfony/config": "^6.1",
23+
"symfony/dependency-injection": "^6.1",
2424
"symfony/event-dispatcher": "^5.4|^6.0",
25-
"symfony/http-kernel": "^5.4|^6.0",
25+
"symfony/http-kernel": "^6.2",
2626
"symfony/http-foundation": "^5.4|^6.0",
2727
"symfony/password-hasher": "^5.4|^6.0",
2828
"symfony/security-core": "^5.4|^6.0",
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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\Http\Attribute;
13+
14+
/**
15+
* @author Ryan Weaver <ryan@knpuniversity.com>
16+
*/
17+
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
18+
class IsGranted
19+
{
20+
public function __construct(
21+
/**
22+
* Sets the first argument that will be passed to isGranted().
23+
*/
24+
public array|string|null $attributes = null,
25+
26+
/**
27+
* Sets the second argument passed to isGranted().
28+
*/
29+
public array|string|null $subject = null,
30+
31+
/**
32+
* The message of the exception - has a nice default if not set.
33+
*/
34+
public ?string $message = null,
35+
36+
/**
37+
* If set, will throw HttpKernel's HttpException with the given $statusCode.
38+
* If null, Security\Core's AccessDeniedException will be used.
39+
*/
40+
public ?int $statusCode = null,
41+
) {
42+
}
43+
}

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

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

77
* Add maximum username length enforcement of 4096 characters in `UserBadge`
8+
* Add `#[IsGranted()]`
89

910
6.0
1011
---
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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\Http\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
16+
use Symfony\Component\HttpKernel\Exception\HttpException;
17+
use Symfony\Component\HttpKernel\KernelEvents;
18+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
19+
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
20+
use Symfony\Component\Security\Http\Attribute\IsGranted;
21+
22+
/**
23+
* Handles the IsGranted attribute on controllers.
24+
*
25+
* @author Ryan Weaver <ryan@knpuniversity.com>
26+
*/
27+
class IsGrantedAttributeListener implements EventSubscriberInterface
28+
{
29+
public function __construct(
30+
private AuthorizationCheckerInterface $authChecker,
31+
) {
32+
}
33+
34+
public function onKernelControllerArguments(ControllerArgumentsEvent $event)
35+
{
36+
/** @var IsGranted[] $attributes */
37+
if (!\is_array($attributes = $event->getAttributes()[IsGranted::class] ?? null)) {
38+
return;
39+
}
40+
41+
$namedArguments = [];
42+
$arguments = $event->getArguments();
43+
$r = $event->getRequest()->attributes->get('_controller_reflectors')[1] ?? new \ReflectionFunction($event->getController());
44+
45+
foreach ($r->getParameters() as $i => $param) {
46+
if ($param->isVariadic()) {
47+
$namedArguments[$param->name] = \array_slice($arguments, $i);
48+
break;
49+
}
50+
if (\array_key_exists($i, $arguments)) {
51+
$namedArguments[$param->name] = $arguments[$i];
52+
}
53+
}
54+
55+
foreach ($attributes as $attribute) {
56+
$subjectRef = $attribute->subject;
57+
$subject = null;
58+
59+
if ($subjectRef) {
60+
if (\is_array($subjectRef)) {
61+
foreach ($subjectRef as $ref) {
62+
if (!\array_key_exists($ref, $namedArguments)) {
63+
throw new \RuntimeException(sprintf('Could not find the subject "%s" for the #[IsGranted] attribute. Try adding a "$%s" argument to your controller method.', $ref, $ref));
64+
}
65+
$subject[$ref] = $namedArguments[$ref];
66+
}
67+
} elseif (!\array_key_exists($subjectRef, $namedArguments)) {
68+
throw new \RuntimeException(sprintf('Could not find the subject "%s" for the #[IsGranted] attribute. Try adding a "$%s" argument to your controller method.', $subjectRef, $subjectRef));
69+
} else {
70+
$subject = $namedArguments[$subjectRef];
71+
}
72+
}
73+
74+
if (!$this->authChecker->isGranted($attribute->attributes, $subject)) {
75+
$message = $attribute->message ?: sprintf('Access Denied by #[IsGranted(%s)] on controller', $this->getIsGrantedString($attribute));
76+
77+
if ($statusCode = $attribute->statusCode) {
78+
throw new HttpException($statusCode, $message);
79+
}
80+
81+
$accessDeniedException = new AccessDeniedException($message);
82+
$accessDeniedException->setAttributes($attribute->attributes);
83+
$accessDeniedException->setSubject($subject);
84+
85+
throw $accessDeniedException;
86+
}
87+
}
88+
}
89+
90+
public static function getSubscribedEvents(): array
91+
{
92+
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 10]];
93+
}
94+
95+
private function getIsGrantedString(IsGranted $isGranted): string
96+
{
97+
$attributes = array_map(fn ($attribute) => '"'.$attribute.'"', (array) $isGranted->attributes);
98+
$argsString = 1 === \count($attributes) ? reset($attributes) : '['.implode(', ', $attributes).']';
99+
100+
if (null !== $isGranted->subject) {
101+
$argsString .= ', "'.implode('", "', (array) $isGranted->subject).'"';
102+
}
103+
104+
return $argsString;
105+
}
106+
}

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