Access Policies
An access policy is a question declared where it applies, as an attribute, rather than asked in the body of a method.
use AccessControl\Attribute\AccessPolicy;
use AccessControl\Attribute\Argument;
#[AccessPolicy('EDIT', new Argument('post'))]
public function edit(Post $post): Response
{
// ...
}The entry point reads the attribute, asks the question, and refuses before your code runs. In a Symfony application that is done for you on controllers and on console commands.
#[AccessPolicy]
new AccessPolicy(
attribute: 'EDIT', // what is being asked
subject: new Argument('post'), // what it is being asked about
strategy: 'deny_overrides', // the combining algorithm, optional
environment: ['ip' => '10.0.0.1'], // extra circumstances, optional
allowIfAllAbstain: null, // null defers to the manager
message: 'Only the author may edit this post.',
);It is repeatable, and it goes on a class, a method or a function. On a class it applies to every method.
The attribute is anything a voter understands: a string, a Closure, an Expression.
Argument: values known only at runtime
An attribute is static; the subject usually is not. Argument names a value the entry point will resolve when the call happens.
The name is the argument's name. Naming one that does not exist raises an UnknownArgumentException rather than passing null to the voters, which would silently ask a different question.
message: what the visitor reads
The reason a voter gives is a diagnostic and is deliberately withheld from the response. The message of a policy is not: it is the text carried by the denial.
Combining policies
Three combinators compose policies into a tree. All three are repeatable attributes and take a message of their own.
All
Every nested policy must grant. It short-circuits on the first denial.
AtLeastOneOf
One granting policy is enough. It short-circuits on the first grant.
When
Requires the nested policies only when a condition holds, and steps aside otherwise, abstaining rather than denying.
The condition speaks about the circumstances of the call, not about the requester. The entry point hands over its own context: an HTTP method is reached as request.isMethod("POST"), a console option as input.getOption("force"). This is what #[IsGranted]'s methods parameter did, generalised, and what tells a web call apart from a console one.
The condition is an Expression and not a string, deliberately: a bare string would be silently compiled as one, which is exactly the trap the expression voter had to be fixed for.
There is no Not
Two reasons, and neither is an oversight.
The negation of a three-valued logic is not defined: Not would turn "nobody had anything to say" into "access granted". And negative permissions are a known trap: Not(ROLE_ADMIN) allows everybody except administrators, anonymous visitors included.
A userland Not is a handler and an attribute of your own, needing no change to the component. Sequentially is not provided either, being already what All and AtLeastOneOf do by short-circuiting.
Writing your own policy and handler
An access policy is any object implementing AccessPolicyInterface; a handler is what knows how to evaluate it.
The evaluator is handed over so a composite handler can evaluate its children through the same path, which is what keeps the profiler tree and the events correct whatever the shape.
In a Symfony application, implementing the interface is enough: autoconfiguration tags the service access_control.policy_handler. A policy no handler supports raises an UnsupportedAccessPolicyException rather than being ignored.
AccessPolicyInterface declares only message. A status code or an exit code is a property of the entry point, not of the policy that refused, and each entry point settles its own.
Last updated
Was this helpful?
