Skip to content

Commit bf8d75e

Browse files
[Security] Add #[IsGranted()]
1 parent 7c194b9 commit bf8d75e

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)