Packages
ash_authentication
3.7.4
5.0.0-rc.12
5.0.0-rc.11
5.0.0-rc.10
5.0.0-rc.9
5.0.0-rc.8
5.0.0-rc.7
5.0.0-rc.6
5.0.0-rc.5
5.0.0-rc.4
5.0.0-rc.3
5.0.0-rc.2
5.0.0-rc.1
5.0.0-rc.0
4.14.1
4.14.0
4.13.7
4.13.6
4.13.5
4.13.4
4.13.3
4.13.2
retired
4.13.1
retired
4.13.0
4.12.0
4.11.0
4.10.0
4.9.9
4.9.8
4.9.7
4.9.6
4.9.5
4.9.4
4.9.3
4.9.2
4.9.1
4.9.0
4.8.7
4.8.6
4.8.5
4.8.3
4.8.2
4.8.1
4.8.0
4.7.6
4.7.5
4.7.4
4.7.3
4.7.2
4.7.1
4.7.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.6
4.5.5
4.5.4
4.5.3
4.5.2
4.5.1
4.5.0
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.12
4.3.11
4.3.10
4.3.9
4.3.8
4.3.7
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
4.0.0-rc.6
4.0.0-rc.5
4.0.0-rc.3
4.0.0-rc.2
4.0.0-rc.1
4.0.0-rc.0
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.16
3.11.15
3.11.14
3.11.13
3.11.12
3.11.11
3.11.10
3.11.9
3.11.8
3.11.7
3.11.6
3.11.5
3.11.4
3.11.3
3.11.2
3.11.1
3.11.0
3.10.8
3.10.7
3.10.6
3.10.5
3.10.4
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.0
3.7.9
3.7.8
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
3.6.1
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.3.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.0
3.0.3
Authentication extension for the Ash Framework.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash_authentication/strategies/password/transformer.ex
defmodule AshAuthentication.Strategy.Password.Transformer do
@moduledoc """
DSL transformer for the password strategy.
Iterates through any password authentication strategies and ensures that all
the correct actions and settings are in place.
"""
alias Ash.{Resource, Type}
alias AshAuthentication.{GenerateTokenChange, Strategy, Strategy.Password}
alias Spark.{Dsl.Transformer, Error.DslError}
import AshAuthentication.Strategy.Custom.Helpers
import AshAuthentication.Utils
import AshAuthentication.Validations
import AshAuthentication.Validations.Action
import AshAuthentication.Validations.Attribute
@doc false
@spec transform(Password.t(), map) :: {:ok, Password.t() | map} | {:error, Exception.t()}
def transform(strategy, dsl_state) do
with :ok <- validate_identity_field(strategy.identity_field, dsl_state),
:ok <- validate_hashed_password_field(strategy.hashed_password_field, dsl_state),
strategy <-
maybe_set_field_lazy(strategy, :register_action_name, &:"register_with_#{&1.name}"),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
strategy.register_action_name,
&build_register_action(&1, strategy)
),
:ok <- validate_register_action(dsl_state, strategy),
strategy <-
maybe_set_field_lazy(strategy, :sign_in_action_name, &:"sign_in_with_#{&1.name}"),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
strategy.sign_in_action_name,
&build_sign_in_action(&1, strategy)
),
:ok <- validate_sign_in_action(dsl_state, strategy),
{:ok, dsl_state, strategy} <- maybe_transform_resettable(dsl_state, strategy),
{:ok, resource} <- persisted_option(dsl_state, :module) do
strategy = %{strategy | resource: resource}
dsl_state =
dsl_state
|> Transformer.replace_entity(
~w[authentication strategies]a,
strategy,
&(Strategy.name(&1) == strategy.name)
)
|> then(fn dsl_state ->
~w[sign_in_action_name register_action_name]a
|> Enum.map(&Map.get(strategy, &1))
|> register_strategy_actions(dsl_state, strategy)
end)
|> then(fn dsl_state ->
strategy
|> Map.get(:resettable, [])
|> Enum.flat_map(fn resettable ->
~w[request_password_reset_action_name password_reset_action_name]a
|> Enum.map(&Map.get(resettable, &1))
end)
|> register_strategy_actions(dsl_state, strategy)
end)
{:ok, dsl_state}
end
end
defp validate_identity_field(identity_field, dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, identity_field),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do
validate_attribute_unique_constraint(dsl_state, [identity_field], resource)
end
end
defp validate_hashed_password_field(hashed_password_field, dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, hashed_password_field),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
validate_attribute_option(attribute, resource, :sensitive?, [true])
end
end
defp build_register_action(_dsl_state, strategy) do
password_opts = [
type: Type.String,
allow_nil?: false,
constraints: [min_length: 8],
sensitive?: true
]
arguments =
[
Transformer.build_entity!(
Resource.Dsl,
[:actions, :create],
:argument,
Keyword.put(password_opts, :name, strategy.password_field)
)
]
|> maybe_append(
strategy.confirmation_required?,
Transformer.build_entity!(
Resource.Dsl,
[:actions, :create],
:argument,
Keyword.put(password_opts, :name, strategy.password_confirmation_field)
)
)
changes =
[]
|> maybe_append(
strategy.confirmation_required?,
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :validate,
validation: Password.PasswordConfirmationValidation
)
)
|> Enum.concat([
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: Password.HashPasswordChange
),
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: GenerateTokenChange
)
])
Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: strategy.register_action_name,
arguments: arguments,
changes: changes,
allow_nil_input: [strategy.hashed_password_field]
)
end
defp validate_register_action(dsl_state, strategy) do
with {:ok, action} <- validate_action_exists(dsl_state, strategy.register_action_name),
:ok <- validate_allow_nil_input(action, strategy.hashed_password_field),
:ok <- validate_password_argument(action, strategy.password_field, true),
:ok <-
validate_password_argument(
action,
strategy.password_confirmation_field,
strategy.confirmation_required?
),
:ok <- validate_action_has_change(action, Password.HashPasswordChange),
:ok <- validate_action_has_change(action, GenerateTokenChange) do
validate_action_has_validation(
action,
Password.PasswordConfirmationValidation,
strategy.confirmation_required?
)
end
end
defp validate_allow_nil_input(action, field) do
allowed_nil_fields =
action
|> Map.get(:arguments, [])
|> Stream.filter(&(&1.allow_nil? == true))
|> Stream.map(& &1.name)
|> Stream.concat(Map.get(action, :allow_nil_input, []))
|> Enum.into(MapSet.new())
if MapSet.member?(allowed_nil_fields, field) do
:ok
else
{:error,
DslError.exception(
path: [:actions, :allow_nil_input],
message:
"Expected the action `#{inspect(action.name)}` to allow nil input for the field `#{inspect(field)}`"
)}
end
end
defp validate_password_argument(action, field, true) do
with :ok <- validate_action_argument_option(action, field, :type, [Ash.Type.String]) do
validate_action_argument_option(action, field, :sensitive?, [true])
end
end
defp validate_password_argument(_action, _field, _), do: :ok
defp validate_action_has_validation(action, validation, true),
do: validate_action_has_validation(action, validation)
defp validate_action_has_validation(_action, _validation, _), do: :ok
defp build_sign_in_action(dsl_state, strategy) do
identity_attribute = Resource.Info.attribute(dsl_state, strategy.identity_field)
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: strategy.identity_field,
type: identity_attribute.type,
allow_nil?: false
),
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: strategy.password_field,
type: Type.String,
allow_nil?: false,
sensitive?: true
)
]
preparations = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: Password.SignInPreparation
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: strategy.sign_in_action_name,
arguments: arguments,
preparations: preparations,
get?: true
)
end
defp validate_sign_in_action(dsl_state, strategy) do
with {:ok, action} <- validate_action_exists(dsl_state, strategy.sign_in_action_name),
:ok <- validate_identity_argument(dsl_state, action, strategy.identity_field),
:ok <- validate_password_argument(action, strategy.password_field, true) do
validate_action_has_preparation(action, Password.SignInPreparation)
end
end
defp validate_identity_argument(dsl_state, action, identity_field) do
identity_attribute = Ash.Resource.Info.attribute(dsl_state, identity_field)
validate_action_argument_option(action, identity_field, :type, [identity_attribute.type])
end
defp maybe_transform_resettable(dsl_state, %{resettable: []} = strategy),
do: {:ok, dsl_state, strategy}
defp maybe_transform_resettable(dsl_state, %{resettable: [resettable]} = strategy) do
with resettable <-
maybe_set_field_lazy(
resettable,
:request_password_reset_action_name,
fn _ -> :"request_password_reset_with_#{strategy.name}" end
),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
resettable.request_password_reset_action_name,
&build_reset_request_action(&1, resettable, strategy)
),
:ok <- validate_reset_request_action(dsl_state, resettable, strategy),
resettable <-
maybe_set_field_lazy(
resettable,
:password_reset_action_name,
fn _ -> :"password_reset_with_#{strategy.name}" end
),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
resettable.password_reset_action_name,
&build_reset_action(&1, resettable, strategy)
),
:ok <- validate_reset_action(dsl_state, resettable, strategy) do
{:ok, dsl_state, %{strategy | resettable: [resettable]}}
else
{:error, reason} ->
{:error, reason}
end
end
defp maybe_transform_resettable(_dsl_state, %{resettable: [_ | _]}),
do:
DslError.exception(
path: [:authentication, :strategies, :password],
message: "Only one `resettable` entity may be present."
)
defp build_reset_request_action(dsl_state, resettable, strategy) do
identity_attribute = Resource.Info.attribute(dsl_state, strategy.identity_field)
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: strategy.identity_field,
type: identity_attribute.type,
allow_nil?: false
)
]
preparations = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: Password.RequestPasswordResetPreparation
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: resettable.request_password_reset_action_name,
arguments: arguments,
preparations: preparations
)
end
defp validate_reset_request_action(dsl_state, resettable, strategy) do
with {:ok, action} <-
validate_action_exists(dsl_state, resettable.request_password_reset_action_name),
:ok <- validate_identity_argument(dsl_state, action, strategy.identity_field) do
validate_action_has_preparation(action, Password.RequestPasswordResetPreparation)
end
end
defp build_reset_action(_dsl_state, resettable, strategy) do
password_opts = [
type: Type.String,
allow_nil?: false,
constraints: [min_length: 8],
sensitive?: true
]
arguments =
[
Transformer.build_entity!(
Resource.Dsl,
[:actions, :update],
:argument,
name: :reset_token,
type: Type.String,
sensitive?: true
),
Transformer.build_entity!(
Resource.Dsl,
[:actions, :update],
:argument,
Keyword.put(password_opts, :name, strategy.password_field)
)
]
|> maybe_append(
strategy.confirmation_required?,
Transformer.build_entity!(
Resource.Dsl,
[:actions, :update],
:argument,
Keyword.put(password_opts, :name, strategy.password_confirmation_field)
)
)
changes =
[
Transformer.build_entity!(Resource.Dsl, [:actions, :update], :validate,
validation: Password.ResetTokenValidation
)
]
|> maybe_append(
strategy.confirmation_required?,
Transformer.build_entity!(Resource.Dsl, [:actions, :update], :validate,
validation: Password.PasswordConfirmationValidation
)
)
|> Enum.concat([
Transformer.build_entity!(Resource.Dsl, [:actions, :update], :change,
change: Password.HashPasswordChange
),
Transformer.build_entity!(Resource.Dsl, [:actions, :update], :change,
change: GenerateTokenChange
)
])
Transformer.build_entity(Resource.Dsl, [:actions], :update,
name: resettable.password_reset_action_name,
arguments: arguments,
changes: changes,
accept: []
)
end
defp validate_reset_action(dsl_state, resettable, strategy) do
with {:ok, action} <-
validate_action_exists(dsl_state, resettable.password_reset_action_name),
:ok <- validate_action_has_validation(action, Password.ResetTokenValidation),
:ok <- validate_action_has_change(action, Password.HashPasswordChange),
:ok <- validate_password_argument(action, strategy.password_field, true),
:ok <-
validate_password_argument(
action,
strategy.password_confirmation_field,
strategy.confirmation_required?
),
:ok <-
validate_action_has_validation(
action,
Password.PasswordConfirmationValidation,
strategy.confirmation_required?
) do
validate_action_has_change(action, GenerateTokenChange)
end
end
end