Policies determine what actions on a resource are permitted for a given actor.
You can specify an actor using the code api via the
actor option, like so:
MyApp.MyApi.read(MyResource, actor: current_user)
Before we jump into the guide, it is critical to understand that the policy code doesn’t actually do anything in the classic sense. It simply builds up a set of policies that are stored for use later. The checker that reads those policies and authorizes requests may run all, some of, or none of your checks, depending on the details of the request being authorized.
To see what checks are built-in, see
Every policy that applies must pass for a given request.
For example, a policy might have a condition
action_type(:read) and another one might
have a condition like
If both apply (i.e an admin is using a read action), then both policies must pass.
A policy can produce one of three results:
:unknown is treated the same as a
A policy contains checks, which determine whether or not the policy passes for a given request.
A bypass policy is just like a policy, except if a bypass passes, then other policies after it do not need to pass. This can be useful for writing complex access rules, or for a simple rule like “an admin can do anything”.
Checks, like policies, evaluate from top to bottom. A check can produce one of three results, the same that a policy can produce. While checks are not necessarily evaluated in order, they logically apply in that order, so you may as well think of it in that way. It can be thought of as a simple step-through algorithm.
For each check, starting from the top:
Run the check.
If it returns
:authorized, the policy is
If it returns
:forbidden, the policy is
If it returns
:unknown, the next check down is checked
- If it returns
Most checks won’t return a status, but instead return a “filter”. Filter checks are applied to the query that is being run, and then the rest of the checks are run. In general, all checks should be filter checks or simple checks.
Lets start with the simplest policy set:
policies do policy always() do authorize_if always() end end
Here, we have a single policy. The first argument to
policy is the “condition”. If the condition is true,
then the policy applies to the request. If a given policy applies, then one of the checks inside the policy must authorize that policy. Every policy that applies to a given request must each be authorized for a request to be authorized.
Within this policy we have a single check, declared with
authorize_if. Checks logically apply from top to bottom, based on their check type. In this case, we’d read the policy as “this policy always applies, and authorizes always”.
There are four check types, all of which do what they sound like they do:
authorize_if- if the check is true, the policy is authorized.
authorize_unless- if the check is false, the policy is authorized.
forbid_if- if the check is true, the policy is forbidden.
forbid_unless- if the check is false, the policy is forbidden.
In each case, if the policy is not authorized or forbidden, the flow moves to the next check.
In this example, we use some of the provided built in checks.
policies do # Anything you can use in a condition, you can use in a check, and vice-versa # This policy applies if the actor is a super_user # Additionally, this policy is declared as a `bypass`. That means that this check is allowed to fail without # failing the whole request, and that if this check *passes*, the entire request passes. bypass actor_attribute_equals(:super_user, true) do authorize_if always() end # This will likely be a common occurrence. Specifically, policies that apply to all read actions policy action_type(:read) do # unless the actor is an active user, forbid their request forbid_unless actor_attribute_equals(:active, true) # if the record is marked as public, authorize the request authorize_if attribute(:public, true) # if the actor is related to the data via that data's `owner` relationship, authorize the request authorize_if relates_to_actor_via(:owner) end end
A simple way to define a policy is by using
expr/1 in the policy. For example:
authorize_if expr(exists(role, name == "owner"))
Keep in mind that, for create actions, many
expr/1 checks won’t make sense, and may return
false when you wouldn’t expect. Expression (and other filter) policies apply to “a synthesized result” of applying the action, so related values won’t be available. For this reason, you may end up wanting to use other checks that are built for working against changesets, or only simple attribute-based filter checks. Custom checks may also be warranted here.
In these and in other filter checks, it is advised to use
exists/2 when referring to relationships, because of the way that the policy authorizer may mix & match your policies when building filters. There is a semantic difference in filters between
friends.first_name == "ted" and friends.last_name == "dansen". This means that you have a single friend with the first_name “bob” and the last name “fred”. If you use
exists, then your policies can be used in filters without excluding unnecessary data, i.e
exists(friends, first_name == "ted") and exists(friends, last_name == "dansen") means “you have one friend with the first_name “ted” and one friend with the last_name “dansen”.
Depending on the action type these expressions behave slightly differently.
- In reads, the expression will be applied to the query.
For creates, the expression applies to the result of applying the changes. In these cases, you can’t use things like
fragmentbecause nothing exists in the database.
- For updates and destroys, the expression applies to the data about to be updated or destroyed, i.e the data before the action is run
The default access type is
:filter. In most cases this will be all you need. In the example above, if a user made a request for all instances
of the resource, it wouldn’t actually return a forbidden error. It simply attaches the appropriate filter to fetch data that the user can see.
If the actor attribute
false, then the request would be forbidden (because there is no data for which they can pass this policy). However, if
true, the authorizer would attach the following filter to the request:
public or owner == ^actor(:_primary_key)
To understand what
actor(:_primary_key) means, see the Filter Templates section in
Additionally, some checks have more expensive components that can’t be checked before the request is run. To enable those, use the
access_type :runtime. All checks that can be implemented as filters or strict checks will still be done that way, but this enables checks to run their
check/4 callback if necessary.
Ash.Policy.Check for more information on writing custom checks, which you will likely need at some point when the built in checks are insufficient
Policy breakdowns can be fetched on demand for a given forbidden error (either an
Ash.Error.Forbidden that contains one ore more
errors, or an
Ash.Error.Forbidden.Policy error itself), via
Here is an example policy breakdown from tests:
Policy Breakdown A check status of `?` implies that the solver did not need to determine that check. Some checks may look like they failed when in reality there was simply no need to check them. Look for policies with `✘` and `✓` in check statuses. A check with a `⬇` means that it didn't determine if the policy was authorized or forbidden, and so moved on to the next check. `🌟` and `⛔` mean that the check was responsible for producing an authorized or forbidden (respectively) status. If no check results in a status (they all have `⬇`) then the policy is assumed to have failed. In some cases, however, the policy may have just been ignored, as described above. Admins and managers can create posts | ⛔: authorize if: actor.admin == true | ✘ | ⬇ authorize if: actor.manager == true | ✘ | ⬇
To remove the help text, you can pass the
help_text?: false option, which would leave you with:
Policy Breakdown Admins and managers can create posts | ⛔: authorize if: actor.admin == true | ✘ | ⬇ authorize if: actor.manager == true | ✘ | ⬇
The following configuration should only ever be used in development mode!
For security reasons, authorization errors don’t include any extra information, aside from
forbidden. To have authorization errors include a policy breakdown (without help text)
use the following config.
config :ash, :policies, show_policy_breakdowns?: true
It is generally safe to log authorization error details, even in production. This can be very helpful when investigating certain classes of issue.
To have ash automatically log each authorization failure, use
config :ash, :policies, log_policy_breakdowns: :error # Use whatever log level you'd like to use here