Ash adapter for Parc, the framework-agnostic PARC authorization contract. Parc
defines the request, the pipeline of preparers, the decider, and the
allow/deny/challenge/error verdict (see Parc and Parc.PolicyDecider); AshParc
binds that contract to Ash's policy DSL, so one policy, declared once, can
authorize every action across every resource.
AshParc exposes that one policy at two enforcement points:
can_perform/2: anAsh.Policy.SimpleCheckref for apoliciesblock, the strict yes/no gate on an action. It builds the request and runs the policy, since Ash invokes the check and wants a verdict (mapping inAshParc.PolicyCheck).build_request/3: theParc.Requestfor an Ash evaluation, for a resource-ownedAsh.Policy.FilterCheckthat row-scopes a read. AshParc only builds it; the resource runs the policy and turns the allowobligationsinto anAsh.Expritself, the one place column names live.
The same decider serves both points. On a read it returns {:allow, obligations};
the gate reads the verdict, and the filter check consumes the obligations.
Defining a policy
Declare the policy as a Parc.Pipeline (see Parc.Pipeline for prepare/decide
and Parc.PolicyDecider for the decider contract), with AshParc.ResetAshContext as
its first preparer to uplift the shared-context keys it trusts and drop the rest. The
decider branches on request.action.name to emit scope obligations on a read
({:allow, [{:scope, :organization, ids}]}), which the filter check below turns into
a filter.
Guarding resource actions
Name the policy from the resource's policies block. A write or generic action is
gated by can_perform/2; :attributes/:arguments name what to project into the
request (where organization_id lives, an instance attribute on a changeset, an
argument on a generic action), and :context lifts an argument or attribute into the
request context under a normalized key the preparers and decider read:
policy action(:update) do
authorize_if AshParc.can_perform(MyApp.AuthzPolicy, attributes: [:organization_id])
end
policy action(:archive) do
authorize_if AshParc.can_perform(MyApp.AuthzPolicy, arguments: [:organization_id])
end
policy action(:read_record) do
authorize_if AshParc.can_perform(MyApp.AuthzPolicy,
arguments: [:organization_id],
context: [target_user_id: {:argument, :user_id}]
)
endA read is row-scoped by a resource-owned Ash.Policy.FilterCheck. It builds the
request with build_request/3, runs it through the policy itself, and turns the
verdict into a filter, the only resource-specific piece, kept where column
knowledge belongs:
policy action_type(:read) do
authorize_if MyApp.Widget.ReadScope
end
defmodule MyApp.Widget.ReadScope do
use Ash.Policy.FilterCheck
@impl true
def filter(actor, authorizer, _opts) do
request = AshParc.build_request(actor, authorizer)
scope(MyApp.AuthzPolicy.run(request))
end
defp scope({:allow, [{:scope, :organization, ids}]}), do: expr(organization_id in ^ids)
defp scope({:deny, _reasons}), do: false
defp scope({:error, reason}), do: raise("scope decider error: #{inspect(reason)}")
endThe {:deny, _} clause returns false, which scopes the read to no rows only under
config :ash, policies: [no_filter_static_forbidden_reads?: false]; without it Ash
raises Ash.Error.Forbidden.
Summary
Functions
Builds the Parc.Request for an Ash policy evaluation context (a changeset,
query, or action input).
An AshParc.PolicyCheck ref for policy, used in an Ash resource policies block
Functions
@spec build_request(term(), map(), keyword()) :: Parc.Request.t()
Builds the Parc.Request for an Ash policy evaluation context (a changeset,
query, or action input).
The request-building half of the adapter. AshParc only builds the request; the
resource's Ash.Policy.FilterCheck runs the policy and turns the verdict into a
filter (see the moduledoc).
opts names what to pass from the action into the request:
:arguments: action arguments to pass intorequest.action.arguments(default[]).:attributes: instance attributes to pass intorequest.resource.attributes, on update and destroy (default[]).:context: arguments/attributes lifted intorequest.contextunder a normalized key (default[]); seecan_perform/2.
Raises ArgumentError when context carries no recognized subject.
An AshParc.PolicyCheck ref for policy, used in an Ash resource policies block:
authorize_if AshParc.can_perform(MyApp.Policy, attributes: [:organization_id])opts names what to pass from the action into the request:
:arguments: action arguments to pass intorequest.action.arguments(default[]).:attributes: instance attributes to pass intorequest.resource.attributes, on update and destroy (default[]).:context: a keyword list lifting an argument or instance attribute intorequest.contextunder a normalized key, e.g.[target_user_id: {:argument, :user_id}]or[..., {:attribute, :user_id}](default[]). The resource, which alone knows its action's argument/attribute names, declares the source; preparers and the decider read the normalized key, so they stay generic. SeeAshParc.RequestBuilder.
Raises ArgumentError at compile time on an invalid spec. The check carries no
permission; the decider derives meaning from the introspected action and the
prepared context.