For the complete documentation index, see llms.txt. This page is also available as Markdown.

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?