Files
Mixer/.agents/skills/ash-framework/references/authorization.md

181 lines
4.8 KiB
Markdown

# 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 `actor` option
- Always set the actor on the query/changeset/input, not when calling the action
- Use policies to define authorization rules
```elixir
# 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:
```elixir
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:
```elixir
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:
1. All policies that apply to an action must pass for the action to be allowed
2. Within each policy, checks evaluate from top to bottom
3. The first check that produces a decision determines the policy result
4. 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**
```elixir
# 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:
```elixir
# 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_unless` for required conditions, then `authorize_if` for 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.
```elixir
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):
```elixir
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:
1. **Simple checks** - Return true/false answers (e.g., "is the actor an admin?")
2. **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:
```elixir
# 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`:
```elixir
# 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
```