Packages

Declarative workflow orchestration for the Ash Framework — multi-step workflows combining human actions, background jobs, and time-based deadlines.

Current section

Files

Jump to
ash_workflow documentation topics authorization.md
Raw

documentation/topics/authorization.md

# Authorization

AshWorkflow integrates with Ash's policy system to control who can trigger workflow transitions.

Every policy in this guide needs `Ash.Policy.Authorizer` on the resource. `AshWorkflow.Transformers.AddPolicies` generates nothing without it, so a `policy` on a step used to be accepted and then never enforced. `AshWorkflow.Verifiers.ValidateStepPolicies` now rejects that at compile time.

```elixir
use Ash.Resource,
  domain: MyApp.Documents,
  authorizers: [Ash.Policy.Authorizer],
  extensions: [AshWorkflow, AshOban]
```

## Step-level policies

Add a `policy` to a manual step to restrict all its transitions to a specific kind of actor:

```elixir
step :manager_review do
  policy actor_attribute_equals(:role, :manager)

  transition :approve, to: :approved
  transition :reject, to: :rejected
end
```

This generates an Ash policy targeting both `:approve` and `:reject`:

```elixir
policies do
  policy action([:approve, :reject]) do
    authorize_if actor_attribute_equals(:role, :manager)
  end
end
```

### Available checks

The `policy` field accepts any `{module, opts}` tuple implementing `Ash.Policy.Check`. Common checks available inside the step block:

```elixir
policy actor_attribute_equals(:role, :admin)
policy relates_to_actor_via(:assigned_to)
policy actor_present()
```

For checks not imported by default, pass the tuple directly:

```elixir
policy {MyApp.Checks.BelongsToTeam, []}
```

### Default allow-all policy

When any step has a `policy` declaration, the extension generates a default "allow all" policy at the end of the policy list. This ensures workflow actions without explicit policies, like create actions and automatic step actions, aren't blocked.

## Resource-level policies

For complex authorization — multi-condition rules, relationship checks, or custom modules — define policies directly on the resource:

```elixir
policies do
  policy action(:approve) do
    authorize_if relates_to_actor_via(:assigned_reviewer)
    authorize_if actor_attribute_equals(:role, :admin)
  end
end
```

The extension skips generating step-level policies for actions that already have user-defined policies targeting them.

## Enabling the authorizer

Step-level policies only take effect if the resource has `Ash.Policy.Authorizer` configured:

```elixir
use Ash.Resource,
  domain: MyApp.Domain,
  data_layer: AshPostgres.DataLayer,
  authorizers: [Ash.Policy.Authorizer],
  extensions: [AshWorkflow, AshOban]
```

Without the authorizer, policies are defined but not enforced.

## Querying permitted actions

The generated `:available_actions` calculation is authorization-aware. When loaded with an actor, it returns only the actions that actor is permitted to perform:

```elixir
# Without actor — returns all transitions for the current step
doc = Ash.load!(doc, :available_actions)
doc.available_actions
#=> [:approve, :reject]

# With actor — filtered by policies
doc = Ash.load!(doc, :available_actions, actor: %{role: :viewer})
doc.available_actions
#=> []

doc = Ash.load!(doc, :available_actions, actor: %{role: :manager})
doc.available_actions
#=> [:approve, :reject]
```

It calls `Ash.can?/2` for each action to check it against the actor's permissions.

## SAT solver dependency

Ash's policy authorizer requires a SAT solver at compile time. Add one of these to your `mix.exs`:

```elixir
{:picosat_elixir, "~> 0.2"}  # recommended for production (NIF)
{:simple_sat, "~> 0.1"}      # pure Elixir alternative
```