Skip to main content

Policies

A policy decides whether an actor may invoke an action. Every action in the graph has exactly one policy. The runtime evaluates it before any data is touched.

The Policy contract

interface Policy
{
public function evaluate(
Actor $actor,
string $actionFqn,
?Subject $subject,
Context $context,
): Decision;
}

A policy returns a Decision enum value:

DecisionMeaning
Permitthe action is allowed
Denythe action is refused
Abstainthe policy makes no decision

Deny-by-default, fail-closed

The PolicyEngine applies two safety rules when it evaluates a policy:

  • Abstain becomes Deny. If a policy returns Abstain, the engine treats it as Deny. There is no "allow because nothing objected".
  • An exception becomes Deny. If a policy throws, the engine catches it and returns Deny. A broken policy fails closed, never open.

If the decision is anything other than Permit, the runtime throws PolicyDenied and the invocation stops before the transaction opens.

The built-in policy: RoleRequired

AUSUS ships RoleRequired for role-based authorization. It permits the action if the actor holds a named role.

You attach it through the DSL with ->requireRole():

'issue' => Action::transition('status', from: 'DRAFT', to: 'ISSUED')
->requireRole('invoice.issuer'),

The compiler creates a PolicyNode for the action that constructs a RoleRequired policy with role: 'invoice.issuer'. At invocation time the engine checks the role against the actor's role list.

Data-dependent authorization (RFC-018)

Role-based policies decide on identity alone. RFC-018 adds guards that read the subject record and structured actor attributes at authorization time — so a rule like "an adjuster may approve a claim only up to their authority limit" is expressed as configuration, not application code.

A guard is attached to an action with ->requireThat(Cond), alongside (not instead of) ->requireRole():

$dsl->actorAttributes(['authority_limit' => Field::integer()]);

'approve' => Action::transition('status', from: 'ASSESSING', to: 'APPROVED')
->requireRole('claims.adjuster')
->requireThat(Cond::lte(Fact::subject('claim_amount'), Fact::actor('authority_limit'))),
  • Facts. Fact::subject(field) reads a field of the loaded subject entity; Fact::actor(attribute) reads a structured actor attribute (declared with Dsl::actorAttributes(...)); Fact::input(key) reads an action input.
  • Conditions. Cond::eq / ne / lt / lte / gt / gte / in, composed with Cond::and / or / not.
  • Compile-time closure. A guard referencing an unknown subject field, actor attribute, or input is rejected when the graph compiles (DanglingFactReference).
  • Fail-closed, in-transaction. The subject is loaded before authorization and the guard runs inside the action's transaction, before any effect. A failing guard raises PolicyDenied (HTTP 403) and rolls the transaction back.

Guards do not change the Policy contract above — they are an additional authorization mechanism on the same action. Actor attributes are seeded through ApplicationConfig::actorAttributes(...) and, over HTTP, an X-Actor-Attributes header parsed fail-safe.

Actors

An actor is who is performing the action. The Actor contract exposes a ref, a role list, a permission list, and a canonical roleHash().

AUSUS ships StubActor — a fixed in-memory actor:

use Ausus\{StubActor, ActorRef};

$actor = new StubActor(
new ActorRef('user', 'user42', 'acme'),
['invoice.creator', 'invoice.issuer', 'invoice.canceler', 'invoice.viewer'],
);

The HTTP API builds a StubActor from request headers (X-Actor-Id, X-Actor-Roles) — see The HTTP API.

Current limitations

  • Authorization is role-based (RoleRequired) and, since RFC-018, data-dependent (requireThat guards, above). There is no separate permission-based policy class.
  • There is no authentication. StubActor is a trusted, caller-supplied identity. Anything exposing the runtime — including the HTTP API — must put a real authentication layer in front of it. The reserved ausus/auth-bridge package is the planned home for that and ships no code.
  • Field-level and projection-level visibility policies are designed but not enforced.