defmodule Ash.Policy.Info do
@moduledoc """
An authorization extension for ash resources.
For more information, see `Ash.Policy.Authorizer`
"""
@type request :: Ash.Engine.Request.t()
alias Spark.Dsl.Extension
@doc "Whether or not Ash policy authorizer is configured to show policy breakdowns in error messages"
def show_policy_breakdowns? do
Application.get_env(:ash, :policies)[:show_policy_breakdowns?] || false
end
@doc "Whether or not Ash policy authorizer is configured to log policy breakdowns"
def log_policy_breakdowns do
Application.get_env(:ash, :policies)[:log_policy_breakdowns]
end
@doc "Whether or not Ash policy authorizer is configured to log successful policy breakdowns"
def log_successful_policy_breakdowns do
Application.get_env(:ash, :policies)[:log_successful_policy_breakdowns]
end
@doc "Gets the field policies relevant to a given field"
def field_policies_for_field(resource, field) do
Extension.get_persisted(resource, :fields_to_field_policies, %{})[field]
end
@doc """
A utility to determine if a given query/changeset would pass authorization.
*This is still experimental.*
"""
def strict_check(_actor, %{action: nil}, _) do
raise "Cannot use `strict_check/3` unless an action has been set on the query/changeset"
end
def strict_check(actor, %{__struct__: Ash.Query} = query, api) do
query = Ash.Query.set_context(query, %{private: %{pre_flight_authorization?: true}})
authorizer = %Ash.Policy.Authorizer{
actor: actor,
resource: query.resource,
action: query.action
}
case Ash.Policy.Authorizer.strict_check(authorizer, %{
api: api,
query: query,
changeset: nil
}) do
{:error, _error} ->
false
{:authorized, _} ->
true
{:filter, _, _} ->
true
_ ->
:maybe
end
end
def strict_check(actor, %{__struct__: Ash.Changeset} = changeset, api) do
changeset =
Ash.Changeset.set_context(changeset, %{private: %{pre_flight_authorization?: true}})
authorizer = %Ash.Policy.Authorizer{
actor: actor,
resource: changeset.resource,
action: changeset.action
}
case Ash.Policy.Authorizer.strict_check(authorizer, %{
api: api,
changeset: changeset,
query: nil
}) do
{:error, _error} ->
false
{:authorized, _} ->
true
{:filter, _, _} ->
:maybe
_ ->
:maybe
end
end
def describe_resource(resource) do
resource
|> policies()
|> describe_policies()
end
defp describe_policies(policies) do
Enum.map_join(policies, "\n", fn policy ->
case policy.condition do
empty when empty in [nil, []] ->
describe_checks(policy.policies)
conditions ->
"When:\n" <>
indent(describe_conditions(conditions)) <>
"\nThen:\n" <> indent(describe_checks(policy.policies))
end
end)
end
defp describe_checks(checks) do
checks
|> Enum.map_join("\n", fn
%{type: type, check_module: check_module, check_opts: check_opts} ->
"#{type}: #{check_module.describe(check_opts)}"
end)
|> Kernel.<>("\n")
end
defp describe_conditions(conditions) do
Enum.map_join(conditions, " and ", fn
{check_module, check_opts} ->
check_module.describe(check_opts)
end)
end
defp indent(string) do
string
|> String.split("\n")
|> Enum.map_join("\n", fn line ->
" " <> line
end)
end
def field_policies(resource) do
resource
|> Extension.get_entities([:field_policies])
|> set_access_type(default_access_type(resource))
end
def policies(resource) do
resource
|> Extension.get_entities([:policies])
|> set_access_type(default_access_type(resource))
end
def default_access_type(resource) do
Extension.get_opt(resource, [:policies], :default_access_type, :strict, false)
end
# This should be done at compile time
defp set_access_type(policies, default) when is_list(policies) do
Enum.map(policies, &set_access_type(&1, default))
end
defp set_access_type(
%Ash.Policy.Policy{
policies: policies,
condition: conditions,
checks: checks,
access_type: access_type
} = policy,
default
) do
%{
policy
| policies: set_access_type(policies, access_type || default),
condition: set_access_type(conditions, access_type || default),
checks: set_access_type(checks, default),
access_type: access_type || default
}
end
defp set_access_type(%Ash.Policy.Check{check_opts: check_opts} = check, default) do
%{check | check_opts: Keyword.update(check_opts, :access_type, default, &(&1 || default))}
end
defp set_access_type({module, opts}, default),
do: {module, Keyword.update(opts, :access_type, default, &(&1 || default))}
defp set_access_type(other, _), do: other
end