Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/Symfony/Component/Security/Http/Attribute/IsGranted.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,25 @@
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
final class IsGranted
{
/** @var string[] */
public readonly array $methods;

/**
* @param string|Expression|\Closure(IsGrantedContext, mixed $subject):bool $attribute The attribute that will be checked against a given authentication token and optional subject
* @param array|string|Expression|\Closure(array<string,mixed>, Request):mixed|null $subject An optional subject - e.g. the current object being voted on
* @param string|null $message A custom message when access is not granted
* @param int|null $statusCode If set, will throw HttpKernel's HttpException with the given $statusCode; if null, Security\Core's AccessDeniedException will be used
* @param int|null $exceptionCode If set, will add the exception code to thrown exception
* @param string|Expression|\Closure(IsGrantedContext, mixed $subject):bool $attribute The attribute that will be checked against a given authentication token and optional subject
* @param array|string|Expression|\Closure(array<string,mixed>, Request):mixed|null $subject An optional subject - e.g. the current object being voted on
* @param string|null $message A custom message when access is not granted
* @param int|null $statusCode If set, will throw HttpKernel's HttpException with the given $statusCode; if null, Security\Core's AccessDeniedException will be used
* @param int|null $exceptionCode If set, will add the exception code to thrown exception
* @param string[]|string $methods HTTP methods to apply validation to. Empty array means all methods are allowed
*/
public function __construct(
public string|Expression|\Closure $attribute,
public array|string|Expression|\Closure|null $subject = null,
public ?string $message = null,
public ?int $statusCode = null,
public ?int $exceptionCode = null,
array|string $methods = [],
) {
$this->methods = (array) $methods;
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/Security/Http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CHANGELOG
* Add support for union types with `#[CurrentUser]`
* Deprecate callable firewall listeners, extend `AbstractListener` or implement `FirewallListenerInterface` instead
* Deprecate `AbstractListener::__invoke`
* Add `$methods` argument to `#[IsGranted]` to restrict validation to specific HTTP methods

7.3
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
$arguments = $event->getNamedArguments();

foreach ($attributes as $attribute) {
if ($attribute->methods && !\in_array($request->getMethod(), array_map('strtoupper', $attribute->methods), true)) {
continue;
}

$subject = null;

if ($subjectRef = $attribute->subject) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,4 +454,74 @@ public function testAccessDeniedExceptionWithExceptionCode()

$listener->onKernelControllerArguments($event);
}

public function testThrowsAccessDeniedExceptionWhenMethodMatchesStringConstraint()
{
$authChecker = $this->createMock(AuthorizationCheckerInterface::class);
$authChecker->expects($this->once())->method('isGranted')->willReturn(false);

$event = new ControllerArgumentsEvent(
$this->createMock(HttpKernelInterface::class),
[new IsGrantedAttributeMethodsController(), 'adminWithMethodGet'],
[],
new Request([], [], [], [], [], ['REQUEST_METHOD' => 'GET']),
null
);

$listener = new IsGrantedAttributeListener($authChecker);
$this->expectException(AccessDeniedException::class);
$listener->onKernelControllerArguments($event);
}

public function testThrowsAccessDeniedExceptionWhenMethodMatchesArrayConstraint()
{
$authChecker = $this->createMock(AuthorizationCheckerInterface::class);
$authChecker->expects($this->once())->method('isGranted')->willReturn(false);

$event = new ControllerArgumentsEvent(
$this->createMock(HttpKernelInterface::class),
[new IsGrantedAttributeMethodsController(), 'adminWithMethodGetAndPost'],
[],
new Request([], [], [], [], [], ['REQUEST_METHOD' => 'POST']),
null
);

$listener = new IsGrantedAttributeListener($authChecker);
$this->expectException(AccessDeniedException::class);
$listener->onKernelControllerArguments($event);
}

public function testSkipsAuthorizationWhenMethodDoesNotMatchArrayConstraint()
{
$authChecker = $this->createMock(AuthorizationCheckerInterface::class);
$authChecker->expects($this->never())->method('isGranted');

$event = new ControllerArgumentsEvent(
$this->createMock(HttpKernelInterface::class),
[new IsGrantedAttributeMethodsController(), 'adminWithMethodGetAndPost'],
[],
new Request([], [], [], [], [], ['REQUEST_METHOD' => 'PUT']),
null
);

$listener = new IsGrantedAttributeListener($authChecker);
$listener->onKernelControllerArguments($event);
}

public function testSkipsAuthorizationWhenMethodDoesNotMatchStringConstraint()
{
$authChecker = $this->createMock(AuthorizationCheckerInterface::class);
$authChecker->expects($this->never())->method('isGranted');

$event = new ControllerArgumentsEvent(
$this->createMock(HttpKernelInterface::class),
[new IsGrantedAttributeMethodsController(), 'adminWithMethodGet'],
[],
new Request([], [], [], [], [], ['REQUEST_METHOD' => 'POST']),
null
);

$listener = new IsGrantedAttributeListener($authChecker);
$listener->onKernelControllerArguments($event);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,14 @@ public function withNestedExpressionInSubject($post, $arg2Name)
public function withRequestAsSubject()
{
}

#[IsGranted(attribute: 'ROLE_ADMIN', methods: 'get')]
public function adminWithMethodGet(): void
{
}

#[IsGranted(attribute: 'ROLE_ADMIN', methods: ['GET', 'POST'])]
public function adminWithMethodGetAndPost(): void
{
}
}
Loading