defmodule AshAuthentication.Transformer do
@moduledoc """
The Authentication transformer
Sets up non-provider-specific configuration for authenticated resources.
"""
use Spark.Dsl.Transformer
alias Ash.Resource
alias AshAuthentication.{Info, Strategy}
alias Spark.{Dsl.Transformer, Error.DslError}
import AshAuthentication.Utils
import AshAuthentication.Validations
import AshAuthentication.Validations.Action
@doc false
@impl true
@spec after?(any) :: boolean()
def after?(Resource.Transformers.ValidatePrimaryActions), do: true
def after?(_), do: false
@doc false
@impl true
@spec before?(any) :: boolean
def before?(Resource.Transformers.DefaultAccept), do: true
def before?(_), do: false
@doc false
@impl true
@spec transform(map) ::
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
def transform(dsl_state) do
with :ok <- validate_at_least_one_strategy(dsl_state),
:ok <- validate_unique_strategy_names(dsl_state),
:ok <- validate_unique_add_on_names(dsl_state),
{:ok, get_by_subject_action_name} <-
Info.authentication_get_by_subject_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
get_by_subject_action_name,
&build_get_by_subject_action/1
),
:ok <- validate_read_action(dsl_state, get_by_subject_action_name),
subject_name <- find_or_generate_subject_name(dsl_state),
current_user when is_atom(current_user) <- ensure_current_user_atom_exists(subject_name) do
dsl_state =
dsl_state
|> Transformer.set_option([:authentication], :subject_name, subject_name)
{:ok, dsl_state}
end
end
defp build_get_by_subject_action(dsl_state) do
with {:ok, get_by_subject_action_name} <-
Info.authentication_get_by_subject_action_name(dsl_state) do
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: get_by_subject_action_name,
get?: true
)
end
end
# sobelow_skip ["DOS.StringToAtom"]
defp find_or_generate_subject_name(dsl_state) do
with nil <- Transformer.get_option(dsl_state, [:authentication], :subject_name),
nil <- Transformer.get_option(dsl_state, [:resource], :short_name) do
# We have to do this because the resource has not yet been compiled, so we can't call `default_short_name/0`.
dsl_state
|> Transformer.get_persisted(:module)
|> Module.split()
|> List.last()
|> Macro.underscore()
|> String.to_atom()
end
end
# sobelow_skip ["DOS.StringToAtom"]
defp ensure_current_user_atom_exists(subject_name),
do: String.to_atom("current_#{subject_name}")
defp validate_at_least_one_strategy(dsl_state) do
ok? =
dsl_state
|> Transformer.get_entities([:authentication, :strategies])
|> Enum.any?()
if ok?,
do: :ok,
else:
{:error,
DslError.exception(
path: [:authentication, :strategies],
message: "Expected at least one authentication strategy"
)}
end
defp validate_read_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <- validate_field_in_values(action, :type, [:read]) do
:ok
else
_ ->
{:error,
DslError.exception(
path: [:actions],
message: "Expected resource to have either read action named `#{action_name}`"
)}
end
end
defp validate_unique_add_on_names(dsl_state) do
dsl_state
|> Transformer.get_entities([:authentication, :add_ons])
|> Enum.map(&Strategy.name/1)
|> validate_unique("add on")
end
defp validate_unique_strategy_names(dsl_state) do
dsl_state
|> Transformer.get_entities([:authentication, :strategies])
|> Enum.map(&Strategy.name/1)
|> validate_unique("strategy")
end
defp validate_unique(strategy_names, descriptor) do
duplicates =
strategy_names
|> Enum.frequencies()
|> Enum.reject(&(elem(&1, 1) == 1))
if Enum.any?(duplicates) do
errors =
duplicates
|> Enum.map_join("\n", fn
{name, 2} -> " * #{descriptor} `#{inspect(name)}` is repeated twice."
{name, n} -> " * #{descriptor} `#{inspect(name)}` is repeated #{n} times."
end)
{:error,
DslError.exception(
path: [:authentication, :strategies],
message: "Strategy names must be unique.\n\n#{errors}"
)}
else
:ok
end
end
end