Packages
ash_authentication
5.0.0-rc.10
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.
Current section
Files
Jump to
Current section
Files
lib/ash_authentication/strategies/recovery_code/transformer.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.RecoveryCode.Transformer do
@moduledoc """
DSL transformer for the recovery_code strategy.
"""
alias AshAuthentication.Strategy
alias AshAuthentication.Strategy.RecoveryCode
alias Spark.Dsl.Transformer
alias Spark.Error.DslError
import AshAuthentication.Strategy.Custom.Helpers
import AshAuthentication.Utils
import AshAuthentication.Validations
import AshAuthentication.Validations.Action
@doc false
@spec transform(RecoveryCode.t(), map) ::
{:ok, RecoveryCode.t() | map} | {:error, Exception.t()}
def transform(strategy, dsl) do
with strategy <- transform_audit_log_window(strategy),
:ok <- validate_recovery_codes_relationship(strategy, dsl),
{:ok, dsl, strategy} <- handle_verify_action(dsl, strategy),
{:ok, dsl, strategy} <- handle_generate_action(dsl, strategy),
{:ok, resource} <- persisted_option(dsl, :module) do
strategy = %{strategy | resource: resource}
action_names =
[strategy.verify_action_name]
|> maybe_append(strategy.generate_enabled?, strategy.generate_action_name)
dsl = register_strategy_actions(action_names, dsl, strategy)
{:ok,
Transformer.replace_entity(
dsl,
[:authentication, :strategies],
strategy,
&(Strategy.name(&1) == strategy.name)
)}
end
end
defp transform_audit_log_window(strategy) when is_integer(strategy.audit_log_window),
do: %{strategy | audit_log_window: strategy.audit_log_window * 60}
defp transform_audit_log_window(strategy) when is_tuple(strategy.audit_log_window) do
{value, unit} = strategy.audit_log_window
seconds =
case unit do
:days -> value * 24 * 60 * 60
:hours -> value * 60 * 60
:minutes -> value * 60
:seconds -> value
end
%{strategy | audit_log_window: seconds}
end
defp validate_recovery_codes_relationship(strategy, dsl) do
case find_relationship(dsl, strategy.recovery_codes_relationship_name) do
{:ok, relationship} ->
if relationship.type == :has_many do
:ok
else
module = Transformer.get_persisted(dsl, :module)
{:error,
DslError.exception(
module: module,
path: [
:authentication,
:strategies,
:recovery_code,
strategy.name,
:recovery_codes_relationship_name
],
message:
"The relationship `#{inspect(strategy.recovery_codes_relationship_name)}` must be a `has_many` relationship."
)}
end
:error ->
module = Transformer.get_persisted(dsl, :module)
{:error,
DslError.exception(
module: module,
path: [
:authentication,
:strategies,
:recovery_code,
strategy.name,
:recovery_codes_relationship_name
],
message: """
The relationship `#{inspect(strategy.recovery_codes_relationship_name)}` does not exist on this resource.
Add a has_many relationship:
relationships do
has_many #{inspect(strategy.recovery_codes_relationship_name)}, YourApp.RecoveryCode
end
"""
)}
end
end
# sobelow_skip ["DOS.BinToAtom"]
defp handle_verify_action(dsl, strategy) when is_nil(strategy.verify_action_name),
do:
handle_verify_action(dsl, %{strategy | verify_action_name: :"verify_with_#{strategy.name}"})
defp handle_verify_action(dsl, strategy) do
with {:ok, dsl} <-
maybe_build_action(
dsl,
strategy.verify_action_name,
&build_verify_action(&1, strategy)
),
:ok <- validate_verify_action(dsl, strategy) do
{:ok, dsl, strategy}
end
end
defp build_verify_action(dsl, strategy) do
module = Transformer.get_persisted(dsl, :module)
arguments = [
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :action], :argument,
name: :user,
type: Ash.Type.Struct,
allow_nil?: false,
sensitive?: true,
description: "The user to verify the recovery code for.",
constraints: [instance_of: module]
),
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :action], :argument,
name: :code,
type: Ash.Type.String,
allow_nil?: false,
sensitive?: true,
description: "The recovery code to verify."
)
]
preparations =
case strategy.brute_force_strategy do
{:preparation, preparation} ->
[
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :action], :prepare,
preparation: preparation
)
]
{:audit_log, _audit_log_name} ->
[
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :action], :prepare,
preparation:
{AshAuthentication.AddOn.AuditLog.BruteForcePreparation,
action_name: strategy.verify_action_name}
)
]
_ ->
[]
end
touches_resources =
with {:audit_log, audit_log} <- strategy.brute_force_strategy,
{:ok, audit_log} <- AshAuthentication.Info.strategy(dsl, audit_log) do
Enum.uniq([module, strategy.recovery_code_resource, audit_log.audit_log_resource])
else
_ -> [module, strategy.recovery_code_resource]
end
Transformer.build_entity(Ash.Resource.Dsl, [:actions], :action,
name: strategy.verify_action_name,
arguments: arguments,
returns: :term,
transaction?: true,
description: "Verify a recovery code and return the user if valid, nil otherwise.",
touches_resources: touches_resources,
run: AshAuthentication.Strategy.RecoveryCode.VerifyAction,
preparations: preparations
)
end
defp validate_verify_action(dsl, strategy) do
module = Transformer.get_persisted(dsl, :module)
with {:ok, action} <- validate_action_exists(dsl, strategy.verify_action_name),
:ok <- validate_action_has_argument(action, :code),
:ok <- validate_action_has_argument(action, :user),
:ok <- validate_action_argument_option(action, :code, :type, [Ash.Type.String, :string]),
:ok <- validate_action_argument_option(action, :code, :allow_nil?, [false]),
:ok <- validate_action_argument_option(action, :user, :type, [Ash.Type.Struct, :struct]),
:ok <- validate_action_argument_option(action, :user, :allow_nil?, [false]),
:ok <-
validate_action_option(action, :run, [
{AshAuthentication.Strategy.RecoveryCode.VerifyAction, []}
]),
:ok <- validate_strategy_preparation(action, strategy.brute_force_strategy) do
constraints =
action.arguments
|> Enum.find(%{}, &(&1.name == :user))
|> Map.get(:constraints, [])
if constraints[:instance_of] == module do
:ok
else
{:error,
DslError.exception(
module: module,
path: [:actions, :action, strategy.verify_action_name, :arguments, :user],
message: "The argument should have the constraint `[instance_of: #{inspect(module)}]`"
)}
end
end
end
defp handle_generate_action(dsl, strategy) when strategy.generate_enabled? != true,
do: {:ok, dsl, strategy}
# sobelow_skip ["DOS.BinToAtom"]
defp handle_generate_action(dsl, strategy) when is_nil(strategy.generate_action_name),
do:
handle_generate_action(dsl, %{
strategy
| generate_action_name: :"generate_#{strategy.name}_codes"
})
defp handle_generate_action(dsl, strategy) do
with {:ok, dsl} <-
maybe_build_action(
dsl,
strategy.generate_action_name,
&build_generate_action(&1, strategy)
),
:ok <- validate_generate_action(dsl, strategy) do
{:ok, dsl, strategy}
end
end
defp build_generate_action(_dsl, strategy) do
arguments = [
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :update], :argument,
name: :recovery_codes,
type: {:array, :string},
allow_nil?: false,
public?: false,
sensitive?: true,
default:
{AshAuthentication.Strategy.RecoveryCode.Actions, :generate_codes_list,
[strategy.code_length, strategy.recovery_code_count, strategy.code_alphabet]}
)
]
changes = [
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :update], :change,
change:
{Ash.Resource.Change.CascadeDestroy,
relationship: strategy.recovery_codes_relationship_name, after_action?: false}
),
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :update], :change,
change:
{AshAuthentication.Strategy.RecoveryCode.HashRecoveryCodesChange,
hash_provider: strategy.hash_provider}
),
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :update], :change,
change:
{Ash.Resource.Change.ManageRelationship,
argument: :recovery_codes,
relationship: strategy.recovery_codes_relationship_name,
opts: [type: :create, value_is_key: strategy.code_field]}
)
]
metadata = [
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :update], :metadata,
name: :recovery_codes,
type: {:array, :string},
allow_nil?: false
)
]
Transformer.build_entity(Ash.Resource.Dsl, [:actions], :update,
name: strategy.generate_action_name,
arguments: arguments,
changes: changes,
metadata: metadata,
accept: [],
require_atomic?: false,
touches_resources: [strategy.recovery_code_resource],
description: "Generate new recovery codes for the user, replacing any existing codes."
)
end
defp validate_generate_action(dsl, strategy) do
with {:ok, action} <- validate_action_exists(dsl, strategy.generate_action_name) do
validate_action_has_argument(action, :recovery_codes)
end
end
defp validate_strategy_preparation(action, {:preparation, preparation}),
do: validate_action_has_preparation(action, preparation)
defp validate_strategy_preparation(action, {:audit_log, _audit_log_name}),
do:
validate_action_has_preparation(
action,
AshAuthentication.AddOn.AuditLog.BruteForcePreparation
)
defp validate_strategy_preparation(_, _), do: :ok
end