improvement(actions): All actions now take optional arguments for the underlying API call. (#61)

Closes #37.
This commit is contained in:
James Harton 2022-12-05 13:04:42 +13:00 committed by GitHub
parent 29f495f5fc
commit 2cee21c9ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 182 additions and 121 deletions

View file

@ -150,13 +150,18 @@ defmodule AshAuthentication do
iex> %{id: user_id} = build_user()
...> {:ok, %{id: ^user_id}} = subject_to_user("user?id=#{user_id}", Example.User)
"""
@spec subject_to_user(subject | URI.t(), Resource.t()) ::
{:ok, Resource.record()} | {:error, any}
def subject_to_user(subject, resource) when is_binary(subject),
do: subject |> URI.parse() |> subject_to_user(resource)
def subject_to_user(%URI{path: subject_name, query: primary_key} = _subject, resource) do
Any options passed will be passed to the underlying `Api.read/2` callback.
"""
@spec subject_to_user(subject | URI.t(), Resource.t(), keyword) ::
{:ok, Resource.record()} | {:error, any}
def subject_to_user(subject, resource, options \\ [])
def subject_to_user(subject, resource, options) when is_binary(subject),
do: subject |> URI.parse() |> subject_to_user(resource, options)
def subject_to_user(%URI{path: subject_name, query: primary_key} = _subject, resource, options) do
with {:ok, resource_subject_name} <- Info.authentication_subject_name(resource),
^subject_name <- to_string(resource_subject_name),
{:ok, action_name} <- Info.authentication_get_by_subject_action_name(resource),
@ -169,7 +174,7 @@ defmodule AshAuthentication do
resource
|> Query.for_read(action_name, %{})
|> Query.filter(^primary_key)
|> api.read()
|> api.read(options)
|> case do
{:ok, [user]} -> {:ok, user}
_ -> {:error, NotFound.exception([])}

View file

@ -18,8 +18,8 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
@doc """
Attempt to confirm a user.
"""
@spec confirm(Confirmation.t(), map) :: {:ok, Resource.record()} | {:error, any}
def confirm(strategy, params) do
@spec confirm(Confirmation.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def confirm(strategy, params, options) do
with {:ok, api} <- Info.authentication_api(strategy.resource),
{:ok, token} <- Map.fetch(params, "confirm"),
{:ok, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource),
@ -28,7 +28,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|> Changeset.new()
|> Changeset.set_context(%{strategy: strategy})
|> Changeset.for_update(strategy.confirm_action_name, params)
|> api.update()
|> api.update(options)
else
:error -> {:error, InvalidToken.exception(type: :confirmation)}
{:error, reason} -> {:error, reason}

View file

@ -44,6 +44,7 @@ defimpl AshAuthentication.Strategy, for: AshAuthentication.AddOn.Confirmation do
def plug(strategy, :confirm, conn), do: Confirmation.Plug.confirm(conn, strategy)
@doc false
@spec action(Confirmation.t(), action, map) :: {:ok, Resource.record()} | {:error, any}
def action(strategy, :confirm, params), do: Confirmation.Actions.confirm(strategy, params)
@spec action(Confirmation.t(), action, map, keyword) :: {:ok, Resource.record()} | {:error, any}
def action(strategy, :confirm, params, options),
do: Confirmation.Actions.confirm(strategy, params, options)
end

View file

@ -11,8 +11,8 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
@doc """
Attempt to sign in a user.
"""
@spec sign_in(OAuth2.t(), map) :: {:ok, Resource.record()} | {:error, any}
def sign_in(%OAuth2{} = strategy, _params) when strategy.registration_enabled?,
@spec sign_in(OAuth2.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def sign_in(%OAuth2{} = strategy, _params, _options) when strategy.registration_enabled?,
do:
{:error,
NoSuchAction.exception(
@ -21,14 +21,14 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
type: :read
)}
def sign_in(%OAuth2{} = strategy, params) do
def sign_in(%OAuth2{} = strategy, params, options) do
api = Info.authentication_api!(strategy.resource)
strategy.resource
|> Query.new()
|> Query.set_context(%{strategy: strategy})
|> Query.for_read(strategy.sign_in_action_name, params)
|> api.read()
|> api.read(options)
|> case do
{:ok, [user]} -> {:ok, user}
_ -> {:error, Errors.AuthenticationFailed.exception([])}
@ -38,8 +38,8 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
@doc """
Attempt to register a new user.
"""
@spec register(OAuth2.t(), map) :: {:ok, Resource.record()} | {:error, any}
def register(%OAuth2{} = strategy, params) when strategy.registration_enabled? do
@spec register(OAuth2.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def register(%OAuth2{} = strategy, params, options) when strategy.registration_enabled? do
api = Info.authentication_api!(strategy.resource)
action = Resource.Info.action(strategy.resource, strategy.register_action_name, :create)
@ -50,10 +50,10 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
upsert?: true,
upsert_identity: action.upsert_identity
)
|> api.create()
|> api.create(options)
end
def register(%OAuth2{} = strategy, _params),
def register(%OAuth2{} = strategy, _params, _options),
do:
{:error,
NoSuchAction.exception(

View file

@ -56,7 +56,10 @@ defimpl AshAuthentication.Strategy, for: AshAuthentication.Strategy.OAuth2 do
@doc """
Perform actions.
"""
@spec action(OAuth2.t(), action, map) :: {:ok, Resource.record()} | {:error, any}
def action(strategy, :register, params), do: OAuth2.Actions.register(strategy, params)
def action(strategy, :sign_in, params), do: OAuth2.Actions.sign_in(strategy, params)
@spec action(OAuth2.t(), action, map, keyword) :: {:ok, Resource.record()} | {:error, any}
def action(strategy, :register, params, options),
do: OAuth2.Actions.register(strategy, params, options)
def action(strategy, :sign_in, params, options),
do: OAuth2.Actions.sign_in(strategy, params, options)
end

View file

@ -12,16 +12,16 @@ defmodule AshAuthentication.Strategy.Password.Actions do
@doc """
Attempt to sign in a user.
"""
@spec sign_in(Password.t(), map) ::
@spec sign_in(Password.t(), map, keyword) ::
{:ok, Resource.record()} | {:error, Errors.AuthenticationFailed.t()}
def sign_in(%Password{} = strategy, params) do
def sign_in(%Password{} = strategy, params, options) do
api = Info.authentication_api!(strategy.resource)
strategy.resource
|> Query.new()
|> Query.set_context(%{strategy: strategy})
|> Query.for_read(strategy.sign_in_action_name, params)
|> api.read()
|> api.read(options)
|> case do
{:ok, [user]} -> {:ok, user}
_ -> {:error, Errors.AuthenticationFailed.exception([])}
@ -31,24 +31,25 @@ defmodule AshAuthentication.Strategy.Password.Actions do
@doc """
Attempt to register a new user.
"""
@spec register(Password.t(), map) :: {:ok, Resource.record()} | {:error, any}
def register(%Password{} = strategy, params) do
@spec register(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def register(%Password{} = strategy, params, options) do
api = Info.authentication_api!(strategy.resource)
strategy.resource
|> Changeset.new()
|> Changeset.set_context(%{strategy: strategy})
|> Changeset.for_create(strategy.register_action_name, params)
|> api.create()
|> api.create(options)
end
@doc """
Request a password reset.
"""
@spec reset_request(Password.t(), map) :: :ok | {:error, any}
@spec reset_request(Password.t(), map, keyword) :: :ok | {:error, any}
def reset_request(
%Password{resettable: [%Password.Resettable{} = resettable]} = strategy,
params
params,
options
) do
api = Info.authentication_api!(strategy.resource)
@ -56,14 +57,14 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|> Query.new()
|> Query.set_context(%{strategy: strategy})
|> Query.for_read(resettable.request_password_reset_action_name, params)
|> api.read()
|> api.read(options)
|> case do
{:ok, _} -> :ok
{:error, reason} -> {:error, reason}
end
end
def reset_request(%Password{} = strategy, _params),
def reset_request(%Password{} = strategy, _params, _options),
do:
{:error,
NoSuchAction.exception(resource: strategy.resource, action: :reset_request, type: :read)}
@ -71,8 +72,12 @@ defmodule AshAuthentication.Strategy.Password.Actions do
@doc """
Attempt to change a user's password using a reset token.
"""
@spec reset(Password.t(), map) :: {:ok, Resource.record()} | {:error, any}
def reset(%Password{resettable: [%Password.Resettable{} = resettable]} = strategy, params) do
@spec reset(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def reset(
%Password{resettable: [%Password.Resettable{} = resettable]} = strategy,
params,
options
) do
with {:ok, token} <- Map.fetch(params, "reset_token"),
{:ok, %{"sub" => subject}, resource} <- Jwt.verify(token, strategy.resource),
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource) do
@ -82,13 +87,13 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|> Changeset.new()
|> Changeset.set_context(%{strategy: strategy})
|> Changeset.for_update(resettable.password_reset_action_name, params)
|> api.update()
|> api.update(options)
else
{:error, %Changeset{} = changeset} -> {:error, changeset}
_ -> {:error, Errors.InvalidToken.exception(type: :reset)}
end
end
def reset(%Password{} = strategy, _params),
def reset(%Password{} = strategy, _params, _options),
do: {:error, NoSuchAction.exception(resource: strategy.resource, action: :reset, type: :read)}
end

View file

@ -62,12 +62,16 @@ defimpl AshAuthentication.Strategy, for: AshAuthentication.Strategy.Password do
@doc """
Perform actions.
"""
@spec action(Password.t(), phase, map) :: {:ok, Resource.record()} | {:error, any}
def action(strategy, :register, params), do: Password.Actions.register(strategy, params)
def action(strategy, :sign_in, params), do: Password.Actions.sign_in(strategy, params)
@spec action(Password.t(), phase, map, keyword) :: {:ok, Resource.record()} | {:error, any}
def action(strategy, :register, params, options),
do: Password.Actions.register(strategy, params, options)
def action(strategy, :reset_request, params),
do: Password.Actions.reset_request(strategy, params)
def action(strategy, :sign_in, params, options),
do: Password.Actions.sign_in(strategy, params, options)
def action(strategy, :reset, params), do: Password.Actions.reset(strategy, params)
def action(strategy, :reset_request, params, options),
do: Password.Actions.reset_request(strategy, params, options)
def action(strategy, :reset, params, options),
do: Password.Actions.reset(strategy, params, options)
end

View file

@ -109,7 +109,10 @@ defprotocol AshAuthentication.Strategy do
that the context is correctly set, etc.
See `actions/1` for a list of actions provided by the strategy.
Any options passed to the action will be passed to the underlying `Ash.Api` function.
"""
@spec action(t, action, params :: map) :: :ok | {:ok, Resource.record()} | {:error, any}
def action(strategy, action_name, params)
@spec action(t, action, params :: map, options :: keyword) ::
:ok | {:ok, Resource.record()} | {:error, any}
def action(strategy, action_name, params, options \\ [])
end

View file

@ -20,14 +20,14 @@ defmodule AshAuthentication.AddOn.Confirmation.ActionsTest do
Example.Repo.delete!(user)
assert {:error, error} = Actions.confirm(strategy, %{"confirm" => token})
assert {:error, error} = Actions.confirm(strategy, %{"confirm" => token}, [])
assert Exception.message(error) == "record not found"
end
test "it returns an error when the token is invalid" do
{:ok, strategy} = Info.strategy(Example.User, :confirm)
assert {:error, error} = Actions.confirm(strategy, %{"confirm" => Ecto.UUID.generate()})
assert {:error, error} = Actions.confirm(strategy, %{"confirm" => Ecto.UUID.generate()}, [])
assert Exception.message(error) == "Invalid confirmation token"
end
@ -42,7 +42,7 @@ defmodule AshAuthentication.AddOn.Confirmation.ActionsTest do
{:ok, token} = Confirmation.confirmation_token(strategy, changeset)
assert {:ok, confirmed_user} = Actions.confirm(strategy, %{"confirm" => token})
assert {:ok, confirmed_user} = Actions.confirm(strategy, %{"confirm" => token}, [])
assert confirmed_user.id == user.id
assert to_string(confirmed_user.username) == new_username

View file

@ -48,7 +48,7 @@ defmodule AshAuthentication.AddOn.Confirmation.StrategyTest do
params = %{"confirm" => Ecto.UUID.generate()}
Confirmation.Actions
|> expect(:confirm, fn rx_strategy, rx_params ->
|> expect(:confirm, fn rx_strategy, rx_params, _opts ->
assert rx_strategy == strategy
assert rx_params == params
end)

View file

@ -9,7 +9,7 @@ defmodule AshAuthentication.Strategy.OAuth2.ActionsTest do
{:ok, strategy} = Info.strategy(Example.User, :oauth2)
assert {:error, error} =
Actions.sign_in(strategy, %{"user_info" => %{}, "oauth_tokens" => %{}})
Actions.sign_in(strategy, %{"user_info" => %{}, "oauth_tokens" => %{}}, [])
assert Exception.message(error) =~ ~r/no such action :sign_in_with_oauth2/i
end
@ -20,18 +20,22 @@ defmodule AshAuthentication.Strategy.OAuth2.ActionsTest do
user = build_user()
assert {:ok, signed_in_user} =
Actions.sign_in(strategy, %{
"user_info" => %{
"nickname" => user.username,
"uid" => user.id,
"sub" => "user:#{user.id}"
Actions.sign_in(
strategy,
%{
"user_info" => %{
"nickname" => user.username,
"uid" => user.id,
"sub" => "user:#{user.id}"
},
"oauth_tokens" => %{
"access_token" => Ecto.UUID.generate(),
"expires_in" => 86_400,
"refresh_token" => Ecto.UUID.generate()
}
},
"oauth_tokens" => %{
"access_token" => Ecto.UUID.generate(),
"expires_in" => 86_400,
"refresh_token" => Ecto.UUID.generate()
}
})
[]
)
assert signed_in_user.id == user.id
assert {:ok, claims} = Jwt.peek(signed_in_user.__metadata__.token)
@ -43,18 +47,22 @@ defmodule AshAuthentication.Strategy.OAuth2.ActionsTest do
strategy = %{strategy | registration_enabled?: false}
assert {:error, error} =
Actions.sign_in(strategy, %{
"user_info" => %{
"nickname" => username(),
"uid" => Ecto.UUID.generate(),
"sub" => "user:#{Ecto.UUID.generate()}"
Actions.sign_in(
strategy,
%{
"user_info" => %{
"nickname" => username(),
"uid" => Ecto.UUID.generate(),
"sub" => "user:#{Ecto.UUID.generate()}"
},
"oauth_tokens" => %{
"access_token" => Ecto.UUID.generate(),
"expires_in" => 86_400,
"refresh_token" => Ecto.UUID.generate()
}
},
"oauth_tokens" => %{
"access_token" => Ecto.UUID.generate(),
"expires_in" => 86_400,
"refresh_token" => Ecto.UUID.generate()
}
})
[]
)
assert Exception.message(error) =~ ~r/authentication failed/i
end
@ -68,18 +76,22 @@ defmodule AshAuthentication.Strategy.OAuth2.ActionsTest do
id = Ecto.UUID.generate()
assert {:ok, user} =
Actions.register(strategy, %{
"user_info" => %{
"nickname" => username,
"uid" => id,
"sub" => "user:#{id}"
Actions.register(
strategy,
%{
"user_info" => %{
"nickname" => username,
"uid" => id,
"sub" => "user:#{id}"
},
"oauth_tokens" => %{
"access_token" => Ecto.UUID.generate(),
"expires_in" => 86_400,
"refresh_token" => Ecto.UUID.generate()
}
},
"oauth_tokens" => %{
"access_token" => Ecto.UUID.generate(),
"expires_in" => 86_400,
"refresh_token" => Ecto.UUID.generate()
}
})
[]
)
assert to_string(user.username) == username
assert {:ok, claims} = Jwt.peek(user.__metadata__.token)
@ -92,18 +104,22 @@ defmodule AshAuthentication.Strategy.OAuth2.ActionsTest do
user = build_user()
assert {:ok, signed_in_user} =
Actions.register(strategy, %{
"user_info" => %{
"nickname" => user.username,
"uid" => user.id,
"sub" => "user:#{user.id}"
Actions.register(
strategy,
%{
"user_info" => %{
"nickname" => user.username,
"uid" => user.id,
"sub" => "user:#{user.id}"
},
"oauth_tokens" => %{
"access_token" => Ecto.UUID.generate(),
"expires_in" => 86_400,
"refresh_token" => Ecto.UUID.generate()
}
},
"oauth_tokens" => %{
"access_token" => Ecto.UUID.generate(),
"expires_in" => 86_400,
"refresh_token" => Ecto.UUID.generate()
}
})
[]
)
assert signed_in_user.id == user.id
assert {:ok, claims} = Jwt.peek(signed_in_user.__metadata__.token)

View file

@ -81,7 +81,7 @@ defmodule AshAuthentication.Strategy.OAuth2.StrategyTest do
params = %{"user_info" => %{}, "oauth_tokens" => %{}}
OAuth2.Actions
|> expect(unquote(action), fn rx_strategy, rx_params ->
|> expect(unquote(action), fn rx_strategy, rx_params, _opts ->
assert rx_strategy == strategy
assert rx_params == params
end)

View file

@ -17,10 +17,14 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
{:ok, strategy} = Info.strategy(Example.User, :password)
assert {:ok, user} =
Actions.sign_in(strategy, %{
"username" => user.username,
"password" => user.__metadata__.password
})
Actions.sign_in(
strategy,
%{
"username" => user.username,
"password" => user.__metadata__.password
},
[]
)
assert {:ok, claims} = Jwt.peek(user.__metadata__.token)
assert claims["sub"] =~ "user?id=#{user.id}"
@ -31,14 +35,22 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
{:ok, strategy} = Info.strategy(Example.User, :password)
assert {:error, %AuthenticationFailed{}} =
Actions.sign_in(strategy, %{"username" => user.username, "password" => password()})
Actions.sign_in(
strategy,
%{"username" => user.username, "password" => password()},
[]
)
end
test "it returns an error when the username and password are incorrect" do
{:ok, strategy} = Info.strategy(Example.User, :password)
assert {:error, %AuthenticationFailed{}} =
Actions.sign_in(strategy, %{"username" => username(), "password" => password()})
Actions.sign_in(
strategy,
%{"username" => username(), "password" => password()},
[]
)
end
end
@ -50,11 +62,15 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
password = password()
assert {:ok, user} =
Actions.register(strategy, %{
"username" => username,
"password" => password,
"password_confirmation" => password
})
Actions.register(
strategy,
%{
"username" => username,
"password" => password,
"password_confirmation" => password
},
[]
)
assert strategy.hash_provider.valid?(password, user.hashed_password)
@ -69,11 +85,15 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
password = password()
assert {:error, error} =
Actions.register(strategy, %{
"username" => user.username,
"password" => password,
"password_confirmation" => password
})
Actions.register(
strategy,
%{
"username" => user.username,
"password" => password,
"password_confirmation" => password
},
[]
)
assert Exception.message(error) =~ ~r/username: has already been taken/
end
@ -82,11 +102,15 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
{:ok, strategy} = Info.strategy(Example.User, :password)
assert {:error, error} =
Actions.register(strategy, %{
"username" => username(),
"password" => password(),
"password_confirmation" => password()
})
Actions.register(
strategy,
%{
"username" => username(),
"password" => password(),
"password_confirmation" => password()
},
[]
)
assert Exception.message(error) =~ ~r/password_confirmation: does not match/
end
@ -99,7 +123,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
log =
capture_log(fn ->
assert :ok = Actions.reset_request(strategy, %{"username" => user.username()})
assert :ok = Actions.reset_request(strategy, %{"username" => user.username()}, [])
end)
assert log =~ ~r/password reset request for user #{user.username}/i
@ -110,7 +134,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
log =
capture_log(fn ->
assert :ok = Actions.reset_request(strategy, %{"username" => username()})
assert :ok = Actions.reset_request(strategy, %{"username" => username()}, [])
end)
refute log =~ ~r/password reset request for user/i
@ -120,7 +144,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
{:ok, strategy} = Info.strategy(Example.User, :password)
strategy = %{strategy | resettable: []}
assert {:error, error} = Actions.reset_request(strategy, %{"username" => username()})
assert {:error, error} = Actions.reset_request(strategy, %{"username" => username()}, [])
assert Exception.message(error) =~ ~r/no such action/i
end
end
@ -139,7 +163,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
"password_confirmation" => new_password
}
assert {:ok, updated_user} = Actions.reset(strategy, params)
assert {:ok, updated_user} = Actions.reset(strategy, params, [])
assert user.id == updated_user.id
assert user.hashed_password != updated_user.hashed_password

View file

@ -132,7 +132,7 @@ defmodule AshAuthentication.Strategy.Password.StrategyTest do
params = %{"username" => Faker.Internet.user_name()}
Password.Actions
|> expect(unquote(action), fn rx_strategy, rx_params ->
|> expect(unquote(action), fn rx_strategy, rx_params, _opts ->
assert rx_strategy == strategy
assert rx_params == params
end)