Current section

Files

Jump to
ash_authentication lib ash_authentication user_identity transformer.ex
Raw

lib/ash_authentication/user_identity/transformer.ex

defmodule AshAuthentication.UserIdentity.Transformer do
@moduledoc """
The user identity transformer.
Sets up the default schema and actions for a user identity resource.
"""
use Spark.Dsl.Transformer
alias Ash.{Resource, Type}
alias AshAuthentication.UserIdentity
alias Spark.{Dsl.Transformer, Error.DslError}
import AshAuthentication.Utils
import AshAuthentication.Validations
import AshAuthentication.Validations.Action
import AshAuthentication.Validations.Attribute
@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.CachePrimaryKey), do: true
def before?(Resource.Transformers.DefaultAccept), do: true
def before?(Resource.Transformers.ValidateRelationshipAttributes), do: true
def before?(Resource.Transformers.BelongsToAttribute), 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, resource} <- persisted_option(dsl_state, :module),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :id, Type.UUID,
allow_nil?: false,
writable?: true,
primary_key?: true,
default: &Ash.UUID.generate/0
),
:ok <- validate_id_field(dsl_state, :id),
{:ok, uid} <- UserIdentity.Info.user_identity_uid_attribute_name(dsl_state),
{:ok, strategy} <-
UserIdentity.Info.user_identity_strategy_attribute_name(dsl_state),
{:ok, user_id} <-
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state),
{:ok, access_token} <-
UserIdentity.Info.user_identity_access_token_attribute_name(dsl_state),
{:ok, access_token_expires_at} <-
UserIdentity.Info.user_identity_access_token_expires_at_attribute_name(dsl_state),
{:ok, refresh_token} <-
UserIdentity.Info.user_identity_refresh_token_attribute_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, strategy, Type.String,
allow_nil?: false,
writable?: true
),
:ok <- validate_strategy_field(dsl_state, strategy),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, uid, Type.String, allow_nil?: false, writable?: true),
:ok <- validate_uid_field(dsl_state, uid),
{:ok, dsl_state} <- maybe_build_identity(dsl_state, [user_id, uid, strategy]),
:ok <-
validate_attribute_unique_constraint(dsl_state, [user_id, uid, strategy], resource),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, access_token, Type.String,
allow_nil?: true,
writable?: true
),
:ok <- validate_token_field(dsl_state, access_token),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, access_token_expires_at, Type.UtcDatetimeUsec,
allow_nil?: true,
writable?: true
),
:ok <- validate_expiry_field(dsl_state, access_token_expires_at),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, refresh_token, Type.String,
allow_nil?: true,
writable?: true
),
:ok <- validate_token_field(dsl_state, refresh_token),
{:ok, user_resource} <- UserIdentity.Info.user_identity_user_resource(dsl_state),
{:ok, user_relationship} <-
UserIdentity.Info.user_identity_user_relationship_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_relationship(
dsl_state,
user_relationship,
&build_user_relationship(&1, user_relationship, user_resource)
),
:ok <- validate_user_relationship(dsl_state, user_relationship, user_resource),
{:ok, upsert_action} <-
UserIdentity.Info.user_identity_upsert_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(dsl_state, upsert_action, &build_upsert_action(&1, upsert_action)),
:ok <- validate_upsert_action(dsl_state, upsert_action),
{:ok, destroy_action} <-
UserIdentity.Info.user_identity_destroy_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
destroy_action,
&build_destroy_action(&1, destroy_action)
),
:ok <-
validate_destroy_action(dsl_state, destroy_action),
{:ok, read_action} <-
UserIdentity.Info.user_identity_read_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(dsl_state, read_action, &build_read_action(&1, read_action)),
:ok <- validate_read_action(dsl_state, read_action) do
{:ok, dsl_state}
end
end
defp validate_id_field(dsl_state, field_name) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, field_name),
:ok <- validate_attribute_option(attribute, resource, :primary_key?, [true]) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
defp validate_strategy_field(dsl_state, field_name) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, field_name),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
defp validate_uid_field(dsl_state, field_name) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, field_name),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
defp validate_token_field(dsl_state, field_name) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, field_name),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [true]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
defp validate_expiry_field(dsl_state, field_name) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, field_name),
:ok <-
validate_attribute_option(attribute, resource, :type, [
Type.UtcDatetimeUsec,
:utc_datetime_usec
]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [true]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
defp build_user_relationship(dsl_state, name, destination) do
with {:ok, id_attr} <- find_pk(destination),
{:ok, api} <- AshAuthentication.Info.authentication_api(destination),
{:ok, user_id} <-
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state) do
Transformer.build_entity(Resource.Dsl, [:relationships], :belongs_to,
name: name,
destination: destination,
define_attribute?: true,
destination_attribute: id_attr.name,
attribute_type: id_attr.type,
source_attribute: user_id,
api: api,
attribute_writable?: true,
writable?: true
)
end
end
defp validate_user_relationship(dsl_state, name, destination) do
with {:ok, id_attr} <- find_pk(destination),
{:ok, api} <- AshAuthentication.Info.authentication_api(destination),
{:ok, relationship} <- find_relationship(dsl_state, name),
{:ok, user_id} <-
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state),
:ok <- validate_field_in_values(relationship, :destination, [destination]),
:ok <- validate_field_in_values(relationship, :destination_attribute, [id_attr.name]),
:ok <- validate_field_in_values(relationship, :source_attribute, [user_id]),
:ok <- validate_field_in_values(relationship, :api, [api]) do
validate_field_in_values(relationship, :attribute_type, [id_attr.type])
end
end
defp build_upsert_action(dsl_state, action_name) do
with {:ok, user_id} <-
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state),
{:ok, uid} <- UserIdentity.Info.user_identity_uid_attribute_name(dsl_state),
{:ok, strategy} <-
UserIdentity.Info.user_identity_strategy_attribute_name(dsl_state),
{:ok, identity} <- find_identity(dsl_state, [user_id, uid, strategy]),
{:ok, user_resource} <- UserIdentity.Info.user_identity_user_resource(dsl_state),
{:ok, user_resource_id} <- find_pk(user_resource) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: :user_info,
type: Type.Map,
allow_nil?: false
),
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: :oauth_tokens,
type: Type.Map,
allow_nil?: false
),
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: user_id,
type: user_resource_id.type,
allow_nil?: false
)
]
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: UserIdentity.UpsertIdentityChange
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: action_name,
upsert?: true,
upsert_identity: identity.name,
arguments: arguments,
changes: changes,
accept: [strategy]
)
end
end
defp validate_upsert_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <- validate_action_argument_option(action, :user_info, :type, [:map, Type.Map]),
:ok <- validate_action_argument_option(action, :user_info, :allow_nil?, [false]),
:ok <- validate_action_argument_option(action, :oauth_tokens, :type, [:map, Type.Map]),
:ok <- validate_action_argument_option(action, :oauth_tokens, :allow_nil?, [false]),
:ok <- validate_action_has_change(action, UserIdentity.UpsertIdentityChange),
:ok <- validate_field_in_values(action, :type, [:create]),
:ok <- validate_field_in_values(action, :upsert?, [true]),
{:ok, user_id} <-
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state),
{:ok, user_resource} <- UserIdentity.Info.user_identity_user_resource(dsl_state),
{:ok, user_resource_id} <- find_pk(user_resource),
:ok <- validate_action_argument_option(action, user_id, :type, [user_resource_id.type]),
:ok <- validate_action_argument_option(action, user_id, :allow_nil?, [false]),
{:ok, uid} <- UserIdentity.Info.user_identity_uid_attribute_name(dsl_state),
{:ok, strategy} <-
UserIdentity.Info.user_identity_strategy_attribute_name(dsl_state),
{:ok, identity} <- find_identity(dsl_state, [uid, user_id, strategy]),
:ok <- validate_field_in_values(action, :upsert_identity, [identity.name]) do
:ok
else
{:error, reason} when is_binary(reason) ->
{:error, DslError.exception(path: [:user_identity], message: reason)}
{:error, reason} ->
{:error, reason}
:error ->
{:error,
DslError.exception(
path: [:user_identity],
message: "Configuration error while validating upsert action."
)}
end
end
defp build_destroy_action(_dsl_state, action_name) do
Transformer.build_entity(Resource.Dsl, [:actions], :destroy, name: action_name, primary?: true)
end
defp validate_destroy_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <- validate_field_in_values(action, :type, [:destroy]),
:ok <- validate_field_in_values(action, :primary?, [true]) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
defp build_read_action(_dsl_state, action_name) do
Transformer.build_entity(Resource.Dsl, [:actions], :read, name: action_name, primary?: true)
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]),
:ok <- validate_field_in_values(action, :primary?, [true]) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
defp find_identity(dsl_state, keys) do
keyset = MapSet.new(keys)
dsl_state
|> Resource.Info.identities()
|> Enum.find_value(:error, fn identity ->
if MapSet.equal?(MapSet.new(identity.keys), keyset), do: {:ok, identity}
end)
end
defp maybe_build_identity(dsl_state, keys) do
dsl_state
|> find_identity(keys)
|> case do
{:ok, _identity} ->
{:ok, dsl_state}
:error ->
keys = Enum.sort(keys)
name =
keys
|> Enum.join("_and_")
|> then(&"unique_on_#{&1}")
|> String.to_atom()
identity =
Transformer.build_entity!(Resource.Dsl, [:identities], :identity,
name: name,
keys: keys
)
{:ok, Transformer.add_entity(dsl_state, [:identities], identity)}
end
end
defp find_pk(resource) do
with [id_field] <- Resource.Info.primary_key(resource),
id_attr when is_map(id_attr) <- Resource.Info.attribute(resource, id_field) do
{:ok, id_attr}
else
nopk when nopk == [] or is_nil(nopk) ->
{:error, "`#{inspect(resource)}` must have a primary key."}
[_ | _] ->
{:error, "Resources with composite primary keys are not supported."}
_other ->
{:error, "Unable to retrieve primary key for resource `#{inspect(resource)}`."}
end
end
end