defmodule AshAuthentication.TokenResource.Transformer do
@moduledoc """
The token resource transformer.
Sets up the default schema and actions for the token resource.
"""
use Spark.Dsl.Transformer
require Ash.Expr
alias Ash.{Resource, Type}
alias AshAuthentication.{TokenResource, TokenResource.Info}
alias Spark.Dsl.Transformer
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?(_), 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, dsl_state} <-
maybe_build_attribute(dsl_state, :jti, :string,
primary_key?: true,
allow_nil?: false,
sensitive?: true,
writable?: true
),
:ok <- validate_jti_field(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :subject, :string, allow_nil?: false, writable?: true),
:ok <- validate_subject_field(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :expires_at, :utc_datetime,
allow_nil?: false,
writable?: true
),
:ok <- validate_expires_at_field(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :purpose, :string,
allow_nil?: false,
writable?: true
),
:ok <- validate_purpose_field(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :extra_data, :map,
allow_nil?: true,
writable?: true
),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :created_at, :utc_datetime_usec,
allow_nil?: false,
private?: true,
default: &DateTime.utc_now/0
),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :updated_at, :utc_datetime_usec,
allow_nil?: false,
private?: true,
default: &DateTime.utc_now/0,
update_default: &DateTime.utc_now/0
),
:ok <- validate_extra_data_field(dsl_state),
{:ok, expunge_expired_action_name} <- Info.token_expunge_expired_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
expunge_expired_action_name,
&build_expunge_expired_action(&1, expunge_expired_action_name)
),
:ok <- validate_expunge_expired_action(dsl_state, expunge_expired_action_name),
{:ok, read_expired_action_name} <- Info.token_read_expired_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
read_expired_action_name,
&build_read_expired_action(&1, read_expired_action_name)
),
:ok <- validate_read_expired_action(dsl_state, read_expired_action_name),
{:ok, revoke_token_action_name} <-
Info.token_revocation_revoke_token_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
revoke_token_action_name,
&build_revoke_token_action(&1, revoke_token_action_name)
),
:ok <- validate_revoke_token_action(dsl_state, revoke_token_action_name),
{:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
is_revoked_action_name,
&build_is_revoked_action(&1, is_revoked_action_name)
),
:ok <- validate_is_revoked_action(dsl_state, is_revoked_action_name),
{:ok, get_confirmation_changes_action_name} <-
Info.token_confirmation_get_changes_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
get_confirmation_changes_action_name,
&build_get_confirmation_changes_action(&1, get_confirmation_changes_action_name)
),
:ok <-
validate_get_confirmation_changes_action(
dsl_state,
get_confirmation_changes_action_name
),
{:ok, store_confirmation_changes_action_name} <-
Info.token_confirmation_store_changes_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
store_confirmation_changes_action_name,
&build_store_confirmation_changes_action(&1, store_confirmation_changes_action_name)
),
:ok <-
validate_store_confirmation_changes_action(
dsl_state,
store_confirmation_changes_action_name
),
{:ok, store_token_action_name} <-
Info.token_store_token_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
store_token_action_name,
&build_store_token_action(&1, store_token_action_name)
),
:ok <- validate_store_token_action(dsl_state, store_token_action_name),
{:ok, get_token_action_name} <- Info.token_get_token_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
get_token_action_name,
&build_get_token_action(&1, get_token_action_name)
),
:ok <- validate_get_token_action(dsl_state, get_token_action_name) do
{:ok, dsl_state}
end
end
defp validate_subject_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :subject),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]) do
validate_attribute_option(attribute, resource, :writable?, [true])
end
end
defp validate_get_token_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <-
validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]),
:ok <- validate_action_argument_option(action, :token, :allow_nil?, [true]),
:ok <- validate_action_argument_option(action, :token, :sensitive?, [true]),
:ok <- validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]),
:ok <- validate_action_argument_option(action, :jti, :allow_nil?, [true]),
:ok <-
validate_action_argument_option(action, :purpose, :type, [Ash.Type.String, :string]) do
validate_action_has_preparation(action, TokenResource.GetTokenPreparation)
end
end
defp build_get_token_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :token,
type: :string,
sensitive?: true
),
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :jti,
type: :string,
sensitive?: false
),
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :purpose,
type: :string,
sensitive?: false
)
]
preparations = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: TokenResource.GetTokenPreparation
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: action_name,
arguments: arguments,
preparations: preparations,
get?: true
)
end
defp validate_store_token_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <- validate_token_argument(action) do
validate_action_has_change(action, TokenResource.StoreTokenChange)
end
end
defp build_store_token_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: :token,
type: :string,
allow_nil?: false,
sensitive?: true
)
]
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: TokenResource.StoreTokenChange
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: action_name,
arguments: arguments,
changes: changes,
accept: [:extra_data, :purpose]
)
end
defp build_store_confirmation_changes_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: :token,
type: :string,
allow_nil?: false,
sensitive?: true
)
]
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: TokenResource.StoreConfirmationChangesChange
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: action_name,
arguments: arguments,
changes: changes,
accept: [:extra_data, :purpose]
)
end
defp validate_store_confirmation_changes_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <- validate_token_argument(action) do
validate_action_has_change(action, TokenResource.StoreConfirmationChangesChange)
end
end
defp build_get_confirmation_changes_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :jti,
type: :string,
allow_nil?: false,
sensitive?: true
)
]
preparations = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: TokenResource.GetConfirmationChangesPreparation
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: action_name,
arguments: arguments,
preparations: preparations,
get?: true
)
end
defp validate_get_confirmation_changes_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <- validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]),
:ok <-
validate_action_has_preparation(
action,
TokenResource.GetConfirmationChangesPreparation
) do
validate_field_in_values(action, :type, [:read])
end
end
defp build_read_expired_action(_dsl_state, action_name) do
import Ash.Filter.TemplateHelpers
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: action_name,
filter: expr(expires_at < now())
)
end
defp validate_read_expired_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name) do
validate_field_in_values(action, :type, [:read])
end
end
defp validate_is_revoked_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <-
validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]),
:ok <- validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]),
:ok <- validate_action_has_preparation(action, TokenResource.IsRevokedPreparation) do
validate_field_in_values(action, :type, [:read])
end
end
defp build_is_revoked_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :token,
type: :string,
allow_nil?: true,
sensitive?: true
),
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :jti,
type: :string,
allow_nil?: true,
sensitive?: true
)
]
preparations = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: TokenResource.IsRevokedPreparation
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: action_name,
get?: true,
preparations: preparations,
arguments: arguments
)
end
defp validate_token_argument(action) do
with :ok <-
validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]),
:ok <- validate_action_argument_option(action, :token, :allow_nil?, [false]) do
validate_action_argument_option(action, :token, :sensitive?, [true])
end
end
defp validate_revoke_token_action(dsl_state, revoke_token_action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, revoke_token_action_name),
:ok <- validate_token_argument(action) do
validate_action_has_change(action, TokenResource.RevokeTokenChange)
end
end
defp build_revoke_token_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: :token,
type: :string,
allow_nil?: false,
sensitive?: true
)
]
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: TokenResource.RevokeTokenChange
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: action_name,
arguments: arguments,
changes: changes,
accept: [:extra_data]
)
end
defp validate_expunge_expired_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name) do
validate_field_in_values(action, :type, [:destroy])
end
end
defp build_expunge_expired_action(_dsl_state, action_name),
do: Transformer.build_entity(Resource.Dsl, [:actions], :destroy, name: action_name)
defp validate_jti_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :jti),
: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, :sensitive?, [true]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
:ok <- validate_attribute_option(attribute, resource, :primary_key?, [true]) do
validate_attribute_option(attribute, resource, :private?, [false])
end
end
defp validate_expires_at_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :expires_at),
:ok <-
validate_attribute_option(attribute, resource, :type, [Type.UtcDatetime, :utc_datetime]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do
validate_attribute_option(attribute, resource, :writable?, [true])
end
end
defp validate_purpose_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :purpose),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do
validate_attribute_option(attribute, resource, :writable?, [true])
end
end
defp validate_extra_data_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :extra_data),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.Map, :map]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [true]) do
validate_attribute_option(attribute, resource, :writable?, [true])
end
end
end