In my Symfony application I am using Voters for RBAC implementation.
I am using it within API platform. For now I managed to make it work both on Collections and Item operations.
The thing that concerns me is the code repetition. Could this be a bad practise? It is not bad to mention is that I am using DDD (Domain Driven Design). I have a lot of entities that needs to be covered by Voters and I read that using one Voter for multiple entities is NOT recommended (resource).
As I am following that advice I am questioning my approach as I have many custom voter classes that have the same code. The main difference is on the entity subject.
I will post an example and welcome all advices as I am on a breaking point where I do not know what is the best way to continue.
Thanks!
class FirstEntityVoter extends Voter
{
private CustomRepository $customRepository;
public function __construct (CustomRepository $customRepository) {
$this->customRepository = $customRepository;
}
protected function supports(
string $attribute,
$subject
): bool
{
// If the subject is a string check if class exists to support collectionOperations
if(is_string($subject) && class_exists($subject)) {
$subject = new $subject;
}
$supportsAttribute = in_array($attribute, ActionEnum::ACTION_LIST);
$supportsSubject = $subject instanceof FirstEntity;
return $supportsAttribute && $supportsSubject;
}
/**
* @throws Exception
*/
protected function voteOnAttribute(
string $attribute,
$subject,
TokenInterface $token
): bool
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
return match ($attribute) {
ActionEnum::READ => $this->canRead($user),
ActionEnum::CREATE => $this->canCreate($user),
ActionEnum::EDIT => $this->canEdit($user),
ActionEnum::DELETE => $this->canDelete($user),
default => throw new Exception(sprintf('Unhandled attribute "%s"', $attribute))
};
}
private function canRead(User $user): bool
{
return $this->customRepository->hasRole(
$user->getId(),
ActionEnum::READ
);
}
private function canCreate(User $user): bool
{
// same as canRead()
}
private function canEdit(User $user): bool
{
// same as canRead()
}
private function canDelete(User $user): bool
{
// same as canRead()
}
Defined in xml files for api platfom config like:
<itemOperation name="get">
<attribute name="method">GET</attribute>
<attribute name="security">is_granted('read', object)</attribute>
</itemOperation>