improvement: add select_for_senders (#189)

* improvement: add select_for_senders
fix: select `hashed_password` on sign in preparation

* improvement: include metadata declaration on register action

* chore: fix typo
This commit is contained in:
Zach Daniel 2023-02-12 03:15:23 -05:00 committed by GitHub
parent ca3dac3878
commit a2bba519c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 106 additions and 1 deletions

View file

@ -124,6 +124,7 @@ defmodule AshAuthentication do
), ),
transformers: [ transformers: [
AshAuthentication.Transformer, AshAuthentication.Transformer,
AshAuthentication.Transformer.SetSelectForSenders,
AshAuthentication.Strategy.Custom.Transformer AshAuthentication.Strategy.Custom.Transformer
], ],
verifiers: [ verifiers: [

View file

@ -80,6 +80,16 @@ defmodule AshAuthentication.Dsl do
action doesn't exist, one will be generated for you. action doesn't exist, one will be generated for you.
""", """,
default: :get_by_subject default: :get_by_subject
],
select_for_senders: [
type: {:list, :atom},
doc: """
A list of fields that we will ensure are selected whenever a sender will be invoked.
This is useful if using something like `ash_graphql` which by default only selects
what fields appear in the query, and if you are exposing these actions that way.
Defaults to `[:email]` if there is an `:email` attribute on the resource, and `[]`
otherwise.
"""
] ]
], ],
sections: [ sections: [

View file

@ -24,9 +24,13 @@ defmodule AshAuthentication.Strategy.MagicLink.RequestPreparation do
identity_field = strategy.identity_field identity_field = strategy.identity_field
identity = Query.get_argument(query, identity_field) identity = Query.get_argument(query, identity_field)
select_for_senders = Info.authentication_select_for_senders!(query.resource)
query query
|> Query.filter(ref(^identity_field) == ^identity) |> Query.filter(ref(^identity_field) == ^identity)
|> Query.before_action(fn query ->
Ash.Query.ensure_selected(query, select_for_senders)
end)
|> Query.after_action(&after_action(&1, &2, strategy)) |> Query.after_action(&after_action(&1, &2, strategy))
end end

View file

@ -25,9 +25,13 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
if Enum.any?(strategy.resettable) do if Enum.any?(strategy.resettable) do
identity_field = strategy.identity_field identity_field = strategy.identity_field
identity = Query.get_argument(query, identity_field) identity = Query.get_argument(query, identity_field)
select_for_senders = Info.authentication_select_for_senders!(query.resource)
query query
|> Query.filter(ref(^identity_field) == ^identity) |> Query.filter(ref(^identity_field) == ^identity)
|> Query.before_action(fn query ->
Ash.Query.ensure_selected(query, select_for_senders)
end)
|> Query.after_action(&after_action(&1, &2, strategy)) |> Query.after_action(&after_action(&1, &2, strategy))
else else
query query

View file

@ -27,6 +27,9 @@ defmodule AshAuthentication.Strategy.Password.SignInPreparation do
query query
|> Query.filter(ref(^identity_field) == ^identity) |> Query.filter(ref(^identity_field) == ^identity)
|> Query.before_action(fn query ->
Ash.Query.ensure_selected(query, [strategy.hashed_password_field])
end)
|> Query.after_action(fn |> Query.after_action(fn
query, [record] when is_binary(:erlang.map_get(strategy.hashed_password_field, record)) -> query, [record] when is_binary(:erlang.map_get(strategy.hashed_password_field, record)) ->
password = Query.get_argument(query, strategy.password_field) password = Query.get_argument(query, strategy.password_field)

View file

@ -85,7 +85,7 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
end end
end end
defp build_register_action(_dsl_state, strategy) do defp build_register_action(dsl_state, strategy) do
password_opts = [ password_opts = [
type: Type.String, type: Type.String,
allow_nil?: false, allow_nil?: false,
@ -129,10 +129,24 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
) )
]) ])
metadata =
if AshAuthentication.Info.authentication_tokens_enabled?(dsl_state) do
[
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :metadata,
name: :token,
type: :string,
allow_nil?: false
)
]
else
[]
end
Transformer.build_entity(Resource.Dsl, [:actions], :create, Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: strategy.register_action_name, name: strategy.register_action_name,
arguments: arguments, arguments: arguments,
changes: changes, changes: changes,
metadata: metadata,
allow_nil_input: [strategy.hashed_password_field] allow_nil_input: [strategy.hashed_password_field]
) )
end end

View file

@ -0,0 +1,34 @@
defmodule AshAuthentication.Transformer.SetSelectForSenders do
@moduledoc """
Sets the `select_for_senders` options to its default value.
"""
use Spark.Dsl.Transformer
alias AshAuthentication.Info
alias Spark.Dsl.Transformer
@doc false
@impl true
@spec after?(any) :: boolean()
def after?(_), do: true
@impl true
def transform(dsl_state) do
dsl_state
|> Info.authentication_select_for_senders()
|> case do
:error ->
if Ash.Resource.Info.attribute(dsl_state, :email) do
{:ok,
Transformer.set_option(dsl_state, [:authentication], :select_for_senders, [
:email
])}
else
{:ok, Transformer.set_option(dsl_state, [:authentication], :select_for_senders, [])}
end
_ ->
{:ok, dsl_state}
end
end
end

View file

@ -129,6 +129,39 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
assert log =~ ~r/password reset request for user #{user.username}/i assert log =~ ~r/password reset request for user #{user.username}/i
end end
test "it selects required fields for senders using `select_for_senders`" do
user = build_user()
{:ok, strategy} = Info.strategy(Example.User, :password)
log =
capture_log(fn ->
params = %{"username" => user.username}
options = []
api = Info.authentication_api!(strategy.resource)
resettable = strategy.resettable |> Enum.at(0)
result =
strategy.resource
|> Ash.Query.new()
|> Ash.Query.set_context(%{
private: %{
ash_authentication?: true
}
})
|> Ash.Query.for_read(resettable.request_password_reset_action_name, params)
|> Ash.Query.select([])
|> api.read(options)
|> case do
{:ok, _} -> :ok
{:error, reason} -> {:error, reason}
end
assert result == :ok
end)
assert log =~ ~r/password reset request for user #{user.username}/i
end
test "it doesn't generate a reset token when no matching user exists and the strategy is resettable" do test "it doesn't generate a reset token when no matching user exists and the strategy is resettable" do
{:ok, strategy} = Info.strategy(Example.User, :password) {:ok, strategy} = Info.strategy(Example.User, :password)

View file

@ -130,6 +130,8 @@ defmodule Example.User do
authentication do authentication do
api Example api Example
select_for_senders([:username])
tokens do tokens do
enabled? true enabled? true
store_all_tokens? true store_all_tokens? true