Introduction
Symfony security has a great feature to check authorization named Voters. Voters are services which allows you to check user authorization in the way you need.
In this article, I would like to share with you another way to define custom authorization checking: Create our custom security attribute
Let’s imagine we store user roles out of the User class (the one which implements the Symfony UserInterface) and we have a controller on which we only want to allow ROLE_SUPERUSER users.
Creating the Attribute
Let’s create a simple attribute which receive the roles we want to allow
#[\Attribute]
class IsUserGranted
{
public function __construct(public readonly array $roles){ }
}
Creating the Event Subscriber
Now, let’s create a subscriber which will keep listening to KernelEvents::CONTROLLER_ARGUMENTS.
class UserGrantedSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments']
];
}
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
{
$attrs = $event->getAttributes();
/** @var IsUserGranted[]|null $isUserGrantedAttrs */
$isUserGrantedAttrs = $attrs[IsUserGranted::class] ?? null;
if(!$isUserGrantedAttrs){
return;
}
$isUserGranted = $isUserGrantedAttrs[0];
// Some code to get user roles from outside the User
$roles = ['......'];
// ------------------------------------
$commonRoles = array_intersect(
$roles,
$isUserGranted->roles
);
if(count($commonRoles) === 0){
throw new AccessDeniedException('You are not granted to get this resource');
}
}
}
After the KernelEvents::CONTROLLER_ARGUMENTS event is received, the function onKernelControllerArguments is executed. The function gets an object of class ControllerArgumentsEvent as a parameter. This object gives us access to controller attributes through the getAttributes method. If the IsUserGranted attribute is present, the user roles will be compared against granted roles and, if there are no matching roles, an AccessDeniedException will be thrown.
Protecting a Controller
The last step is to use our new attribute in a controller. Let’s create a controller and set it our attribute:
#[IsUserGranted(roles: ["ROLE_SUPERUSER"])]
class AdminController extends AbstractController
{
#[Route('/admin/domains', name: 'get_admin_domains', methods: ['GET'])]
public function getAdminDomains(): JsonResponse
{
return new JsonResponse([
'ares.com',
'atila.com',
], Response::HTTP_OK);
}
}
And that’s all. If we try to access the resource with a user who does not have a ROLE_SUPERUSER, access will be denied.
Conclusion
This is another way you can consider if you want to add an extra authorization layer. As I've said in the introduction, this way can be useful when you want to apply the same authorization logic to all controller actions but the IsGranted attribute does not match your needs.
PHP attributes combined with the PHP reflection capabilities are a fantastic combination that allow developers to add extra behavior to classes in a really descriptive way. In my last published book, I use attributes to mark services as API operations, mark operations as background etc. If you want to know more, you can find the book here: [https://amzn.eu/d/3eO1DDi].