symfony-security-voters
Symfony security voters — role hierarchy, voter pattern, access control, authorization, permissions, IsGranted attribute. Triggers on: security, voter, role, authorization, access control, permission,
SKILL.mdskills/symfony-security-voters/SKILL.md
GitHubSymfony Security & Voters
You are an expert in Symfony security with Voters pattern within hexagonal architecture.
When to Activate
- User needs authorization on endpoints
- User asks about voters or role hierarchy
- User wants access control logic
- User mentions RBAC, permissions, or security
Version Notes
| Feature | Requires | Fallback |
|---|---|---|
#[IsGranted('ATTR', subject: 'argName')] |
Symfony 6.2 | AuthorizationCheckerInterface::isGranted() |
?Vote $vote = null 4th voter parameter (reasons in profiler / logs) |
Symfony 7.3 (part of the signature in 8.0) | Declare it anyway: harmless before 7.3 |
#[IsGranted] methods: option, #[CurrentUser] union types |
Symfony 7.4 | separate routes / single user class |
#[IsGranted] with a static closure |
Symfony 7.3 + PHP 8.5 | Voter |
subject: new Expression('...') |
Symfony 6.2 | string argument name |
UserInterface::eraseCredentials() |
deprecated 7.3 | do not keep plain passwords on the user |
Core Rule: Every Endpoint Has a ROLE
No endpoint is publicly accessible without explicit #[IsGranted] or Voter check. Public endpoints use #[IsGranted('PUBLIC_ACCESS')] so the decision is visible.
#[Route('/api/users', methods: ['GET'])]
#[IsGranted('ROLE_USER_LIST')] // ALWAYS required
public function list(): JsonResponse { /* ... */ }
Role Hierarchy
Define roles in security.yaml:
security:
role_hierarchy:
ROLE_ADMIN: [ROLE_USER_CREATE, ROLE_USER_EDIT, ROLE_USER_DELETE, ROLE_USER_LIST, ROLE_USER_VIEW]
ROLE_MANAGER: [ROLE_USER_LIST, ROLE_USER_VIEW, ROLE_ORDER_MANAGE]
ROLE_USER: [ROLE_USER_VIEW]
Naming Convention
- Module-scoped:
ROLE_{MODULE}_{ACTION} - Examples:
ROLE_USER_CREATE,ROLE_ORDER_VIEW,ROLE_REPORT_GENERATE - Voter attributes (resource permissions) have no
ROLE_prefix:ORDER_EDIT
Simple Cases: Use #[IsGranted]
#[IsGranted('ROLE_USER_CREATE')]
public function create(): JsonResponse { /* ... */ }
Complex Cases: Use Voters
When authorization depends on the resource ("can this user edit THIS order?"). Controllers only know the ID, so the voter receives the ID and loads what it needs through a read port: Presentation never touches Domain entities.
namespace App\Infrastructure\{Module}\Security;
use App\Domain\{Module}\Port\{Entity}ReadRepositoryInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** @extends Voter<string, string> */
final class {Entity}Voter extends Voter
{
public const VIEW = '{MODULE}_VIEW';
public const EDIT = '{MODULE}_EDIT';
public function __construct(
private readonly {Entity}ReadRepositoryInterface $repository,
) {
}
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::VIEW, self::EDIT], true) && is_string($subject);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
$view = $this->repository->findById($subject);
if ($view === null) {
$vote?->addReason('Resource not found');
return false;
}
return match ($attribute) {
self::VIEW, self::EDIT => $view->ownerId === $token->getUserIdentifier(),
default => false,
};
}
}
Using the Voter in a Controller
#[Route('/{id}', methods: ['PUT'])]
#[IsGranted('{MODULE}_EDIT', subject: 'id')] // 'id' = controller argument name
public function update(string $id, #[MapRequestPayload] Update{Entity}Request $request): JsonResponse
{
$this->commandBus->dispatch(new Update{Entity}($id /* , ... */));
return $this->success(null);
}
Use the attribute string ('ORDER_EDIT') in Presentation instead of importing the voter class from Infrastructure.
References
See references/ for detailed guides:
voter-patterns.md- Full voter examples and testingrole-hierarchy.md- Role hierarchy design, security.yaml, security user adapter