mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-21 05:43:05 +12:00
improvement: remove the need for a strategy in changeset/query contexts. (#89)
The action -> strategy mapping is now stored directly in the resource DSL. Closes #84.
This commit is contained in:
parent
83d04170bb
commit
6dfbf03f11
16 changed files with 98 additions and 35 deletions
|
@ -26,7 +26,6 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|
|||
{:ok, user} <- AshAuthentication.subject_to_user(subject, strategy.resource) do
|
||||
user
|
||||
|> Changeset.new()
|
||||
|> Changeset.set_context(%{strategy: strategy})
|
||||
|> Changeset.for_update(strategy.confirm_action_name, params)
|
||||
|> api.update(options)
|
||||
else
|
||||
|
@ -53,7 +52,6 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|
|||
{:ok, _token_record} <-
|
||||
token_resource
|
||||
|> Changeset.new()
|
||||
|> Changeset.set_context(%{strategy: strategy})
|
||||
|> Changeset.for_create(store_changes_action, %{
|
||||
token: token,
|
||||
extra_data: changes,
|
||||
|
|
|
@ -4,7 +4,7 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
|
|||
"""
|
||||
|
||||
use Ash.Resource.Change
|
||||
alias AshAuthentication.{AddOn.Confirmation.Actions, Jwt}
|
||||
alias AshAuthentication.{AddOn.Confirmation.Actions, Info, Jwt}
|
||||
|
||||
alias Ash.{
|
||||
Changeset,
|
||||
|
@ -17,12 +17,13 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
|
|||
@impl true
|
||||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||
def change(changeset, _opts, _context) do
|
||||
case Map.fetch(changeset.context, :strategy) do
|
||||
case Info.strategy_for_action(changeset.resource, changeset.action.name) do
|
||||
{:ok, strategy} ->
|
||||
do_change(changeset, strategy)
|
||||
|
||||
:error ->
|
||||
raise AssumptionFailed, message: "Strategy is missing from the changeset context."
|
||||
raise AssumptionFailed,
|
||||
message: "Action does not correlate with an authentication strategy"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -66,13 +66,16 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
|
|||
:ok <- validate_confirmed_at_attribute(dsl_state, strategy),
|
||||
{:ok, dsl_state} <- maybe_build_change(dsl_state, Confirmation.ConfirmationHookChange),
|
||||
{:ok, resource} <- persisted_option(dsl_state, :module) do
|
||||
strategy = %{strategy | resource: resource}
|
||||
|
||||
dsl_state =
|
||||
dsl_state
|
||||
|> Transformer.replace_entity(
|
||||
[:authentication, :add_ons],
|
||||
%{strategy | resource: resource},
|
||||
strategy,
|
||||
&(&1.name == strategy.name)
|
||||
)
|
||||
|> Transformer.persist({:authentication_action, strategy.confirm_action_name}, strategy)
|
||||
|
||||
{:ok, dsl_state}
|
||||
else
|
||||
|
|
|
@ -7,10 +7,15 @@ defmodule AshAuthentication.Info do
|
|||
extension: AshAuthentication,
|
||||
sections: [:authentication]
|
||||
|
||||
alias AshAuthentication.Strategy
|
||||
alias Spark.Dsl.Extension
|
||||
|
||||
@type dsl_or_resource :: module | map
|
||||
|
||||
@doc """
|
||||
Retrieve a named strategy from a resource.
|
||||
"""
|
||||
@spec strategy(dsl_or_resource :: map | module, atom) :: {:ok, strategy} | :error
|
||||
@spec strategy(dsl_or_resource | module, atom) :: {:ok, strategy} | :error
|
||||
when strategy: struct
|
||||
def strategy(dsl_or_resource, name) do
|
||||
dsl_or_resource
|
||||
|
@ -24,7 +29,7 @@ defmodule AshAuthentication.Info do
|
|||
@doc """
|
||||
Retrieve a named strategy from a resource (raising version).
|
||||
"""
|
||||
@spec strategy!(dsl_or_resource :: map | module, atom) :: strategy | no_return
|
||||
@spec strategy!(dsl_or_resource | module, atom) :: strategy | no_return
|
||||
when strategy: struct
|
||||
def strategy!(dsl_or_resource, name) do
|
||||
case strategy(dsl_or_resource, name) do
|
||||
|
@ -35,4 +40,31 @@ defmodule AshAuthentication.Info do
|
|||
raise "No strategy named `#{inspect(name)}` found on resource `#{inspect(dsl_or_resource)}`"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Given an action name, retrieve the strategy it is for from the DSL
|
||||
configuration.
|
||||
"""
|
||||
@spec strategy_for_action(dsl_or_resource, atom) :: {:ok, Strategy.t()} | :error
|
||||
def strategy_for_action(dsl_or_resource, action_name) do
|
||||
case Extension.get_persisted(dsl_or_resource, {:authentication_action, action_name}) do
|
||||
nil -> :error
|
||||
value -> {:ok, value}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Given an action name, retrieve the strategy it is for from the DSL
|
||||
configuration.
|
||||
"""
|
||||
@spec strategy_for_action!(dsl_or_resource, atom) :: Strategy.t() | no_return
|
||||
def strategy_for_action!(dsl_or_resource, action_name) do
|
||||
case strategy_for_action(dsl_or_resource, action_name) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
:error ->
|
||||
raise "No strategy action named `#{inspect(action_name)}` found on resource `#{inspect(dsl_or_resource)}`"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,7 +26,6 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
|
|||
|
||||
strategy.resource
|
||||
|> Query.new()
|
||||
|> Query.set_context(%{strategy: strategy})
|
||||
|> Query.for_read(strategy.sign_in_action_name, params)
|
||||
|> api.read(options)
|
||||
|> case do
|
||||
|
@ -45,7 +44,6 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
|
|||
|
||||
strategy.resource
|
||||
|> Changeset.new()
|
||||
|> Changeset.set_context(%{strategy: strategy})
|
||||
|> Changeset.for_create(strategy.register_action_name, params,
|
||||
upsert?: true,
|
||||
upsert_identity: action.upsert_identity
|
||||
|
|
|
@ -4,7 +4,7 @@ defmodule AshAuthentication.Strategy.OAuth2.IdentityChange do
|
|||
"""
|
||||
|
||||
use Ash.Resource.Change
|
||||
alias AshAuthentication.UserIdentity
|
||||
alias AshAuthentication.{Info, UserIdentity}
|
||||
alias Ash.{Changeset, Error.Framework.AssumptionFailed, Resource.Change}
|
||||
import AshAuthentication.Utils, only: [is_falsy: 1]
|
||||
|
||||
|
@ -12,13 +12,15 @@ defmodule AshAuthentication.Strategy.OAuth2.IdentityChange do
|
|||
@impl true
|
||||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||
def change(changeset, _opts, _context) do
|
||||
case Map.fetch(changeset.context, :strategy) do
|
||||
case Info.strategy_for_action(changeset.resource, changeset.action.name) do
|
||||
{:ok, strategy} ->
|
||||
do_change(changeset, strategy)
|
||||
|
||||
:error ->
|
||||
{:error,
|
||||
AssumptionFailed.exception(message: "Strategy is missing from the changeset context.")}
|
||||
AssumptionFailed.exception(
|
||||
message: "Action does not correlate with an authentication strategy"
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
|
|||
"""
|
||||
use Ash.Resource.Preparation
|
||||
alias Ash.{Error.Framework.AssumptionFailed, Query, Resource.Preparation}
|
||||
alias AshAuthentication.{Errors.AuthenticationFailed, Jwt, UserIdentity}
|
||||
alias AshAuthentication.{Errors.AuthenticationFailed, Info, Jwt, UserIdentity}
|
||||
require Ash.Query
|
||||
import AshAuthentication.Utils, only: [is_falsy: 1]
|
||||
|
||||
|
@ -19,7 +19,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
|
|||
@impl true
|
||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
||||
def prepare(query, _opts, _context) do
|
||||
case Map.fetch(query.context, :strategy) do
|
||||
case Info.strategy_for_action(query.resource, query.action.name) do
|
||||
:error ->
|
||||
{:error,
|
||||
AssumptionFailed.exception(message: "Strategy is missing from the changeset context.")}
|
||||
|
|
|
@ -52,13 +52,23 @@ defmodule AshAuthentication.Strategy.OAuth2.Transformer do
|
|||
:ok <- maybe_validate_register_action(dsl_state, strategy),
|
||||
:ok <- maybe_validate_sign_in_action(dsl_state, strategy),
|
||||
{:ok, resource} <- persisted_option(dsl_state, :module) do
|
||||
strategy = %{strategy | resource: resource}
|
||||
|
||||
dsl_state =
|
||||
dsl_state
|
||||
|> Transformer.replace_entity(
|
||||
[:authentication, :strategies],
|
||||
%{strategy | resource: resource},
|
||||
~w[authentication strategies]a,
|
||||
strategy,
|
||||
&(&1.name == strategy.name)
|
||||
)
|
||||
|> then(fn dsl_state ->
|
||||
~w[register_action_name sign_in_action_name]a
|
||||
|> Stream.map(&Map.get(strategy, &1))
|
||||
|> Enum.reduce(
|
||||
dsl_state,
|
||||
&Transformer.persist(&2, {:authentication_action, &1}, strategy)
|
||||
)
|
||||
end)
|
||||
|
||||
{:ok, dsl_state}
|
||||
else
|
||||
|
|
|
@ -19,7 +19,6 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
|||
|
||||
strategy.resource
|
||||
|> Query.new()
|
||||
|> Query.set_context(%{strategy: strategy})
|
||||
|> Query.for_read(strategy.sign_in_action_name, params)
|
||||
|> api.read(options)
|
||||
|> case do
|
||||
|
@ -37,7 +36,6 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
|||
|
||||
strategy.resource
|
||||
|> Changeset.new()
|
||||
|> Changeset.set_context(%{strategy: strategy})
|
||||
|> Changeset.for_create(strategy.register_action_name, params)
|
||||
|> api.create(options)
|
||||
end
|
||||
|
@ -55,7 +53,6 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
|||
|
||||
strategy.resource
|
||||
|> Query.new()
|
||||
|> Query.set_context(%{strategy: strategy})
|
||||
|> Query.for_read(resettable.request_password_reset_action_name, params)
|
||||
|> api.read(options)
|
||||
|> case do
|
||||
|
@ -85,7 +82,6 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
|||
|
||||
user
|
||||
|> Changeset.new()
|
||||
|> Changeset.set_context(%{strategy: strategy})
|
||||
|> Changeset.for_update(resettable.password_reset_action_name, params)
|
||||
|> api.update(options)
|
||||
else
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChange do
|
|||
|
||||
use Ash.Resource.Change
|
||||
alias Ash.{Changeset, Error.Framework.AssumptionFailed, Resource.Change}
|
||||
alias AshAuthentication.Info
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
|
@ -15,7 +16,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChange do
|
|||
def change(changeset, _opts, _) do
|
||||
changeset
|
||||
|> Changeset.before_action(fn changeset ->
|
||||
with {:ok, strategy} <- Map.fetch(changeset.context, :strategy),
|
||||
with {:ok, strategy} <- Info.strategy_for_action(changeset.resource, changeset.action.name),
|
||||
value when is_binary(value) <-
|
||||
Changeset.get_argument(changeset, strategy.password_field),
|
||||
{:ok, hash} <- strategy.hash_provider.hash(value) do
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
|
|||
|
||||
use Ash.Resource.Validation
|
||||
alias Ash.{Changeset, Error.Changes.InvalidArgument, Error.Framework.AssumptionFailed}
|
||||
alias AshAuthentication.Info
|
||||
|
||||
@doc """
|
||||
Validates that the password and password confirmation fields contain
|
||||
|
@ -15,7 +16,7 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
|
|||
@impl true
|
||||
@spec validate(Changeset.t(), keyword) :: :ok | {:error, String.t() | Exception.t()}
|
||||
def validate(changeset, _) do
|
||||
case Map.fetch(changeset.context, :strategy) do
|
||||
case Info.strategy_for_action(changeset.resource, changeset.action.name) do
|
||||
{:ok, %{confirmation_required?: true} = strategy} ->
|
||||
validate_password_confirmation(changeset, strategy)
|
||||
|
||||
|
@ -24,7 +25,9 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
|
|||
|
||||
:error ->
|
||||
{:error,
|
||||
AssumptionFailed.exception(message: "Strategy is missing from the changeset context.")}
|
||||
AssumptionFailed.exception(
|
||||
message: "Action does not correlate with an authentication strategy"
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,14 +10,14 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
|
|||
"""
|
||||
use Ash.Resource.Preparation
|
||||
alias Ash.{Query, Resource.Preparation}
|
||||
alias AshAuthentication.Strategy.Password
|
||||
alias AshAuthentication.{Info, Strategy.Password}
|
||||
require Ash.Query
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
||||
def prepare(query, _opts, _context) do
|
||||
strategy = Map.fetch!(query.context, :strategy)
|
||||
strategy = Info.strategy_for_action!(query.resource, query.action.name)
|
||||
|
||||
if Enum.any?(strategy.resettable) do
|
||||
identity_field = strategy.identity_field
|
||||
|
|
|
@ -5,13 +5,13 @@ defmodule AshAuthentication.Strategy.Password.ResetTokenValidation do
|
|||
|
||||
use Ash.Resource.Validation
|
||||
alias Ash.{Changeset, Error.Changes.InvalidArgument}
|
||||
alias AshAuthentication.Jwt
|
||||
alias AshAuthentication.{Info, Jwt}
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec validate(Changeset.t(), keyword) :: :ok | {:error, Exception.t()}
|
||||
def validate(changeset, _) do
|
||||
with {:ok, strategy} <- Map.fetch(changeset.context, :strategy),
|
||||
with {:ok, strategy} <- Info.strategy_for_action(changeset.resource, changeset.action.name),
|
||||
token when is_binary(token) <- Changeset.get_argument(changeset, :reset_token),
|
||||
{:ok, %{"act" => token_action}, _} <- Jwt.verify(token, changeset.resource),
|
||||
{:ok, [resettable]} <- Map.fetch(strategy, :resettable),
|
||||
|
|
|
@ -13,7 +13,7 @@ defmodule AshAuthentication.Strategy.Password.SignInPreparation do
|
|||
an authentication failed error.
|
||||
"""
|
||||
use Ash.Resource.Preparation
|
||||
alias AshAuthentication.{Errors.AuthenticationFailed, Jwt}
|
||||
alias AshAuthentication.{Errors.AuthenticationFailed, Info, Jwt}
|
||||
alias Ash.{Query, Resource.Preparation}
|
||||
require Ash.Query
|
||||
|
||||
|
@ -21,7 +21,7 @@ defmodule AshAuthentication.Strategy.Password.SignInPreparation do
|
|||
@impl true
|
||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
||||
def prepare(query, _opts, _context) do
|
||||
strategy = Map.fetch!(query.context, :strategy)
|
||||
strategy = Info.strategy_for_action!(query.resource, query.action.name)
|
||||
identity_field = strategy.identity_field
|
||||
identity = Query.get_argument(query, identity_field)
|
||||
|
||||
|
|
|
@ -71,13 +71,35 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
|||
: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(
|
||||
[:authentication, :strategies],
|
||||
%{strategy | resource: resource},
|
||||
~w[authentication strategies]a,
|
||||
strategy,
|
||||
&(&1.name == strategy.name)
|
||||
)
|
||||
|> then(fn dsl_state ->
|
||||
~w[sign_in_action_name register_action_name]a
|
||||
|> Stream.map(&Map.get(strategy, &1))
|
||||
|> Enum.reduce(
|
||||
dsl_state,
|
||||
&Transformer.persist(&2, {:authentication_action, &1}, strategy)
|
||||
)
|
||||
end)
|
||||
|> then(fn dsl_state ->
|
||||
strategy
|
||||
|> Map.get(:resettable, [])
|
||||
|> Stream.flat_map(fn resettable ->
|
||||
~w[request_password_reset_action_name password_reset_action_name]a
|
||||
|> Stream.map(&Map.get(resettable, &1))
|
||||
end)
|
||||
|> Enum.reduce(
|
||||
dsl_state,
|
||||
&Transformer.persist(&2, {:authentication_action, &1}, strategy)
|
||||
)
|
||||
end)
|
||||
|
||||
{:ok, dsl_state}
|
||||
end
|
||||
|
|
|
@ -79,12 +79,9 @@ defmodule DataCase do
|
|||
|> Map.put_new(:password, password)
|
||||
|> Map.put_new(:password_confirmation, password)
|
||||
|
||||
{:ok, strategy} = AshAuthentication.Info.strategy(Example.User, :password)
|
||||
|
||||
user =
|
||||
Example.User
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.set_context(%{strategy: strategy})
|
||||
|> Ash.Changeset.for_create(:register_with_password, attrs)
|
||||
|> Example.create!()
|
||||
|
||||
|
|
Loading…
Reference in a new issue