181 lines
4.8 KiB
Markdown
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
|
|
```
|