Current section
Files
Jump to
Current section
Files
lib/let_me/builder.ex
defmodule LetMe.Builder do
@moduledoc false
def get_acc_attribute(module, name) do
module
|> Module.get_attribute(name, [])
|> Enum.reverse()
end
def introspection_functions(%{} = rules) do
quote do
defp __rules__, do: unquote(Macro.escape(rules))
@impl LetMe.Policy
def list_rules, do: Map.values(__rules__())
@impl LetMe.Policy
def list_rules(opts), do: LetMe.filter_rules(list_rules(), opts)
@impl LetMe.Policy
def filter_allowed_actions(rules, subject, object) when is_list(rules) do
LetMe.filter_allowed_actions(rules, subject, object, __MODULE__)
end
@impl LetMe.Policy
def fetch_rule(action) when is_atom(action),
do: Map.fetch(__rules__(), action)
@impl LetMe.Policy
def fetch_rule!(action) when is_atom(action),
do: Map.fetch!(__rules__(), action)
@impl LetMe.Policy
def get_rule(action) when is_atom(action),
do: Map.get(__rules__(), action)
end
end
def schema_functions(schemas) when is_list(schemas) do
schema_function_clauses =
for {object_name, schema} <- schemas do
quote do
def get_schema(unquote(object_name)), do: unquote(schema)
end
end
object_function_clauses =
for {object_name, schema} <- schemas do
quote do
def get_object_name(unquote(schema)), do: unquote(object_name)
end
end
quote do
@impl LetMe.Policy
unquote(schema_function_clauses)
def get_schema(object_name) when is_atom(object_name), do: nil
@impl LetMe.Policy
unquote(object_function_clauses)
def get_object_name(%schema{}), do: get_object_name(schema)
def get_object_name(schema), do: nil
end
end
def authorize_functions(%{} = rules, opts) do
check_module = Keyword.fetch!(opts, :check_module)
error_reason = Keyword.fetch!(opts, :error_reason)
error_message = Keyword.fetch!(opts, :error_message)
rule_clauses = Enum.map(rules, &permit_function_clause(&1, check_module))
typespec =
rules
|> Map.keys()
|> Enum.reduce(&{:|, [], [&1, &2]})
quote do
@typedoc """
Valid actions to be passed to the various `authorize` functions. These are auto-generated
from the defined policy rules.
"""
@type action() :: unquote(typespec)
@impl LetMe.Policy
@spec authorize?(action(), any, any, keyword) :: boolean()
def authorize?(action, subject, object \\ nil, opts \\ [])
unquote(rule_clauses)
def authorize?(action, _, _, _) when is_atom(action) do
Logger.warning(
"Permission checked for rule that does not exist: #{action}",
action: action,
policy_module: unquote(check_module)
)
false
end
@impl LetMe.Policy
@spec authorize(action(), any, any, keyword) ::
:ok | {:error, unquote(error_reason)}
def authorize(action, subject, object \\ nil, opts \\ []) do
if authorize?(action, subject, object, opts),
do: :ok,
else: {:error, unquote(error_reason)}
end
@impl LetMe.Policy
@spec authorize!(action(), any, any, keyword) :: :ok
def authorize!(action, subject, object \\ nil, opts \\ []) do
if authorize?(action, subject, object, opts),
do: :ok,
else: raise(LetMe.UnauthorizedError, message: unquote(error_message))
end
end
end
defp permit_function_clause(
{rule_name, %LetMe.Rule{} = rule},
check_module
) do
pre_hook_calls = build_pre_hook_calls(rule.pre_hooks, check_module)
allow_condition = build_conditions(rule.allow, check_module)
deny_condition = build_conditions(rule.deny, check_module)
# check for conditions that are always true or false to prevent
# "this check/guard will always yield the same result" warning
combined_condition =
case {allow_condition, deny_condition} do
{false, _} -> false
{_, true} -> false
{_, false} -> allow_condition
{true, _} -> quote(do: !unquote(deny_condition))
_ -> quote(do: !unquote(deny_condition) && unquote(allow_condition))
end
quote do
def authorize?(unquote(rule_name), subject, object, opts) do
unquote(pre_hook_calls)
unquote(combined_condition)
end
end
end
defp build_pre_hook_calls([], _), do: nil
defp build_pre_hook_calls(pre_hooks, check_module)
when is_list(pre_hooks) do
functions =
Enum.map(pre_hooks, fn
pre_hook when is_atom(pre_hook) ->
{check_module, pre_hook, []}
{module, function} ->
{module, function, []}
{module, function, args} when is_list(args) ->
{module, function, [args]}
{_, _, args} ->
raise ArgumentError, """
Invalid pre-hook options
Expected pre-hook options to be a keyword list, got:
#{inspect(args)}
"""
end)
quote do
{subject, object} =
Enum.reduce(
unquote(Macro.escape(functions)),
{subject, object},
fn {module, function, args}, {subject, object} ->
args =
args
|> List.flatten()
|> Keyword.merge(opts)
|> case do
[] -> []
args -> [args]
end
apply(module, function, [subject, object] ++ args)
end
)
end
end
defp build_conditions([], _), do: false
defp build_conditions([checks], check_module) do
build_check(checks, check_module)
end
defp build_conditions(conditions, check_module) when is_list(conditions) do
quote do
Enum.any?(unquote(Enum.map(conditions, &build_check(&1, check_module))))
end
end
defp build_check([], _), do: false
defp build_check([check], check_module) do
build_check(check, check_module)
end
defp build_check(checks, check_module) when is_list(checks) do
quote do
Enum.all?(unquote(Enum.map(checks, &build_check(&1, check_module))))
end
end
defp build_check(true, _) do
quote do
true
end
end
defp build_check(false, _) do
quote do
false
end
end
defp build_check(function, check_module) when is_atom(function) do
quote do
apply(unquote(check_module), unquote(function), [subject, object])
end
end
defp build_check({function, opts}, check_module)
when is_atom(function) do
quote do
apply(unquote(check_module), unquote(function), [
subject,
object,
unquote(opts)
])
end
end
end