|
| 1 | +<!-- |
| 2 | +SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs/contributors> |
| 3 | +
|
| 4 | +SPDX-License-Identifier: MIT |
| 5 | +--> |
| 6 | + |
| 7 | +# Authorization |
| 8 | + |
| 9 | +- When performing administrative actions, you can bypass authorization with `authorize?: false` |
| 10 | +- To run actions as a particular user, look that user up and pass it as the `actor` option |
| 11 | +- Always set the actor on the query/changeset/input, not when calling the action |
| 12 | +- Use policies to define authorization rules |
| 13 | + |
| 14 | +```elixir |
| 15 | +# Good |
| 16 | +Post |
| 17 | +|> Ash.Query.for_read(:read, %{}, actor: current_user) |
| 18 | +|> Ash.read!() |
| 19 | + |
| 20 | +# BAD, DO NOT DO THIS |
| 21 | +Post |
| 22 | +|> Ash.Query.for_read(:read, %{}) |
| 23 | +|> Ash.read!(actor: current_user) |
| 24 | +``` |
| 25 | + |
| 26 | +## Policies |
| 27 | + |
| 28 | +To use policies, add the `Ash.Policy.Authorizer` to your resource: |
| 29 | + |
| 30 | +```elixir |
| 31 | +defmodule MyApp.Post do |
| 32 | + use Ash.Resource, |
| 33 | + domain: MyApp.Blog, |
| 34 | + authorizers: [Ash.Policy.Authorizer] |
| 35 | + |
| 36 | + # Rest of resource definition... |
| 37 | +end |
| 38 | +``` |
| 39 | + |
| 40 | +## Policy Basics |
| 41 | + |
| 42 | +Policies determine what actions on a resource are permitted for a given actor. Define policies in the `policies` block: |
| 43 | + |
| 44 | +```elixir |
| 45 | +policies do |
| 46 | + # A simple policy that applies to all read actions |
| 47 | + policy action_type(:read) do |
| 48 | + # Authorize if record is public |
| 49 | + authorize_if expr(public == true) |
| 50 | + |
| 51 | + # Authorize if actor is the owner |
| 52 | + authorize_if relates_to_actor_via(:owner) |
| 53 | + end |
| 54 | + |
| 55 | + # A policy for create actions |
| 56 | + policy action_type(:create) do |
| 57 | + # Only allow active users to create records |
| 58 | + forbid_unless actor_attribute_equals(:active, true) |
| 59 | + |
| 60 | + # Ensure the record being created relates to the actor |
| 61 | + authorize_if relating_to_actor(:owner) |
| 62 | + end |
| 63 | +end |
| 64 | +``` |
| 65 | + |
| 66 | +## Policy Evaluation Flow |
| 67 | + |
| 68 | +Policies evaluate from top to bottom with the following logic: |
| 69 | + |
| 70 | +1. All policies that apply to an action must pass for the action to be allowed |
| 71 | +2. Within each policy, checks evaluate from top to bottom |
| 72 | +3. The first check that produces a decision determines the policy result |
| 73 | +4. If no check produces a decision, the policy defaults to forbidden |
| 74 | + |
| 75 | +## IMPORTANT: Policy Check Logic |
| 76 | + |
| 77 | +**the first check that yields a result determines the policy outcome** |
| 78 | + |
| 79 | +```elixir |
| 80 | +# WRONG - This is OR logic, not AND logic! |
| 81 | +policy action_type(:update) do |
| 82 | + authorize_if actor_attribute_equals(:admin?, true) # If this passes, policy passes |
| 83 | + authorize_if relates_to_actor_via(:owner) # Only checked if first fails |
| 84 | +end |
| 85 | +``` |
| 86 | + |
| 87 | +To require BOTH conditions in that example, you would use `forbid_unless` for the first condition: |
| 88 | + |
| 89 | +```elixir |
| 90 | +# CORRECT - This requires BOTH conditions |
| 91 | +policy action_type(:update) do |
| 92 | + forbid_unless actor_attribute_equals(:admin?, true) # Must be admin |
| 93 | + authorize_if relates_to_actor_via(:owner) # AND must be owner |
| 94 | +end |
| 95 | +``` |
| 96 | + |
| 97 | +Alternative patterns for AND logic: |
| 98 | +- Use multiple separate policies (each must pass independently) |
| 99 | +- Use a single complex expression with `expr(condition1 and condition2)` |
| 100 | +- Use `forbid_unless` for required conditions, then `authorize_if` for the final check |
| 101 | + |
| 102 | +## Bypass Policies |
| 103 | + |
| 104 | +Use bypass policies to allow certain actors to bypass other policy restrictions. This should be used almost exclusively for admin bypasses. |
| 105 | + |
| 106 | +```elixir |
| 107 | +policies do |
| 108 | + # Bypass policy for admins - if this passes, other policies don't need to pass |
| 109 | + bypass actor_attribute_equals(:admin, true) do |
| 110 | + authorize_if always() |
| 111 | + end |
| 112 | + |
| 113 | + # Regular policies follow... |
| 114 | + policy action_type(:read) do |
| 115 | + # ... |
| 116 | + end |
| 117 | +end |
| 118 | +``` |
| 119 | + |
| 120 | +## Field Policies |
| 121 | + |
| 122 | +Field policies control access to specific fields (attributes, calculations, aggregates): |
| 123 | + |
| 124 | +```elixir |
| 125 | +field_policies do |
| 126 | + # Only supervisors can see the salary field |
| 127 | + field_policy :salary do |
| 128 | + authorize_if actor_attribute_equals(:role, :supervisor) |
| 129 | + end |
| 130 | + |
| 131 | + # Allow access to all other fields |
| 132 | + field_policy :* do |
| 133 | + authorize_if always() |
| 134 | + end |
| 135 | +end |
| 136 | +``` |
| 137 | + |
| 138 | +## Policy Checks |
| 139 | + |
| 140 | +There are two main types of checks used in policies: |
| 141 | + |
| 142 | +1. **Simple checks** - Return true/false answers (e.g., "is the actor an admin?") |
| 143 | +2. **Filter checks** - Return filters to apply to data (e.g., "only show records owned by the actor") |
| 144 | + |
| 145 | +You can use built-in checks or create custom ones: |
| 146 | + |
| 147 | +```elixir |
| 148 | +# Built-in checks |
| 149 | +authorize_if actor_attribute_equals(:role, :admin) |
| 150 | +authorize_if relates_to_actor_via(:owner) |
| 151 | +authorize_if expr(public == true) |
| 152 | + |
| 153 | +# Custom check module |
| 154 | +authorize_if MyApp.Checks.ActorHasPermission |
| 155 | +``` |
| 156 | + |
| 157 | +### Custom Policy Checks |
| 158 | + |
| 159 | +Create custom checks by implementing `Ash.Policy.SimpleCheck` or `Ash.Policy.FilterCheck`: |
| 160 | + |
| 161 | +```elixir |
| 162 | +# Simple check - returns true/false |
| 163 | +defmodule MyApp.Checks.ActorHasRole do |
| 164 | + use Ash.Policy.SimpleCheck |
| 165 | + |
| 166 | + def match?(%{role: actor_role}, _context, opts) do |
| 167 | + actor_role == (opts[:role] || :admin) |
| 168 | + end |
| 169 | + def match?(_, _, _), do: false |
| 170 | +end |
| 171 | + |
| 172 | +# Filter check - returns query filter |
| 173 | +defmodule MyApp.Checks.VisibleToUserLevel do |
| 174 | + use Ash.Policy.FilterCheck |
| 175 | + |
| 176 | + def filter(actor, _authorizer, _opts) do |
| 177 | + expr(visibility_level <= ^actor.user_level) |
| 178 | + end |
| 179 | +end |
| 180 | + |
| 181 | +# Usage |
| 182 | +policy action_type(:read) do |
| 183 | + authorize_if {MyApp.Checks.ActorHasRole, role: :manager} |
| 184 | + authorize_if MyApp.Checks.VisibleToUserLevel |
| 185 | +end |
| 186 | +``` |
| 187 | + |
0 commit comments