Packages
ash_authentication
5.0.0-rc.2
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/igniter.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
# credo:disable-for-this-file Credo.Check.Design.AliasUsage
if Code.ensure_loaded?(Igniter) do
defmodule AshAuthentication.Igniter do
@moduledoc "Codemods for working with AshAuthentication"
@doc "Adds a secret to a secret module that reads from application env, if one for that module/path doesn't exist already."
@spec add_new_secret_from_env(Igniter.t(), module(), Ash.Resource.t(), list(atom), atom()) ::
Igniter.t()
def add_new_secret_from_env(igniter, module, resource, path, env_key) do
otp_app = Igniter.Project.Application.app_name(igniter)
func =
quote do
def secret_for(unquote(path), unquote(resource), _opts, _context),
do: Application.fetch_env(unquote(otp_app), unquote(env_key))
end
full =
quote do
use AshAuthentication.Secret
unquote(func)
end
|> Sourceror.to_string()
Igniter.Project.Module.find_and_update_or_create_module(igniter, module, full, fn zipper ->
with {:ok, zipper} <-
Igniter.Code.Function.move_to_def(zipper, :secret_for, 4, target: :at),
zipper when not is_nil(zipper) <- Sourceror.Zipper.down(zipper),
zipper when not is_nil(zipper) <- Sourceror.Zipper.down(zipper),
true <- Igniter.Code.Common.nodes_equal?(zipper, path),
zipper when not is_nil(zipper) <- Sourceror.Zipper.right(zipper),
true <- Igniter.Code.Common.nodes_equal?(zipper, resource) do
{:ok, zipper}
else
_ ->
{:ok, Igniter.Code.Common.add_code(zipper, func)}
end
end)
end
@doc "Adds a secret to a secret module that reads from application env"
@spec add_secret_from_env(Igniter.t(), module(), Ash.Resource.t(), list(atom), atom()) ::
Igniter.t()
def add_secret_from_env(igniter, module, resource, path, env_key) do
otp_app = Igniter.Project.Application.app_name(igniter)
func =
quote do
def secret_for(unquote(path), unquote(resource), _opts, _context),
do: Application.fetch_env(unquote(otp_app), unquote(env_key))
end
full =
quote do
use AshAuthentication.Secret
unquote(func)
end
|> Sourceror.to_string()
Igniter.Project.Module.find_and_update_or_create_module(igniter, module, full, fn zipper ->
{:ok, Igniter.Code.Common.add_code(zipper, func)}
end)
end
@doc "Adds a new add_on to the authentication.strategies section of a resource"
@spec add_new_add_on(
Igniter.t(),
Ash.Resource.t(),
type :: atom,
name :: atom | nil,
contents :: String.t()
) :: Igniter.t()
def add_new_add_on(igniter, resource, type, name, contents) do
{igniter, defines?} = defines_add_on(igniter, resource, type, name)
if defines? do
igniter
else
add_add_on(igniter, resource, contents)
end
end
@doc "Adds an add on to the authentication.add_ons section of a resource"
@spec add_add_on(
Igniter.t(),
Ash.Resource.t(),
contents :: String.t()
) :: Igniter.t()
def add_add_on(igniter, resource, contents) do
Igniter.Project.Module.find_and_update_module!(igniter, resource, fn zipper ->
with {:authentication, {:ok, zipper}} <-
{:authentication, enter_section(zipper, :authentication)},
{:add_ons, _authentication_zipper, {:ok, zipper}} <-
{:add_ons, zipper, enter_section(zipper, :add_ons)} do
{:ok, Igniter.Code.Common.add_code(zipper, contents)}
else
{:authentication, :error} ->
{:ok,
Igniter.Code.Common.add_code(zipper, """
authentication do
add_ons do
#{contents}
end
end
""")}
{:add_ons, zipper, :error} ->
{:ok,
Igniter.Code.Common.add_code(zipper, """
add_ons do
#{contents}
end
""")}
end
end)
end
@doc "Returns true if the given resource defines an authentication add on with the provided name"
@spec defines_add_on(Igniter.t(), Ash.Resource.t(), constructor :: atom(), name :: atom()) ::
{Igniter.t(), true | false}
def defines_add_on(igniter, resource, constructor, name) do
Spark.Igniter.find(igniter, resource, fn _, zipper ->
with {:ok, zipper} <- enter_section(zipper, :authentication),
{:ok, zipper} <- enter_section(zipper, :add_ons),
{:ok, _zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
constructor,
[1, 2],
&add_on_matches?(&1, name)
) do
{:ok, true}
else
_ ->
:error
end
end)
|> case do
{:ok, igniter, _module, _value} ->
{igniter, true}
{:error, igniter} ->
{igniter, false}
end
end
defp add_on_matches?(zipper, nil) do
Igniter.Code.Function.move_to_nth_argument(zipper, 1) == :error
end
defp add_on_matches?(zipper, name) do
Igniter.Code.Function.argument_equals?(zipper, 0, name)
end
@doc "Adds a new strategy to the authentication.strategies section of a resource"
@spec add_new_strategy(
Igniter.t(),
Ash.Resource.t(),
type :: atom,
name :: atom,
contents :: String.t()
) :: Igniter.t()
def add_new_strategy(igniter, resource, type, name, contents) do
{igniter, defines?} = defines_strategy(igniter, resource, type, name)
if defines? do
igniter
else
add_strategy(igniter, resource, contents)
end
end
@doc "Adds a strategy to the authentication.strategies section of a resource"
@spec add_strategy(
Igniter.t(),
Ash.Resource.t(),
contents :: String.t()
) :: Igniter.t()
def add_strategy(igniter, resource, contents) do
Igniter.Project.Module.find_and_update_module!(igniter, resource, fn zipper ->
with {:authentication, {:ok, zipper}} <-
{:authentication, enter_section(zipper, :authentication)},
{:strategies, _authentication_zipper, {:ok, zipper}} <-
{:strategies, zipper, enter_section(zipper, :strategies)} do
{:ok, Igniter.Code.Common.add_code(zipper, contents)}
else
{:authentication, :error} ->
{:ok,
Igniter.Code.Common.add_code(zipper, """
authentication do
strategies do
#{contents}
end
end
""")}
{:strategies, zipper, :error} ->
{:ok,
Igniter.Code.Common.add_code(zipper, """
strategies do
#{contents}
end
""")}
end
end)
end
@doc "Returns true if the given resource defines an authentication strategy with the provided name"
@spec defines_strategy(Igniter.t(), Ash.Resource.t(), constructor :: atom(), name :: atom()) ::
{Igniter.t(), true | false}
def defines_strategy(igniter, resource, constructor, name) do
Spark.Igniter.find(igniter, resource, fn _, zipper ->
with {:ok, zipper} <- enter_section(zipper, :authentication),
{:ok, zipper} <- enter_section(zipper, :strategies),
{:ok, _zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
constructor,
[1, 2],
&Igniter.Code.Function.argument_equals?(&1, 0, name)
) do
{:ok, true}
else
_ ->
:error
end
end)
|> case do
{:ok, igniter, _module, _value} ->
{igniter, true}
{:error, igniter} ->
{igniter, false}
end
end
@doc "Returns true if the given resource defines an authentication strategy of the provided type"
@spec defines_strategy_of_type(
Igniter.t(),
Ash.Resource.t(),
constructor :: atom()
) ::
{Igniter.t(), true | false}
def defines_strategy_of_type(igniter, resource, constructor) do
Spark.Igniter.find(igniter, resource, fn _, zipper ->
with {:ok, zipper} <- enter_section(zipper, :authentication),
{:ok, zipper} <- enter_section(zipper, :strategies),
{:ok, _zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
constructor,
[1, 2]
) do
{:ok, true}
else
_ ->
:error
end
end)
|> case do
{:ok, igniter, _module, _value} ->
{igniter, true}
{:error, igniter} ->
{igniter, false}
end
end
@doc """
Ensures a unique identity exists for the given field on a resource.
Adds an identity named `:unique_<field>` on `[:<field>]` if one doesn't already exist.
"""
@spec ensure_identity(Igniter.t(), Ash.Resource.t(), atom()) :: Igniter.t()
# sobelow_skip ["DOS.BinToAtom"]
def ensure_identity(igniter, resource, identity_field) do
Ash.Resource.Igniter.add_new_identity(
igniter,
resource,
:"unique_#{identity_field}",
"""
identity :unique_#{identity_field}, [:#{identity_field}]
"""
)
end
@doc """
Ensures a `get_by_<field>` read action exists on the resource.
"""
@spec ensure_get_by_action(Igniter.t(), Ash.Resource.t(), atom()) :: Igniter.t()
# sobelow_skip ["DOS.BinToAtom"]
def ensure_get_by_action(igniter, resource, identity_field) do
Ash.Resource.Igniter.add_new_action(
igniter,
resource,
:"get_by_#{identity_field}",
"""
read :get_by_#{identity_field} do
description "Looks up a user by their #{identity_field}"
get_by :#{identity_field}
end
"""
)
end
@doc """
Adds the `remember_me` strategy to a resource if it doesn't already exist.
"""
@spec add_remember_me_strategy(Igniter.t(), Ash.Resource.t()) :: Igniter.t()
def add_remember_me_strategy(igniter, resource) do
add_new_strategy(
igniter,
resource,
:remember_me,
:remember_me,
"""
remember_me :remember_me
"""
)
end
@doc """
Returns the parent module of a given module.
Useful for deriving a domain module from a resource module.
## Example
iex> AshAuthentication.Igniter.parent_module(MyApp.Accounts.User)
MyApp.Accounts
"""
@spec parent_module(module()) :: module()
def parent_module(module) do
module |> Module.split() |> :lists.droplast() |> Module.concat()
end
@doc """
Parses a module from a string, or returns an atom as-is.
"""
@spec maybe_parse_module(atom() | String.t()) :: module()
def maybe_parse_module(atom) when is_atom(atom), do: atom
def maybe_parse_module(string) when is_binary(string),
do: Igniter.Project.Module.parse(string)
defp enter_section(zipper, name) do
with {:ok, zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
name,
1
) do
Igniter.Code.Common.move_to_do_block(zipper)
end
end
end
end