project-authzlisted
Install: claude install-skill ubermuda/loupe
# Authorization
## Voters
Invoke the `symfony-authorization` skill before you write or change a Voter, an `#[IsGranted]` attribute, or an `is_granted()` call. It documents the generic layer:
- voter class shape
- dotted attribute naming
- `#[IsGranted]` placement and `subject:` resolution
- voter scoping
- read-vs-write separation
- Twig `is_granted(constant(...))`
- the rename checklist
The rules below are project policy on top of that layer.
Never use `IS_AUTHENTICATED_FULLY` in `#[IsGranted]`. Every access check names a Voter constant and a subject. `IS_AUTHENTICATED_FULLY` bypasses the Voter layer and cannot express resource-level ownership.
```php
// ✗ — grants access to any authenticated user regardless of ownership
#[IsGranted('IS_AUTHENTICATED_FULLY')]
// ✓ — delegates to the Voter which checks ownership
#[IsGranted(ProjectVoter::WORKSPACE_MANAGE, subject: 'workspace')]
```
Put `#[IsGranted]` on the class. gamache's `controller.isGrantedNotClassLevel` rule fails a method-level `#[IsGranted]` on a single-action controller. `denyAccessUnlessGranted()` inside `__invoke()` is forbidden.
An imperative `denyAccessUnlessGranted()` call shows that you have not found the right (subject, permission) pair. Resolve both:
- Subject: the most specific entity the route already resolves. `__invoke(Comment $comment)` gives the subject `'comment'`. The subject need not be the entity the policy checks, because the voter walks up to `comment.version.document.owner`. "The docume