| name | ash-authorization |
| description | Authorization with policies, bypasses, and custom checks |
Authorization
- When performing administrative actions, you can bypass authorization with
authorize?: false - To run actions as a particular user, look that user up and pass it as the
actoroption - Always set the actor on the query/changeset/input, not when calling the action
- Use policies to define authorization rules
# Good
Post
|> Ash.Query.for_read(:read, %{}, actor: current_user)
|> Ash.read!()
# BAD, DO NOT DO THIS
Post
|> Ash.Query.for_read(:read, %{})
|> Ash.read!(actor: current_user)
Policies
To use policies, add the Ash.Policy.Authorizer to your resource:
defmodule MyApp.Post do
use Ash.Resource,
domain: MyApp.Blog,
authorizers: [Ash.Policy.Authorizer]
# Rest of resource definition...
end
Policy Basics
Policies determine what actions on a resource are permitted for a given actor. Define policies in the policies block:
policies do
# A simple policy that applies to all read actions
policy action_type(:read) do
# Authorize if record is public
authorize_if expr(public == true)
# Authorize if actor is the owner
authorize_if relates_to_actor_via(:owner)
end
# A policy for create actions
policy action_type(:create) do
# Only allow active users to create records
forbid_unless actor_attribute_equals(:active, true)
# Ensure the record being created relates to the actor
authorize_if relating_to_actor(:owner)
end
end
Policy Evaluation Flow
Policies evaluate from top to bottom with the following logic:
- All policies that apply to an action must pass for the action to be allowed
- Within each policy, checks evaluate from top to bottom
- The first check that produces a decision determines the policy result
- If no check produces a decision, the policy defaults to forbidden
IMPORTANT: Policy Check Logic
the first check that yields a result determines the policy outcome
# WRONG - This is OR logic, not AND logic!
policy action_type(:update) do
authorize_if actor_attribute_equals(:admin?, true) # If this passes, policy passes
authorize_if relates_to_actor_via(:owner) # Only checked if first fails
end
To require BOTH conditions in that example, you would use forbid_unless for the first condition:
# CORRECT - This requires BOTH conditions
policy action_type(:update) do
forbid_unless actor_attribute_equals(:admin?, true) # Must be admin
authorize_if relates_to_actor_via(:owner) # AND must be owner
end
Alternative patterns for AND logic:
- Use multiple separate policies (each must pass independently)
- Use a single complex expression with
expr(condition1 and condition2) - Use
forbid_unlessfor required conditions, thenauthorize_iffor the final check
Bypass Policies
Use bypass policies to allow certain actors to bypass other policy restrictions. This should be used almost exclusively for admin bypasses.
policies do
# Bypass policy for admins - if this passes, other policies don't need to pass
bypass actor_attribute_equals(:admin, true) do
authorize_if always()
end
# Regular policies follow...
policy action_type(:read) do
# ...
end
end
Field Policies
Field policies control access to specific fields (attributes, calculations, aggregates):
field_policies do
# Only supervisors can see the salary field
field_policy :salary do
authorize_if actor_attribute_equals(:role, :supervisor)
end
# Allow access to all other fields
field_policy :* do
authorize_if always()
end
end
Policy Checks
There are two main types of checks used in policies:
- Simple checks - Return true/false answers (e.g., "is the actor an admin?")
- Filter checks - Return filters to apply to data (e.g., "only show records owned by the actor")
You can use built-in checks or create custom ones:
# Built-in checks
authorize_if actor_attribute_equals(:role, :admin)
authorize_if relates_to_actor_via(:owner)
authorize_if expr(public == true)
# Custom check module
authorize_if MyApp.Checks.ActorHasPermission
Custom Policy Checks
Create custom checks by implementing Ash.Policy.SimpleCheck or Ash.Policy.FilterCheck:
# Simple check - returns true/false
defmodule MyApp.Checks.ActorHasRole do
use Ash.Policy.SimpleCheck
def match?(%{role: actor_role}, _context, opts) do
actor_role == (opts[:role] || :admin)
end
def match?(_, _, _), do: false
end
# Filter check - returns query filter
defmodule MyApp.Checks.VisibleToUserLevel do
use Ash.Policy.FilterCheck
def filter(actor, _authorizer, _opts) do
expr(visibility_level <= ^actor.user_level)
end
end
# Usage
policy action_type(:read) do
authorize_if {MyApp.Checks.ActorHasRole, role: :manager}
authorize_if MyApp.Checks.VisibleToUserLevel
end