Skip to content

Commit 0d47620

Browse files
committed
feature #61359 [Security] Add $methods support to #[IsGranted] to restrict access by HTTP method (santysisi)
This PR was merged into the 7.4 branch. Discussion ---------- [Security] Add `$methods` support to `#[IsGranted]` to restrict access by HTTP method | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | no | License | MIT ### Description This PR adds support for restricting `#[IsGranted]` validation to specific HTTP methods via a new `$methods` argument. ### What's New You can now define access control per HTTP method directly in the `#[IsGranted]` attribute. This allows greater flexibility when securing controller actions that handle multiple HTTP verbs. ```php #[IsGranted('ROLE_ADMIN', methods: ['GET', 'POST'])] public function someAction() {} #[IsGranted('ROLE_ADMIN', methods: 'POST')] public function otherAction() {} ``` * If the current request method does not match, the attribute is ignored. * If the method matches, the usual access check logic runs as expected. This change aligns `#[IsGranted]` more closely with other HTTP-aware attributes like: * `#[IsCsrfTokenValid]` * `#[IsSignatureValid]` (currently under review) Commits ------- 68f0fca [Security] Add `$methods` support to `#[IsGranted]` to restrict access by HTTP method
2 parents 4b58076 + 68f0fca commit 0d47620

File tree

5 files changed

+96
-5
lines changed

5 files changed

+96
-5
lines changed

src/Symfony/Component/Security/Http/Attribute/IsGranted.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,25 @@
2424
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
2525
final class IsGranted
2626
{
27+
/** @var string[] */
28+
public readonly array $methods;
29+
2730
/**
28-
* @param string|Expression|\Closure(IsGrantedContext, mixed $subject):bool $attribute The attribute that will be checked against a given authentication token and optional subject
29-
* @param array|string|Expression|\Closure(array<string,mixed>, Request):mixed|null $subject An optional subject - e.g. the current object being voted on
30-
* @param string|null $message A custom message when access is not granted
31-
* @param int|null $statusCode If set, will throw HttpKernel's HttpException with the given $statusCode; if null, Security\Core's AccessDeniedException will be used
32-
* @param int|null $exceptionCode If set, will add the exception code to thrown exception
31+
* @param string|Expression|\Closure(IsGrantedContext, mixed $subject):bool $attribute The attribute that will be checked against a given authentication token and optional subject
32+
* @param array|string|Expression|\Closure(array<string,mixed>, Request):mixed|null $subject An optional subject - e.g. the current object being voted on
33+
* @param string|null $message A custom message when access is not granted
34+
* @param int|null $statusCode If set, will throw HttpKernel's HttpException with the given $statusCode; if null, Security\Core's AccessDeniedException will be used
35+
* @param int|null $exceptionCode If set, will add the exception code to thrown exception
36+
* @param string[]|string $methods HTTP methods to apply validation to. Empty array means all methods are allowed
3337
*/
3438
public function __construct(
3539
public string|Expression|\Closure $attribute,
3640
public array|string|Expression|\Closure|null $subject = null,
3741
public ?string $message = null,
3842
public ?int $statusCode = null,
3943
public ?int $exceptionCode = null,
44+
array|string $methods = [],
4045
) {
46+
$this->methods = (array) $methods;
4147
}
4248
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add support for union types with `#[CurrentUser]`
88
* Deprecate callable firewall listeners, extend `AbstractListener` or implement `FirewallListenerInterface` instead
99
* Deprecate `AbstractListener::__invoke`
10+
* Add `$methods` argument to `#[IsGranted]` to restrict validation to specific HTTP methods
1011

1112
7.3
1213
---

src/Symfony/Component/Security/Http/EventListener/IsGrantedAttributeListener.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
4747
$arguments = $event->getNamedArguments();
4848

4949
foreach ($attributes as $attribute) {
50+
if ($attribute->methods && !\in_array($request->getMethod(), array_map('strtoupper', $attribute->methods), true)) {
51+
continue;
52+
}
53+
5054
$subject = null;
5155

5256
if ($subjectRef = $attribute->subject) {

src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,4 +454,74 @@ public function testAccessDeniedExceptionWithExceptionCode()
454454

455455
$listener->onKernelControllerArguments($event);
456456
}
457+
458+
public function testThrowsAccessDeniedExceptionWhenMethodMatchesStringConstraint()
459+
{
460+
$authChecker = $this->createMock(AuthorizationCheckerInterface::class);
461+
$authChecker->expects($this->once())->method('isGranted')->willReturn(false);
462+
463+
$event = new ControllerArgumentsEvent(
464+
$this->createMock(HttpKernelInterface::class),
465+
[new IsGrantedAttributeMethodsController(), 'adminWithMethodGet'],
466+
[],
467+
new Request([], [], [], [], [], ['REQUEST_METHOD' => 'GET']),
468+
null
469+
);
470+
471+
$listener = new IsGrantedAttributeListener($authChecker);
472+
$this->expectException(AccessDeniedException::class);
473+
$listener->onKernelControllerArguments($event);
474+
}
475+
476+
public function testThrowsAccessDeniedExceptionWhenMethodMatchesArrayConstraint()
477+
{
478+
$authChecker = $this->createMock(AuthorizationCheckerInterface::class);
479+
$authChecker->expects($this->once())->method('isGranted')->willReturn(false);
480+
481+
$event = new ControllerArgumentsEvent(
482+
$this->createMock(HttpKernelInterface::class),
483+
[new IsGrantedAttributeMethodsController(), 'adminWithMethodGetAndPost'],
484+
[],
485+
new Request([], [], [], [], [], ['REQUEST_METHOD' => 'POST']),
486+
null
487+
);
488+
489+
$listener = new IsGrantedAttributeListener($authChecker);
490+
$this->expectException(AccessDeniedException::class);
491+
$listener->onKernelControllerArguments($event);
492+
}
493+
494+
public function testSkipsAuthorizationWhenMethodDoesNotMatchArrayConstraint()
495+
{
496+
$authChecker = $this->createMock(AuthorizationCheckerInterface::class);
497+
$authChecker->expects($this->never())->method('isGranted');
498+
499+
$event = new ControllerArgumentsEvent(
500+
$this->createMock(HttpKernelInterface::class),
501+
[new IsGrantedAttributeMethodsController(), 'adminWithMethodGetAndPost'],
502+
[],
503+
new Request([], [], [], [], [], ['REQUEST_METHOD' => 'PUT']),
504+
null
505+
);
506+
507+
$listener = new IsGrantedAttributeListener($authChecker);
508+
$listener->onKernelControllerArguments($event);
509+
}
510+
511+
public function testSkipsAuthorizationWhenMethodDoesNotMatchStringConstraint()
512+
{
513+
$authChecker = $this->createMock(AuthorizationCheckerInterface::class);
514+
$authChecker->expects($this->never())->method('isGranted');
515+
516+
$event = new ControllerArgumentsEvent(
517+
$this->createMock(HttpKernelInterface::class),
518+
[new IsGrantedAttributeMethodsController(), 'adminWithMethodGet'],
519+
[],
520+
new Request([], [], [], [], [], ['REQUEST_METHOD' => 'POST']),
521+
null
522+
);
523+
524+
$listener = new IsGrantedAttributeListener($authChecker);
525+
$listener->onKernelControllerArguments($event);
526+
}
457527
}

src/Symfony/Component/Security/Http/Tests/Fixtures/IsGrantedAttributeMethodsController.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,14 @@ public function withNestedExpressionInSubject($post, $arg2Name)
7777
public function withRequestAsSubject()
7878
{
7979
}
80+
81+
#[IsGranted(attribute: 'ROLE_ADMIN', methods: 'get')]
82+
public function adminWithMethodGet(): void
83+
{
84+
}
85+
86+
#[IsGranted(attribute: 'ROLE_ADMIN', methods: ['GET', 'POST'])]
87+
public function adminWithMethodGetAndPost(): void
88+
{
89+
}
8090
}

0 commit comments

Comments
 (0)