AshParc (ash_parc v0.1.0)

Copy Markdown View Source

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:

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}]
               )
end

A 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)}")
end

The {: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

build_request(actor, context, opts \\ [])

@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 into request.action.arguments (default []).
  • :attributes: instance attributes to pass into request.resource.attributes, on update and destroy (default []).
  • :context: arguments/attributes lifted into request.context under a normalized key (default []); see can_perform/2.

Raises ArgumentError when context carries no recognized subject.

can_perform(policy, opts \\ [])

@spec can_perform(
  module(),
  keyword()
) :: {module(), keyword()}

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 into request.action.arguments (default []).
  • :attributes: instance attributes to pass into request.resource.attributes, on update and destroy (default []).
  • :context: a keyword list lifting an argument or instance attribute into request.context under 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. See AshParc.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.