This commit is contained in:
Zach Daniel 2019-12-26 16:33:35 -05:00
parent 694b7b362e
commit 4d3688532f
No known key found for this signature in database
GPG key ID: A57053A671EE649E
32 changed files with 777 additions and 264 deletions

View file

@ -129,3 +129,6 @@ end
contain *part* of the filter, requiring that the whole thing is covered by all contain *part* of the filter, requiring that the whole thing is covered by all
of the `and`s at least of the `and`s at least
* add `init` to checks, and error check their construction when building the DSL * add `init` to checks, and error check their construction when building the DSL
* Support filtering side loads. Especially useful in authorization code?
* Booleans need to not support `nil` values. That has to be a different type.
boolean filter/authorization logic is greatly enhanced if that is the case.

View file

@ -32,7 +32,8 @@ defmodule Ash.Actions.Create do
Ash.Authorization.Request.new( Ash.Authorization.Request.new(
resource: resource, resource: resource,
authorization_steps: action.authorization_steps, authorization_steps: action.authorization_steps,
changeset: changeset changeset: changeset,
source: "create request"
) )
Authorizer.authorize(user, %{}, [auth_request]) Authorizer.authorize(user, %{}, [auth_request])
@ -96,10 +97,21 @@ defmodule Ash.Actions.Create do
end end
end) end)
changeset =
resource
|> struct()
|> Ecto.Changeset.cast(attributes_with_defaults, allowed_keys)
|> Map.put(:action, :create)
resource resource
|> struct() |> Ash.attributes()
|> Ecto.Changeset.cast(attributes_with_defaults, allowed_keys) |> Enum.reject(&Map.get(&1, :allow_nil?))
|> Map.put(:action, :create) |> Enum.reduce(changeset, fn attr, changeset ->
case Ecto.Changeset.get_field(changeset, attr.name) do
nil -> Ecto.Changeset.add_error(changeset, attr.name, "must not be nil")
_value -> changeset
end
end)
end end
defp default(%{default: {:constant, value}}), do: value defp default(%{default: {:constant, value}}), do: value

View file

@ -28,7 +28,8 @@ defmodule Ash.Actions.Destroy do
Ash.Authorization.Request.new( Ash.Authorization.Request.new(
resource: resource, resource: resource,
authorization_steps: action.authorization_steps, authorization_steps: action.authorization_steps,
destroy: record destroy: record,
source: "destroy request"
) )
Authorizer.authorize(user, %{}, [auth_request]) Authorizer.authorize(user, %{}, [auth_request])

View file

@ -8,11 +8,13 @@ defmodule Ash.Actions.Read do
side_loads = Keyword.get(params, :side_load, []) side_loads = Keyword.get(params, :side_load, [])
page_params = Keyword.get(params, :page, []) page_params = Keyword.get(params, :page, [])
# TODO: Going to have to figure out side loads. I don't
# think that they can actually reasonably share facts :/
with %Ash.Filter{errors: [], authorizations: filter_auths} = filter <- with %Ash.Filter{errors: [], authorizations: filter_auths} = filter <-
Ash.Filter.parse(resource, filter), Ash.Filter.parse(resource, filter),
{:ok, side_load_auths} <- SideLoad.process(resource, side_loads, filter), {:ok, side_load_auths} <- SideLoad.process(resource, side_loads, filter),
{:auth, :authorized} <- :ok <- do_authorize(params, side_load_auths ++ filter_auths),
{:auth, do_authorize(params, side_load_auths ++ filter_auths)},
query <- Ash.DataLayer.resource_to_query(resource), query <- Ash.DataLayer.resource_to_query(resource),
{:ok, sort} <- Ash.Actions.Sort.process(resource, sort), {:ok, sort} <- Ash.Actions.Sort.process(resource, sort),
{:ok, sorted_query} <- Ash.DataLayer.sort(query, sort, resource), {:ok, sorted_query} <- Ash.DataLayer.sort(query, sort, resource),
@ -26,16 +28,23 @@ defmodule Ash.Actions.Read do
SideLoad.side_load(resource, paginator, side_loads, api) SideLoad.side_load(resource, paginator, side_loads, api)
else else
%Ash.Filter{errors: errors} -> {:error, errors} %Ash.Filter{errors: errors} -> {:error, errors}
{:auth, :forbidden} -> {:error, "forbidden"}
{:error, error} -> {:error, error} {:error, error} -> {:error, error}
end end
end end
defp do_authorize(params, auths) do defp do_authorize(params, auths) do
if params[:authorization] do if params[:authorization] do
strict_access =
case Keyword.fetch(params[:authorization], :strict_access?) do
{:ok, value} -> value
:error -> true
end
auths = Enum.map(auths, fn auth -> %{auth | strict_access?: strict_access} end)
Authorizer.authorize(params[:authorization][:user], %{}, auths) Authorizer.authorize(params[:authorization][:user], %{}, auths)
else else
:authorized :ok
end end
end end
end end

View file

@ -38,7 +38,8 @@ defmodule Ash.Actions.Update do
Ash.Authorization.Request.new( Ash.Authorization.Request.new(
resource: resource, resource: resource,
authorization_steps: action.authorization_steps, authorization_steps: action.authorization_steps,
changeset: changeset changeset: changeset,
source: "update action"
) )
Authorizer.authorize(user, %{}, [auth_request]) Authorizer.authorize(user, %{}, [auth_request])
@ -88,8 +89,22 @@ defmodule Ash.Actions.Update do
|> Ash.attributes() |> Ash.attributes()
|> Enum.map(& &1.name) |> Enum.map(& &1.name)
record changeset =
|> Ecto.Changeset.cast(attributes, allowed_keys) record
|> Map.put(:action, :update) |> Ecto.Changeset.cast(attributes, allowed_keys)
|> Map.put(:action, :update)
resource
|> Ash.attributes()
|> Enum.reject(&Map.get(&1, :allow_nil?))
|> Enum.reduce(changeset, fn attr, changeset ->
case Ecto.Changeset.fetch_change(changeset, attr.name) do
{:ok, nil} ->
Ecto.Changeset.add_error(changeset, attr.name, "must not be nil")
_ ->
changeset
end
end)
end end
end end

View file

@ -3,16 +3,25 @@ defmodule Ash.Api do
opts: [ opts: [
interface?: :boolean, interface?: :boolean,
max_page_size: :integer, max_page_size: :integer,
default_page_size: :integer default_page_size: :integer,
# TODO: Support configuring this from env variables
authorization_explanations: [:boolean]
],
defaults: [
interface?: true,
max_page_size: 100,
default_page_size: 25,
authorization_explanations: false
], ],
defaults: [interface?: true, max_page_size: 100, default_page_size: 25],
describe: [ describe: [
interface?: interface?:
"If set to false, no code interface is defined for this resource e.g `MyApi.create(...)` is not defined.", "If set to false, no code interface is defined for this resource e.g `MyApi.create(...)` is not defined.",
max_page_size: max_page_size:
"The maximum page size for any read action. Any request for a higher page size will simply use this number. Uses the smaller of the Api's or Resource's value.", "The maximum page size for any read action. Any request for a higher page size will simply use this number. Uses the smaller of the Api's or Resource's value.",
default_page_size: default_page_size:
"The default page size for any read action. If no page size is specified, this value is used. Uses the smaller of the Api's or Resource's value." "The default page size for any read action. If no page size is specified, this value is used. Uses the smaller of the Api's or Resource's value.",
authorization_explanations:
"A setting that determines whether or not verbose authorization errors should be returned."
], ],
constraints: [ constraints: [
max_page_size: max_page_size:
@ -63,6 +72,7 @@ defmodule Ash.Api do
@interface? opts[:interface?] @interface? opts[:interface?]
@side_load_type :simple @side_load_type :simple
@side_load_config [] @side_load_config []
@authorization_explanations opts[:authorization_explanations] || false
Module.register_attribute(__MODULE__, :mix_ins, accumulate: true) Module.register_attribute(__MODULE__, :mix_ins, accumulate: true)
Module.register_attribute(__MODULE__, :resources, accumulate: true) Module.register_attribute(__MODULE__, :resources, accumulate: true)
@ -169,6 +179,7 @@ defmodule Ash.Api do
def mix_ins(), do: @mix_ins def mix_ins(), do: @mix_ins
def resources(), do: @resources def resources(), do: @resources
def side_load_config(), do: {@side_load_type, @side_load_config} def side_load_config(), do: {@side_load_type, @side_load_config}
def authorization_explanations(), do: @authorization_explanations
def get_resource(mod) when mod in @resources, do: {:ok, mod} def get_resource(mod) when mod in @resources, do: {:ok, mod}

View file

@ -11,12 +11,6 @@ defmodule Ash.Authorization do
@type request :: Ash.Authorization.Request.t() @type request :: Ash.Authorization.Request.t()
# Required sideloads before checks are run @type side_load :: {:side_load, Keyword.t()}
# @type side_load_instruction :: {:side_load, Ash.side_load()} @type prepare_instruction :: side_load
# The result for this check is predetermined for all records
# that could be passed in from this request.
@type decision :: {:decision, boolean}
# @type precheck_context :: {:context, %{optional(atom) => term}}
# @type precheck_result :: side_load_instruction() | decision() | precheck_context()
@type precheck_result :: decision()
end end

View file

@ -22,29 +22,30 @@ defmodule Ash.Authorization.Authorizer do
authorization_steps = authorization_steps_with_relationship_path(requests_by_relationship) authorization_steps = authorization_steps_with_relationship_path(requests_by_relationship)
{facts, instructions} = strict_check_facts(user, requests) facts = strict_check_facts(user, requests)
solve(authorization_steps, facts, instructions) solve(authorization_steps, facts, facts)
end end
defp solve(authorization_steps, facts, instructions) do defp solve(authorization_steps, facts, strict_check_facts) do
case SatSolver.solve(authorization_steps, facts) do case SatSolver.solve(authorization_steps, facts) do
{:error, :unsatisfiable} -> {:error, :unsatisfiable} ->
:forbidden {:error,
Ash.Error.Forbidden.exception(
authorization_steps: authorization_steps,
facts: facts,
strict_check_facts: strict_check_facts
)}
{:ok, scenario} -> {:ok, scenario} ->
case get_all_scenarios(authorization_steps, scenario, facts, instructions) do scenarios = get_all_scenarios(authorization_steps, scenario, facts)
{:ok, []} ->
:forbidden
{:ok, scenarios} -> irrelevant_clauses = irrelevant_clauses(scenarios)
irrelevant_clauses = irrelevant_clauses(scenarios)
scenarios scenarios
|> Enum.map(&Map.drop(&1, irrelevant_clauses)) |> Enum.map(&Map.drop(&1, irrelevant_clauses))
|> Enum.uniq() |> Enum.uniq()
|> verify_scenarios(facts, instructions) |> verify_scenarios(authorization_steps, facts, strict_check_facts)
end
end end
end end
@ -72,15 +73,14 @@ defmodule Ash.Authorization.Authorizer do
authorization_steps, authorization_steps,
scenario, scenario,
facts, facts,
instructions,
negations \\ [], negations \\ [],
scenarios \\ nil scenarios \\ []
) do ) do
scenarios = scenarios || [scenario] scenarios = [scenario | scenarios]
case scenario_is_reality(scenario, facts) do case scenario_is_reality(scenario, facts) do
:reality -> :reality ->
{:ok, scenarios} scenarios
:not_reality -> :not_reality ->
raise "SAT SOLVER ERROR" raise "SAT SOLVER ERROR"
@ -94,28 +94,40 @@ defmodule Ash.Authorization.Authorizer do
authorization_steps, authorization_steps,
scenario_after_negation, scenario_after_negation,
facts, facts,
instructions,
negations_assuming_scenario_false, negations_assuming_scenario_false,
scenarios scenarios
) )
{:error, :unsatisfiable} -> {:error, :unsatisfiable} ->
{:ok, [scenario | scenarios]} scenarios
end end
end end
end end
defp verify_scenarios(scenarios, facts, _instructions) do defp verify_scenarios(scenarios, authorization_steps, facts, strict_check_facts) do
if any_scenarios_reality?(scenarios, facts) do if any_scenarios_reality?(scenarios, facts) do
:authorized :ok
else else
# TODO: Start gathering facts. If no facts remain to be gathered, case fetch_facts(scenarios, facts) do
# and no scenario is reality, then we are forbidden. :all_facts_fetched ->
# Use instructions from strict checks here. {:error,
:forbidden Ash.Error.Forbidden.exception(
scenarios: scenarios,
authorization_steps: authorization_steps,
facts: facts,
strict_check_facts: strict_check_facts
)}
{:ok, new_facts} ->
solve(authorization_steps, new_facts, strict_check_facts)
end
end end
end end
defp fetch_facts(scenarios, facts) do
Ash.Authorization.FactFinder.find_facts(scenarios, facts)
end
defp any_scenarios_reality?(scenarios, facts) do defp any_scenarios_reality?(scenarios, facts) do
Enum.any?(scenarios, fn scenario -> Enum.any?(scenarios, fn scenario ->
scenario_is_reality(scenario, facts) == :reality scenario_is_reality(scenario, facts) == :reality
@ -123,17 +135,20 @@ defmodule Ash.Authorization.Authorizer do
end end
defp scenario_is_reality(scenario, facts) do defp scenario_is_reality(scenario, facts) do
Enum.reduce_while(scenario, :reality, fn {fact, requirement}, status -> scenario
|> Map.drop([true, false])
|> Enum.reduce_while(:reality, fn {fact, requirement}, status ->
case Map.fetch(facts, fact) do case Map.fetch(facts, fact) do
{:ok, value} -> {:ok, value} ->
if value == requirement do cond do
if status == :reality do value == requirement ->
{:cont, :reality}
else
{:cont, status} {:cont, status}
end
else value == :unknowable ->
{:halt, :not_reality} {:cont, :maybe}
true ->
{:halt, :not_reality}
end end
:error -> :error ->
@ -143,10 +158,8 @@ defmodule Ash.Authorization.Authorizer do
end end
defp strict_check_facts(user, requests) do defp strict_check_facts(user, requests) do
Enum.reduce(requests, {%{true: true, false: false}, []}, fn request, {facts, instructions} -> Enum.reduce(requests, %{true: true, false: false}, fn request, facts ->
{new_facts, new_instructions} = Ash.Authorization.Checker.strict_check(user, request, facts) Ash.Authorization.Checker.strict_check(user, request, facts)
{new_facts, instructions ++ new_instructions}
end) end)
end end

View file

@ -7,7 +7,7 @@ defmodule Ash.Authorization.Check.AttributeEquals do
@impl true @impl true
def describe(opts) do def describe(opts) do
"record.#{opts[:field]} == #{inspect(opts[:value])}" "this_record.#{opts[:field]} == #{inspect(opts[:value])}"
end end
@impl true @impl true
@ -17,19 +17,21 @@ defmodule Ash.Authorization.Check.AttributeEquals do
case Ash.Filter.parse(request.resource, [{field, eq: value}]) do case Ash.Filter.parse(request.resource, [{field, eq: value}]) do
%{errors: []} = parsed -> %{errors: []} = parsed ->
cond do if Ash.Filter.strict_subset_of?(parsed, request.filter) do
Ash.Filter.contains?(parsed, request.filter) -> {:ok, true}
[decision: true] else
case Ash.Filter.parse(request.resource, [{field, not_eq: value}]) do
request.strict_access? -> %{errors: []} = parsed ->
[decision: false] if Ash.Filter.strict_subset_of?(parsed, request.filter) do
{:ok, false}
true -> else
[] {:ok, :unknown}
end
end
end end
%{errors: errors} -> %{errors: errors} ->
[error: errors] {:error, errors}
end end
end end
end end

View file

@ -6,16 +6,28 @@ defmodule Ash.Authorization.Check do
@type options :: Keyword.t() @type options :: Keyword.t()
@callback strict_check(Ash.user(), Ash.Authorization.request(), options) :: @callback strict_check(Ash.user(), Ash.Authorization.request(), options) :: boolean | :unknown
list(Ash.Authorization.precheck_result())
@callback check(Ash.user(), Ash.Authorization.request(), options) :: boolean @callback prepare(Ash.user(), Ash.Authorization.request(), options) ::
list(Ash.Authorization.prepare_instruction()) | {:error, Ash.error()}
@callback check(Ash.user(), list(Ash.record()), Ash.Authorization.request(), options) ::
{:ok, boolean | list(Ash.record())} | {:error, Ash.error()}
@callback describe(options()) :: String.t() @callback describe(options()) :: String.t()
@optional_callbacks check: 3 @optional_callbacks check: 4, prepare: 3
def defines_check?(module) do
:erlang.function_exported(module, :check, 4)
end
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
@behaviour Ash.Authorization.Check @behaviour Ash.Authorization.Check
@impl true
def prepare(_, _, _), do: []
defoverridable prepare: 3
end end
end end
end end

View file

@ -6,8 +6,8 @@ defmodule Ash.Authorization.Check.RelatedToUserVia do
end end
@impl true @impl true
def describe(relationship) do def describe(opts) do
"#{Enum.join(relationship, ".")} is the user" "#{Enum.join(opts[:relationship], ".")} is the user"
end end
@impl true @impl true
@ -20,22 +20,48 @@ defmodule Ash.Authorization.Check.RelatedToUserVia do
case Ash.Filter.parse(request.resource, candidate_filter) do case Ash.Filter.parse(request.resource, candidate_filter) do
%{errors: []} = parsed -> %{errors: []} = parsed ->
cond do if Ash.Filter.strict_subset_of?(parsed, request.filter) do
Ash.Filter.contains?(parsed, request.filter) -> {:ok, true}
[decision: true] else
{:ok, :unknown}
request.strict_access? ->
[decision: false]
true ->
[]
end end
%{errors: errors} -> %{errors: errors} ->
[error: errors] {:error, errors}
end end
end end
@impl true
def prepare(_user, _request, opts) do
[side_load: put_into_relationship_path(opts[:relationship], [])]
end
@impl true
def check(user, records, _request, options) do
matches =
Enum.filter(records, fn record ->
related_records = get_related(record, options[:relationship])
Enum.any?(related_records, fn related ->
primary_key = Ash.primary_key(user)
Map.take(related, primary_key) == Map.take(user, primary_key)
end)
end)
{:ok, matches}
end
defp get_related(record, []), do: record
defp get_related(record, [relationship | rest]) do
Enum.flat_map(record, fn record ->
record
|> Map.get(relationship)
|> List.wrap()
|> Enum.map(&get_related(&1, rest))
end)
end
defp put_into_relationship_path([], value), do: value defp put_into_relationship_path([], value), do: value
defp put_into_relationship_path([item | rest], value) do defp put_into_relationship_path([item | rest], value) do

View file

@ -16,6 +16,6 @@ defmodule Ash.Authorization.Check.Static do
@impl true @impl true
def strict_check(_user, _request, options) do def strict_check(_user, _request, options) do
[decision: options[:result]] {:ok, options[:result]}
end end
end end

View file

@ -12,6 +12,6 @@ defmodule Ash.Authorization.Check.UserAttribute do
@impl true @impl true
def strict_check(user, _request, options) do def strict_check(user, _request, options) do
[decision: Map.get(user, options[:field]) == options[:value]] {:ok, Map.fetch(user, options[:field]) == {:ok, options[:value]}}
end end
end end

View file

@ -7,7 +7,7 @@ defmodule Ash.Authorization.Check.UserAttributeMatchesRecord do
@impl true @impl true
def describe(opts) do def describe(opts) do
"user.#{opts[:user_field]} == record.#{opts[:record_field]}" "user.#{opts[:user_field]} == this_record.#{opts[:record_field]}"
end end
@impl true @impl true
@ -17,19 +17,14 @@ defmodule Ash.Authorization.Check.UserAttributeMatchesRecord do
case Ash.Filter.parse(request.resource, [{record_field, eq: Map.get(user, user_field)}]) do case Ash.Filter.parse(request.resource, [{record_field, eq: Map.get(user, user_field)}]) do
%{errors: []} = parsed -> %{errors: []} = parsed ->
cond do if Ash.Filter.strict_subset_of?(parsed, request.filter) do
Ash.Filter.contains?(parsed, request.filter) -> {:ok, true}
[decision: true] else
{:ok, :unknown}
request.strict_access? ->
[decision: false]
true ->
[]
end end
%{errors: errors} -> %{errors: errors} ->
[error: errors] {:error, errors}
end end
end end
end end

View file

@ -1,37 +1,44 @@
defmodule Ash.Authorization.Checker do defmodule Ash.Authorization.Checker do
def strict_check(user, request, facts) do def strict_check(user, request, facts) do
Enum.reduce( request.authorization_steps
request.authorization_steps, |> Enum.reduce(facts, fn {_step, condition}, facts ->
{facts, []}, case Map.fetch(facts, {request.relationship, condition}) do
fn { {:ok, _boolean_result} ->
_step_type, facts
condition
},
{facts, instructions} ->
case Map.fetch(facts, {request.relationship, condition}) do
{:ok, _boolean_result} ->
{facts, instructions}
:error -> :error ->
case do_strict_check(condition, user, request) do case do_strict_check(condition, user, request) do
{:unknown, new_instructions} -> :unknown ->
{facts, instructions ++ new_instructions} facts
{boolean, new_instructions} -> :unknowable ->
{Map.put(facts, {request.relationship, condition}, boolean), Map.put(facts, {request.relationship, condition}, :unknowable)
instructions ++ new_instructions}
end boolean ->
end Map.put(facts, {request.relationship, condition}, boolean)
end
end end
) end)
end end
defp do_strict_check({module, opts}, user, request) do defp do_strict_check({module, opts}, user, request) do
strict_check_results = module.strict_check(user, request, opts) case module.strict_check(user, request, opts) do
{:ok, boolean} when is_boolean(boolean) ->
boolean
case Keyword.fetch(strict_check_results, :decision) do {:ok, :unknown} ->
{:ok, boolean} -> {boolean, []} cond do
:error -> {:unknown, []} request.strict_access? ->
# This means "we needed a fact that we have no way of getting"
# Because the fact was needed in the `strict_check` step
:unknowable
Ash.Authorization.Check.defines_check?(module) ->
:unknown
true ->
:unknowable
end
end end
end end
end end

View file

@ -0,0 +1,8 @@
defmodule Ash.Authorization.FactFinder do
def find_facts(scenarios, facts) do
# IO.inspect(scenarios, label: "Scenarios")
# IO.inspect(facts, label: "Facts")
:all_facts_fetched
end
end

View file

@ -5,7 +5,8 @@ defmodule Ash.Authorization.Request do
:filter, :filter,
:action_type, :action_type,
:relationship, :relationship,
:strict_access? :strict_access?,
:source
] ]
@type t :: %__MODULE__{ @type t :: %__MODULE__{
@ -14,7 +15,8 @@ defmodule Ash.Authorization.Request do
authorization_steps: list(term), authorization_steps: list(term),
filter: Ash.Filter.t(), filter: Ash.Filter.t(),
relationship: list(atom), relationship: list(atom),
strict_access?: boolean strict_access?: boolean,
source: String.t()
} }
def new(opts) do def new(opts) do

View file

@ -11,6 +11,15 @@ defmodule Ash.Authorization.SatSolver do
end end
end) end)
facts =
Enum.reduce(facts, %{}, fn {key, value}, acc ->
if value == :unknowable do
acc
else
Map.put(acc, key, value)
end
end)
facts_expression = facts_to_statement(facts) facts_expression = facts_to_statement(facts)
negations = negations =
@ -82,11 +91,11 @@ defmodule Ash.Authorization.SatSolver do
defp solutions_to_predicate_values({:error, error}, _), do: {:error, error} defp solutions_to_predicate_values({:error, error}, _), do: {:error, error}
# Is it really this easy
defp compile_authorization_steps_expression([{:authorize_if, clause}], facts) do defp compile_authorization_steps_expression([{:authorize_if, clause}], facts) do
case Map.fetch(facts, clause) do case Map.fetch(facts, clause) do
{:ok, true} -> true {:ok, true} -> true
{:ok, false} -> false {:ok, false} -> false
{:ok, :unknowable} -> false
:error -> clause :error -> clause
end end
end end
@ -99,6 +108,9 @@ defmodule Ash.Authorization.SatSolver do
{:ok, false} -> {:ok, false} ->
compile_authorization_steps_expression(rest, facts) compile_authorization_steps_expression(rest, facts)
{:ok, :unknowable} ->
compile_authorization_steps_expression(rest, facts)
:error -> :error ->
{:or, clause, compile_authorization_steps_expression(rest, facts)} {:or, clause, compile_authorization_steps_expression(rest, facts)}
end end
@ -106,31 +118,56 @@ defmodule Ash.Authorization.SatSolver do
defp compile_authorization_steps_expression([{:authorize_unless, clause}], facts) do defp compile_authorization_steps_expression([{:authorize_unless, clause}], facts) do
case Map.fetch(facts, clause) do case Map.fetch(facts, clause) do
{:ok, true} -> false {:ok, true} ->
{:ok, false} -> true false
:error -> {:not, clause}
{:ok, false} ->
true
{:ok, :unknowable} ->
false
:error ->
{:not, clause}
end end
end end
defp compile_authorization_steps_expression([{:authorize_unless, clause} | rest], facts) do defp compile_authorization_steps_expression([{:authorize_unless, clause} | rest], facts) do
case Map.fetch(facts, clause) do case Map.fetch(facts, clause) do
{:ok, true} -> compile_authorization_steps_expression(rest, facts) {:ok, true} ->
{:ok, false} -> true compile_authorization_steps_expression(rest, facts)
:error -> {:or, {:not, clause}, compile_authorization_steps_expression(rest, facts)}
{:ok, false} ->
true
{:ok, :unknowable} ->
compile_authorization_steps_expression(rest, facts)
:error ->
{:or, {:not, clause}, compile_authorization_steps_expression(rest, facts)}
end end
end end
defp compile_authorization_steps_expression([{:forbid_if, clause}], facts) do defp compile_authorization_steps_expression([{:forbid_if, clause}], facts) do
case Map.fetch(facts, clause) do case Map.fetch(facts, clause) do
{:ok, true} -> false {:ok, true} ->
{:ok, false} -> true false
:error -> {:not, clause}
{:ok, false} ->
true
{:ok, :unknowable} ->
false
:error ->
{:not, clause}
end end
end end
defp compile_authorization_steps_expression([{:forbid_if, clause} | rest], facts) do defp compile_authorization_steps_expression([{:forbid_if, clause} | rest], facts) do
case Map.fetch(facts, clause) do case Map.fetch(facts, clause) do
{:ok, true} -> false {:ok, true} -> false
{:ok, :unknowable} -> false
{:ok, false} -> compile_authorization_steps_expression(rest, facts) {:ok, false} -> compile_authorization_steps_expression(rest, facts)
:error -> {:and, {:not, clause}, compile_authorization_steps_expression(rest, facts)} :error -> {:and, {:not, clause}, compile_authorization_steps_expression(rest, facts)}
end end
@ -140,6 +177,7 @@ defmodule Ash.Authorization.SatSolver do
case Map.fetch(facts, clause) do case Map.fetch(facts, clause) do
{:ok, true} -> true {:ok, true} -> true
{:ok, false} -> false {:ok, false} -> false
{:ok, :unknowable} -> false
:error -> clause :error -> clause
end end
end end
@ -148,6 +186,7 @@ defmodule Ash.Authorization.SatSolver do
case Map.fetch(facts, clause) do case Map.fetch(facts, clause) do
{:ok, true} -> compile_authorization_steps_expression(rest, facts) {:ok, true} -> compile_authorization_steps_expression(rest, facts)
{:ok, false} -> false {:ok, false} -> false
{:ok, :unknowable} -> false
:error -> {:and, clause, compile_authorization_steps_expression(rest, facts)} :error -> {:and, clause, compile_authorization_steps_expression(rest, facts)}
end end
end end

103
lib/ash/error/forbidden.ex Normal file
View file

@ -0,0 +1,103 @@
defmodule Ash.Error.Forbidden do
@moduledoc "Raised when authorization for an action fails"
defexception [:scenarios, :authorization_steps, :facts, :strict_check_facts]
# TODO: Use better logic to format this
def message(error) do
header = "forbidden:\n\n"
explained_steps = explain_steps(error.authorization_steps, error.facts)
explained_facts = explain_facts(error.facts, error.strict_check_facts || %{})
main_message =
header <>
"Facts Gathered\n" <>
indent(explained_facts) <> "\n\nAuthorization Steps:\n" <> indent(explained_steps)
main_message <> "\n\nScenarios:\n" <> indent(explain_scenarios(error.scenarios))
end
defp explain_scenarios(scenarios) when scenarios in [nil, []] do
"""
No scenarios found. Under construction.
Eventually, scenarios will explain what data you could change to make the request possible.
"""
end
defp explain_scenarios(scenarios) do
"""
#{Enum.count(scenarios)} found. Under construction.
Eventually, scenarios will explain what data you could change to make the request possible.
"""
end
defp explain_facts(facts, strict_check_facts) do
facts
|> Map.drop([true, false])
|> Enum.map_join("\n", fn {{rel, {mod, opts}}, status} ->
gets_star? =
Map.fetch(strict_check_facts, {rel, {mod, opts}}) in [
{:ok, true},
{:ok, false}
]
star =
if gets_star? do
""
else
""
end
if rel == [] do
status_to_mark(status) <> " " <> mod.describe(opts) <> star
else
status_to_mark(status) <> " " <> Enum.join(rel, ".") <> " " <> mod.describe(opts) <> star
end
end)
end
defp status_to_mark(true), do: ""
defp status_to_mark(false), do: ""
defp status_to_mark(:unknowable), do: "?!"
defp status_to_mark(nil), do: "?"
defp indent(string) do
string
|> String.split("\n")
|> Enum.map(fn line -> " " <> line end)
|> Enum.join("\n")
end
defp explain_steps(sets_of_authorization_steps, facts) do
Enum.map_join(sets_of_authorization_steps, "---", fn authorization_steps ->
authorization_steps
|> Enum.map(fn {step, {relationship, {mod, opts}}} ->
mark = step_to_mark(step, Map.get(facts, {relationship, {mod, opts}}))
if relationship == [] do
to_string(step) <> ": " <> mod.describe(opts) <> " " <> mark
else
to_string(step) <>
": #{Enum.join(relationship, ".")} " <> mod.describe(opts) <> " " <> mark
end
end)
|> Enum.join("\n")
end)
end
defp step_to_mark(:authorize_if, true), do: ""
defp step_to_mark(:authorize_if, false), do: ""
defp step_to_mark(:authorize_if, _), do: ""
defp step_to_mark(:forbid_if, true), do: ""
defp step_to_mark(:forbid_if, false), do: ""
defp step_to_mark(:forbid_if, _), do: ""
defp step_to_mark(:authorize_unless, true), do: ""
defp step_to_mark(:authorize_unless, false), do: ""
defp step_to_mark(:authorize_unless, _), do: ""
defp step_to_mark(:forbid_unless, true), do: ""
defp step_to_mark(:forbid_unless, false), do: ""
defp step_to_mark(:forbid_unless, _), do: ""
end

View file

@ -26,10 +26,10 @@ defmodule Ash.Filter.And do
%__MODULE__{left: left, right: right} %__MODULE__{left: left, right: right}
end end
def contains?(%{left: left, right: right}, predicate) do def strict_subset_of?(attribute, %{left: left, right: right}, predicate) do
Ash.Filter.predicate_contains?(left, predicate) or Ash.Filter.predicate_strict_subset_of?(attribute, left, predicate) or
Ash.Filter.predicate_contains?(right, predicate) Ash.Filter.predicate_strict_subset_of?(attribute, right, predicate)
end end
def contains?(_, _), do: false def strict_subset_of?(_, _), do: false
end end

View file

@ -11,13 +11,29 @@ defmodule Ash.Filter.Eq do
end end
end end
def contains?(%__MODULE__{value: value}, %__MODULE__{value: candidate}) do def strict_subset_of?(
%{type: :boolean, allow_nil?: false},
%__MODULE__{value: true},
%Ash.Filter.NotEq{value: false}
) do
true
end
def strict_subset_of?(
%{type: :boolean, allow_nil?: false},
%__MODULE__{value: false},
%Ash.Filter.NotEq{value: true}
) do
true
end
def strict_subset_of?(_attribute, %__MODULE__{value: value}, %__MODULE__{value: candidate}) do
value == candidate value == candidate
end end
def contains?(%__MODULE__{value: value}, %Ash.Filter.In{values: [candidate]}) do def strict_subset_of?(_attribute, %__MODULE__{value: value}, %Ash.Filter.In{values: [candidate]}) do
value == candidate value == candidate
end end
def contains?(_, _), do: false def strict_subset_of?(_, _, _), do: false
end end

View file

@ -49,54 +49,43 @@ defmodule Ash.Filter do
authorization_steps: authorization_steps, authorization_steps: authorization_steps,
filter: parsed_filter, filter: parsed_filter,
action_type: :read, action_type: :read,
relationship: path relationship: path,
source: "#{Enum.join(path, ".")} filter"
) )
) )
end end
@doc "Returns true if the second argument is a strict subset (always returns the same or less data) as the first" @doc """
def strict_subset?(%{not: nil, ors: [], relationships: rels, attributes: attrs}, %{ Returns true if the second argument is a strict subset (always returns the same or less data) of the first
not: nil, """
ors: [], def strict_subset_of?(nil, nil), do: true
relationships: rels,
attributes: attrs
})
when attrs == %{} and rels == %{} do
true
end
def strict_subset?(_, %{not: nil, ors: [], relationships: rels, attributes: attrs}) def strict_subset_of?(_, nil), do: false
when attrs == %{} and rels == %{} do
end
def contains?(nil, nil), do: true def strict_subset_of?(filter, candidate) do
def contains?(_, nil), do: false
def contains?(filter, candidate) do
unless filter.ors in [[], nil], do: raise("Can't do ors contains yet") unless filter.ors in [[], nil], do: raise("Can't do ors contains yet")
unless filter.not in [[], nil], do: raise("Can't do not contains yet") unless filter.not in [[], nil], do: raise("Can't do not contains yet")
unless candidate.ors in [[], nil], do: raise("Can't do ors contains yet") unless candidate.ors in [[], nil], do: raise("Can't do ors contains yet")
unless candidate.not in [[], nil], do: raise("Can't do not contains yet") unless candidate.not in [[], nil], do: raise("Can't do not contains yet")
attributes_contained? = attributes_contained? =
Enum.all?(filter.attributes, fn {attr, predicate} -> Enum.any?(filter.attributes, fn {attr, predicate} ->
contains_attribute?(candidate, attr, predicate) contains_attribute?(candidate, attr, predicate)
end) end)
relationships_contained? = relationships_contained? =
Enum.all?(filter.relationships, fn {relationship, relationship_filter} -> Enum.any?(filter.relationships, fn {relationship, relationship_filter} ->
contains_relationship?(candidate, relationship, relationship_filter) contains_relationship?(candidate, relationship, relationship_filter)
end) end)
# TODO: put these behind functions to optimize them. # TODO: put these behind functions to optimize them.
attributes_contained? && relationships_contained? attributes_contained? or relationships_contained?
end end
defp contains_relationship?(filter, relationship, candidate_relationship_filter) do defp contains_relationship?(filter, relationship, candidate_relationship_filter) do
case filter.relationships do case filter.relationships do
%{^relationship => relationship_filter} -> %{^relationship => relationship_filter} ->
contains?(relationship_filter, candidate_relationship_filter) strict_subset_of?(relationship_filter, candidate_relationship_filter)
_ -> _ ->
false false
@ -105,8 +94,13 @@ defmodule Ash.Filter do
defp contains_attribute?(filter, attr, candidate_predicate) do defp contains_attribute?(filter, attr, candidate_predicate) do
case filter.attributes do case filter.attributes do
%{^attr => predicate} -> predicate_contains?(predicate, candidate_predicate) %{^attr => predicate} ->
_ -> false attribute = Ash.attribute(filter.resource, attr)
predicate_strict_subset_of?(attribute, predicate, candidate_predicate)
_ ->
false
end end
end end
@ -122,8 +116,8 @@ defmodule Ash.Filter do
end end
end end
def predicate_contains?(%left_struct{} = left, right) do def predicate_strict_subset_of?(attribute, %left_struct{} = left, right) do
left_struct.contains?(left, right) left_struct.strict_subset_of?(attribute, left, right)
end end
def add_to_filter(filter, additions) do def add_to_filter(filter, additions) do

View file

@ -1,6 +1,10 @@
defmodule Ash.Filter.In do defmodule Ash.Filter.In do
defstruct [:values] defstruct [:values]
def new(resource, attr_type, [value]) do
Ash.Filter.Eq.new(resource, attr_type, value)
end
def new(_resource, attr_type, values) do def new(_resource, attr_type, values) do
casted = casted =
values values
@ -28,13 +32,13 @@ defmodule Ash.Filter.In do
end end
end end
def contains?(%__MODULE__{values: values}, %__MODULE__{values: candidate_values}) do def strict_subset_of?(_attr, %__MODULE__{values: values}, %__MODULE__{values: candidate_values}) do
Enum.all?(candidate_values, fn candidate -> candidate in values end) Enum.all?(candidate_values, fn candidate -> candidate in values end)
end end
def contains?(%__MODULE__{values: values}, %Ash.Filter.Eq{value: candidate}) do def strict_subset_of?(_attr, %__MODULE__{values: values}, %Ash.Filter.Eq{value: candidate}) do
candidate in values candidate in values
end end
def contains?(_, _), do: false def strict_subset_of?(_attr, _, _), do: false
end end

View file

@ -109,7 +109,7 @@ defimpl Inspect, for: Ash.Filter do
and_clauses -> and_clauses ->
Inspect.Algebra.container_doc("(", and_clauses, ")", opts, fn term, _ -> term end, Inspect.Algebra.container_doc("(", and_clauses, ")", opts, fn term, _ -> term end,
break: :flex, break: :flex,
separator: " and " separator: " and"
) )
end end

View file

@ -11,13 +11,33 @@ defmodule Ash.Filter.NotEq do
end end
end end
def contains?(%__MODULE__{value: value}, %__MODULE__{value: candidate}) do def strict_subset_of?(
value != candidate %{type: :boolean, allow_nil?: false},
%__MODULE__{value: true},
%Ash.Filter.NotEq{value: false}
) do
true
end end
def contains?(%__MODULE__{value: value}, %Ash.Filter.In{values: candidate_values}) do def strict_subset_of?(
%{type: :boolean, allow_nil?: false},
%__MODULE__{value: false},
%Ash.Filter.NotEq{value: true}
) do
true
end
def strict_subset_of?(_attr, %__MODULE__{value: value}, %__MODULE__{value: candidate}) do
value == candidate
end
def strict_subset_of?(_attr, %__MODULE__{value: value}, %Ash.Filter.Eq{value: other_value}) do
value != other_value
end
def strict_subset_of?(_attr, %__MODULE__{value: value}, %Ash.Filter.In{values: candidate_values}) do
value not in candidate_values value not in candidate_values
end end
def contains?(_, _), do: false def strict_subset_of?(_, _, _), do: false
end end

View file

@ -1,6 +1,10 @@
defmodule Ash.Filter.NotIn do defmodule Ash.Filter.NotIn do
defstruct [:values] defstruct [:values]
def new(resource, attr_type, [value]) do
Ash.Filter.NotEq.new(resource, attr_type, value)
end
def new(_resource, attr_type, values) do def new(_resource, attr_type, values) do
casted = casted =
values values
@ -28,20 +32,22 @@ defmodule Ash.Filter.NotIn do
end end
end end
def contains?(%__MODULE__{values: values}, %__MODULE__{values: candidate_values}) do def strict_subset_of?(_attr, %__MODULE__{values: values}, %__MODULE__{values: candidate_values}) do
Enum.all?(candidate_values, fn candidate -> candidate not in values end) Enum.all?(candidate_values, fn candidate -> candidate not in values end)
end end
def contains?(%__MODULE__{values: values}, %Ash.Filter.Eq{value: candidate}) do def strict_subset_of?(_attr, %__MODULE__{values: values}, %Ash.Filter.Eq{value: candidate}) do
candidate not in values candidate not in values
end end
# TODO: Something like `foo != 1 and foo != 2` should be considered as contained in the # TODO: Something like `foo != 1 and foo != 2` should be considered as contained in the
# filter `not in [1, 2]`. This will only ever work if we simplify "and" filters wherever # filter `not in [1, 2]`. This will only ever work if we simplify "and" filters wherever
# possible. # possible.
def contains?(%__MODULE__{values: [single_value]}, %Ash.Filter.NotEq{value: candidate}) do def strict_subset_of?(_attr, %__MODULE__{values: [single_value]}, %Ash.Filter.NotEq{
value: candidate
}) do
single_value == candidate single_value == candidate
end end
def contains?(_, _), do: false def strict_subset_of?(_, _, _), do: false
end end

View file

@ -22,10 +22,10 @@ defmodule Ash.Filter.Or do
new(resource, attr_type, [left, right]) new(resource, attr_type, [left, right])
end end
def contains?(%{left: left, right: right}, predicate) do def strict_subset_of?(attr, %{left: left, right: right}, predicate) do
Ash.Filter.predicate_contains?(left, predicate) and Ash.Filter.predicate_strict_subset_of?(attr, left, predicate) and
Ash.Filter.predicate_contains?(right, predicate) Ash.Filter.predicate_strict_subset_of?(attr, right, predicate)
end end
def contains?(_, _), do: false def strict_subset_of?(_, _, _), do: false
end end

View file

@ -1,7 +1,7 @@
defmodule Ash.Resource.Attributes.Attribute do defmodule Ash.Resource.Attributes.Attribute do
@doc false @doc false
defstruct [:name, :type, :primary_key?, :default] defstruct [:name, :type, :allow_nil?, :primary_key?, :default]
@type t :: %__MODULE__{ @type t :: %__MODULE__{
name: atom(), name: atom(),
@ -13,6 +13,7 @@ defmodule Ash.Resource.Attributes.Attribute do
@schema Ashton.schema( @schema Ashton.schema(
opts: [ opts: [
primary_key?: :boolean, primary_key?: :boolean,
allow_nil?: :boolean,
default: [ default: [
{:function, 0}, {:function, 0},
{:tuple, {:module, :atom}}, {:tuple, {:module, :atom}},
@ -20,9 +21,14 @@ defmodule Ash.Resource.Attributes.Attribute do
] ]
], ],
defaults: [ defaults: [
primary_key?: false primary_key?: false,
allow_nil?: true
], ],
describe: [ describe: [
allow_nil?: """
Whether or not to allow `null` values. Ash can perform optimizations with this information, so if you do not
expect any null values, make sure to set this switch.
""",
primary_key?: primary_key?:
"Whether this field is, or is part of, the primary key of a resource.", "Whether this field is, or is part of, the primary key of a resource.",
default: default:
@ -41,6 +47,7 @@ defmodule Ash.Resource.Attributes.Attribute do
%__MODULE__{ %__MODULE__{
name: name, name: name,
type: type, type: type,
allow_nil?: opts[:allow_nil?],
primary_key?: opts[:primary_key?], primary_key?: opts[:primary_key?],
default: default default: default
}} }}

View file

@ -36,6 +36,15 @@ defmodule Ash.Resource.Attributes do
path: [:attributes, :attribute] path: [:attributes, :attribute]
end end
unless is_atom(type) do
raise Ash.Error.ResourceDslError,
message:
"Attribute type must be a built in type or a type module, got: #{inspect(type)}",
path: [:attributes, :attribute, name]
end
type = Ash.Type.get_type(type)
unless type in Ash.Type.builtins() or Ash.Type.ash_type?(type) do unless type in Ash.Type.builtins() or Ash.Type.ash_type?(type) do
raise Ash.Error.ResourceDslError, raise Ash.Error.ResourceDslError,
message: message:

View file

@ -20,8 +20,8 @@ defmodule Ash.Type do
@builtins [ @builtins [
string: [ecto_type: :string, filters: [:eq, :in, :not_eq, :not_in], sortable?: true], string: [ecto_type: :string, filters: [:eq, :in, :not_eq, :not_in], sortable?: true],
integer: [ecto_type: :integer, filters: [:eq, :in, :not_eq, :not_in], sortable?: true], integer: [ecto_type: :integer, filters: [:eq, :in, :not_eq, :not_in], sortable?: true],
int: [ecto_type: :integer, filters: [:eq, :in, :not_eq, :not_in], sortable?: true],
boolean: [ecto_type: :boolean, filters: [:eq, :not_eq], sortable?: true], boolean: [ecto_type: :boolean, filters: [:eq, :not_eq], sortable?: true],
int: [ecto_type: :integer, filters: [:eq, :in, :not_eq, :not_in], sortable?: true],
uuid: [ecto_type: :binary_id, filters: [:eq, :in, :not_eq, :not_in], sortable?: true], uuid: [ecto_type: :binary_id, filters: [:eq, :in, :not_eq, :not_in], sortable?: true],
utc_datetime: [ utc_datetime: [
ecto_type: :utc_datetime, ecto_type: :utc_datetime,
@ -30,10 +30,20 @@ defmodule Ash.Type do
] ]
] ]
@short_names []
@builtin_names Keyword.keys(@builtins) @builtin_names Keyword.keys(@builtins)
@type t :: module | atom @type t :: module | atom
@spec get_type(atom | module) :: atom | module
def get_type(value) do
case Keyword.fetch(@short_names, value) do
{:ok, mod} -> mod
:error -> value
end
end
@spec supports_filter?(t(), Ash.DataLayer.filter_type(), Ash.data_layer()) :: boolean @spec supports_filter?(t(), Ash.DataLayer.filter_type(), Ash.data_layer()) :: boolean
def supports_filter?(type, filter_type, data_layer) when type in @builtin_names do def supports_filter?(type, filter_type, data_layer) when type in @builtin_names do
data_layer.can?({:filter, filter_type}) and filter_type in @builtins[type][:filters] data_layer.can?({:filter, filter_type}) and filter_type in @builtins[type][:filters]
@ -85,7 +95,7 @@ defmodule Ash.Type do
def ash_type?(atom) when atom in @builtin_names, do: true def ash_type?(atom) when atom in @builtin_names, do: true
def ash_type?(module) when is_atom(module) do def ash_type?(module) when is_atom(module) do
:erlang.function_exported(module, :__info__, 1) and ash_type_module?(module) Code.ensure_compiled?(module) and ash_type_module?(module)
end end
def ash_type?(_), do: false def ash_type?(_), do: false

View file

@ -0,0 +1,200 @@
defmodule Ash.Test.Authorization.GetAuthorizationTest do
use ExUnit.Case, async: true
defmodule Author do
use Ash.Resource, name: "authors", type: "author"
use Ash.DataLayer.Ets, private?: true
actions do
read :default,
authorization_steps: [
# You can see yourself
authorize_if: user_attribute_matches_record(:id, :id),
# You can't see anything else unless you're a manager
forbid_unless: user_attribute(:manager, true),
# No one can see a fired author
forbid_if: attribute_equals(:fired, true),
# Managers can't see `self_manager` authors
authorize_unless: attribute_equals(:self_manager, true)
]
create :default
end
attributes do
attribute :name, :string
attribute :self_manager, :boolean
attribute :fired, :boolean
end
relationships do
many_to_many :posts, Ash.Test.Authorization.AuthorizationTest.Post,
through: Ash.Test.Authorization.AuthorizationTest.AuthorPost
end
end
defmodule User do
use Ash.Resource, name: "users", type: "user"
use Ash.DataLayer.Ets, private?: true
actions do
read :default
create :default
end
attributes do
attribute :name, :string
attribute :manager, :boolean
end
end
defmodule AuthorPost do
use Ash.Resource, name: "author_posts", type: "author_post", primary_key: false
use Ash.DataLayer.Ets, private?: true
actions do
read :default
create :default
end
attributes do
attribute :name, :string
end
relationships do
belongs_to :post, Ash.Test.Authorization.AuthorizationTest.Post, primary_key?: true
belongs_to :author, Author, primary_key?: true
end
end
defmodule Post do
use Ash.Resource, name: "posts", type: "post"
use Ash.DataLayer.Ets, private?: true
actions do
read :default,
authorization_steps: [
authorize_if: attribute_equals(:published, true),
authorize_if: related_to_user_via(:authors)
]
create :default
end
attributes do
attribute :title, :string
attribute :contents, :string
attribute :published, :boolean
end
relationships do
has_many :author_posts, AuthorPost
many_to_many :authors, Author, through: :author_posts
end
end
defmodule Api do
use Ash.Api
resources [Post, Author, AuthorPost, User]
end
test "it succeeds if you match a strict_check" do
author = Api.create!(Author, attributes: %{name: "foo"})
user = Api.create!(User, attributes: %{id: author.id})
Api.get!(Author, author.id, authorization: [user: user])
end
test "it succeeds if you match the data checks" do
author = Api.create!(Author, attributes: %{name: "foo", fired: false, self_manager: false})
user = Api.create!(User, attributes: %{manager: true})
Api.get!(Author, author.id, authorization: [user: user])
end
# test "it succeeds if you match the first rule" do
# end
# test "it succeeds if you match the second rule" do
# user = Api.create!(User)
# Api.read!(Post,
# authorization: [user: user],
# filter: [published: true]
# )
# end
# test "it succeeds if you match both rules" do
# author = Api.create!(Author, attributes: %{name: "foo"})
# user = Api.create!(User, attributes: %{id: author.id})
# Api.read!(Post,
# authorization: [user: user],
# filter: [published: true, authors: [id: author.id]]
# )
# end
# test "it fails if you don't match either" do
# user = Api.create!(User)
# assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
# Api.read!(Post,
# authorization: [user: user],
# filter: [published: false]
# )
# end
# end
# test "it fails if it can't confirm that you match either" do
# user = Api.create!(User)
# assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
# Api.read!(Post,
# authorization: [user: user]
# )
# end
# end
# test "authorize_if falls through properly" do
# user = Api.create!(User, attributes: %{manager: true})
# Api.read!(Author,
# filter: [fired: [not_eq: true], self_manager: false],
# authorization: [user: user]
# )
# end
# test "authorize_unless doesn't trigger if its check is not true" do
# user = Api.create!(User, attributes: %{manager: true})
# assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
# Api.read!(Author,
# filter: [fired: false, self_manager: true],
# authorization: [user: user]
# )
# end
# end
# test "forbid_if triggers if its check is true" do
# user = Api.create!(User, attributes: %{manager: true})
# assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
# Api.read!(Author,
# filter: [fired: true, self_manager: false],
# authorization: [user: user]
# )
# end
# end
# test "forbid_unless doesn't trigger if its check is true" do
# user = Api.create!(User, attributes: %{manager: false})
# assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
# Api.read!(Author,
# filter: [fired: false, self_manager: false],
# authorization: [user: user]
# )
# end
# end
end

View file

@ -99,100 +99,95 @@ defmodule Ash.Test.Authorization.ReadAuthorizationTest do
resources [Post, Author, AuthorPost, User] resources [Post, Author, AuthorPost, User]
end end
describe "read authorization" do test "it succeeds if you match the first rule" do
test "it succeeds if you match the first rule" do author = Api.create!(Author, attributes: %{name: "foo"})
author = Api.create!(Author, attributes: %{name: "foo"}) user = Api.create!(User, attributes: %{id: author.id})
user = Api.create!(User, attributes: %{id: author.id})
Api.read!(Post,
authorization: [user: user],
filter: [authors: [id: author.id]]
)
end
test "it succeeds if you match the second rule" do
user = Api.create!(User)
Api.read!(Post,
authorization: [user: user],
filter: [published: true]
)
end
test "it succeeds if you match both rules" do
author = Api.create!(Author, attributes: %{name: "foo"})
user = Api.create!(User, attributes: %{id: author.id})
Api.read!(Post,
authorization: [user: user],
filter: [published: true, authors: [id: author.id]]
)
end
test "it fails if you don't match either" do
user = Api.create!(User)
assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
Api.read!(Post, Api.read!(Post,
authorization: [user: user], authorization: [user: user],
filter: [authors: [id: author.id]] filter: [published: false]
) )
end end
end
test "it succeeds if you match the second rule" do test "it fails if it can't confirm that you match either" do
user = Api.create!(User) user = Api.create!(User)
assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
Api.read!(Post, Api.read!(Post,
authorization: [user: user], authorization: [user: user]
filter: [published: true]
) )
end end
end
test "it succeeds if you match both rules" do test "authorize_if falls through properly" do
author = Api.create!(Author, attributes: %{name: "foo"}) user = Api.create!(User, attributes: %{manager: true})
user = Api.create!(User, attributes: %{id: author.id})
Api.read!(Post, Api.read!(Author,
authorization: [user: user], filter: [fired: [not_eq: true], self_manager: [not_eq: true]],
filter: [published: true, authors: [id: author.id]] authorization: [user: user]
)
end
test "authorize_unless doesn't trigger if its check is not true" do
user = Api.create!(User, attributes: %{manager: true})
assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
Api.read!(Author,
filter: [fired: false, self_manager: true],
authorization: [user: user]
) )
end end
end
test "it fails if you don't match either" do test "forbid_if triggers if its check is true" do
user = Api.create!(User) user = Api.create!(User, attributes: %{manager: true})
assert_raise Ash.Error.FrameworkError, ~r/forbidden/, fn -> assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
Api.read!(Post, Api.read!(Author,
authorization: [user: user], filter: [fired: true, self_manager: false],
filter: [published: false] authorization: [user: user]
) )
end
end end
end
test "it fails if it can't confirm that you match either" do test "forbid_unless doesn't trigger if its check is true" do
user = Api.create!(User) user = Api.create!(User, attributes: %{manager: false})
assert_raise Ash.Error.FrameworkError, ~r/forbidden/, fn ->
Api.read!(Post,
authorization: [user: user]
)
end
end
# forbid_unless: user_attribute(:manager, true),
# forbid_if: attribute(:fired, true),
test "authorize_if falls through properly" do
user = Api.create!(User, attributes: %{manager: true})
assert_raise Ash.Error.Forbidden, ~r/forbidden/, fn ->
Api.read!(Author, Api.read!(Author,
filter: [fired: false, self_manager: false], filter: [fired: false, self_manager: false],
authorization: [user: user] authorization: [user: user]
) )
end end
test "authorize_unless doesn't trigger if its check is not true" do
user = Api.create!(User, attributes: %{manager: true})
assert_raise Ash.Error.FrameworkError, ~r/forbidden/, fn ->
Api.read!(Author,
filter: [fired: false, self_manager: true],
authorization: [user: user]
)
end
end
test "forbid_if triggers if its check is true" do
user = Api.create!(User, attributes: %{manager: true})
assert_raise Ash.Error.FrameworkError, ~r/forbidden/, fn ->
Api.read!(Author,
filter: [fired: true, self_manager: false],
authorization: [user: user]
)
end
end
test "forbid_unless doesn't trigger if its check is true" do
user = Api.create!(User, attributes: %{manager: false})
assert_raise Ash.Error.FrameworkError, ~r/forbidden/, fn ->
Api.read!(Author,
filter: [fired: false, self_manager: false],
authorization: [user: user]
)
end
end
end end
end end