Packages
ash_authentication
5.0.0-rc.6
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/utils.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Utils do
@moduledoc false
alias Ash.{Domain, Resource}
alias Spark.{Dsl, Dsl.Transformer}
@doc """
Returns `true` if `falsy` is either `nil` or `false`.
"""
@spec is_falsy(any) :: Macro.t()
defguard is_falsy(falsy) when falsy in [nil, false]
@doc """
Returns `false` if `truthy` is either `nil` or `false`.
"""
@spec is_truthy(any) :: Macro.t()
defguard is_truthy(truthy) when truthy not in [nil, false]
@doc """
Convert a list of `String.Chars.t` into a sentence.
## Example
iex> ~w[Marty Doc Einstein] |> to_sentence()
"Marty, Doc and Einstein"
"""
@spec to_sentence(Enum.t(), [
{:separator, String.t()} | {:final, String.t()} | {:whitespace, boolean}
]) :: String.t()
def to_sentence(elements, opts \\ []) do
opts =
[separator: ",", final: "and", whitespace: true]
|> Keyword.merge(opts)
|> Map.new()
elements
|> Enum.to_list()
|> case do
[] ->
""
[element] ->
to_string(element)
[_ | _] = elements ->
elements
|> Enum.reverse()
|> convert_to_sentence("", opts.separator, opts.final, opts.whitespace)
end
end
defp convert_to_sentence([last], result, _, final, true), do: "#{result} #{final} #{last}"
defp convert_to_sentence([last], result, _, final, false), do: "#{result}#{final}#{last}"
defp convert_to_sentence([next | rest], "", sep, final, ws),
do: convert_to_sentence(rest, to_string(next), sep, final, ws)
defp convert_to_sentence([next | rest], result, sep, final, true),
do: convert_to_sentence(rest, "#{result}#{sep} #{next}", sep, final, true)
defp convert_to_sentence([next | rest], result, sep, final, false),
do: convert_to_sentence(rest, "#{result}#{sep}#{next}", sep, final, false)
@doc """
Optionally append an element to a collection.
When `test` is truthy, append `element` to the collection.
"""
@spec maybe_append(Enum.t(), test :: any, element :: any) :: Enum.t()
def maybe_append(collection, test, _element) when is_falsy(test), do: collection
def maybe_append(collection, _test, element), do: Enum.concat(collection, [element])
@doc """
Optionally concat a collection to another collection.
When `test` is truthy, concat the collections together.
"""
@spec maybe_concat(Enum.t(), test :: any, Enum.t()) :: Enum.t()
def maybe_concat(collection, test, _new_elements) when is_falsy(test), do: collection
def maybe_concat(collection, _test, new_elements), do: Enum.concat(collection, new_elements)
@doc """
Used within transformers to infer `domain` from a resource if the option is not set.
"""
def maybe_set_domain(dsl_state, section) do
domain = Transformer.get_persisted(dsl_state, :domain)
if domain && !Transformer.get_option(dsl_state, [section], :domain) do
{:ok, Transformer.set_option(dsl_state, [section], :domain, domain)}
else
{:ok, dsl_state}
end
end
@doc """
Used within transformers to optionally build actions as needed.
"""
@spec maybe_build_action(Dsl.t(), atom, (map -> map)) :: {:ok, atom | map} | {:error, any}
def maybe_build_action(dsl_state, action_name, builder) when is_function(builder, 1) do
with nil <- Resource.Info.action(dsl_state, action_name),
{:ok, action} <- builder.(dsl_state) do
{:ok, Transformer.add_entity(dsl_state, [:actions], action)}
else
action when is_map(action) -> {:ok, dsl_state}
{:error, reason} -> {:error, reason}
end
end
@doc """
Used within transformers to optionally build attributes as needed.
"""
@spec maybe_build_attribute(Dsl.t(), atom, atom | module, keyword) :: {:ok, Dsl.t()}
def maybe_build_attribute(dsl_state, name, type, options) do
if Resource.Info.attribute(dsl_state, name) do
{:ok, dsl_state}
else
options =
options
|> Keyword.put(:name, name)
|> Keyword.put(:type, type)
attribute = Transformer.build_entity!(Resource.Dsl, [:attributes], :attribute, options)
{:ok, Transformer.add_entity(dsl_state, [:attributes], attribute)}
end
end
@doc """
Used within transformers to optionally build relationships as needed.
"""
@spec maybe_build_relationship(
Dsl.t(),
relationship_name :: atom,
(Dsl.t() -> {:ok, Resource.Relationships.relationship()})
) :: {:ok, Dsl.t()} | {:error, Exception.t()}
def maybe_build_relationship(dsl_state, relationship_name, builder)
when is_function(builder, 1) do
with :error <- find_relationship(dsl_state, relationship_name),
{:ok, relationship} <- builder.(dsl_state) do
{:ok, Transformer.add_entity(dsl_state, [:relationships], relationship)}
else
{:ok, _relationship} -> {:ok, dsl_state}
{:error, reason} -> {:error, reason}
end
end
@doc """
Find a relationship from a resource.
"""
@spec find_relationship(Dsl.t(), relationship_name :: atom) ::
{:ok, Resource.Relationships.relationship()} | :error
def find_relationship(dsl_state, relationship_name) do
dsl_state
|> Resource.Info.relationships()
|> Enum.find(&(&1.name == relationship_name))
|> case do
nil -> :error
relationship -> {:ok, relationship}
end
end
@doc """
Optionally set a field in a map.
Like `Map.put_new/3` except that it overwrites fields if their contents are
falsy.
"""
@spec maybe_set_field(map, any, any) :: map
def maybe_set_field(map, field, value) when is_falsy(:erlang.map_get(field, map)),
do: Map.put(map, field, value)
def maybe_set_field(map, _field, _value), do: map
@doc """
Like `maybe_set_field/3` except that the value is lazily generated.
"""
@spec maybe_set_field_lazy(input, any, (input -> value)) :: map when input: map, value: any
def maybe_set_field_lazy(map, field, generator)
when is_falsy(:erlang.map_get(field, map)) and is_function(generator, 1),
do: Map.put(map, field, generator.(map))
def maybe_set_field_lazy(map, _field, _generator), do: map
@doc """
Asserts that `resource` is an Ash resource and `extension` is a Spark DSL
extension.
"""
@spec assert_resource_has_extension(Resource.t(), Spark.Dsl.Extension.t()) ::
:ok | {:error, term}
def assert_resource_has_extension(resource, extension) do
with :ok <- assert_is_resource(resource) do
assert_has_extension(resource, extension)
end
end
@doc """
Asserts that `module` is actually an Ash resource.
"""
@spec assert_is_resource(Resource.t()) :: :ok | {:error, term}
def assert_is_resource(module) do
with :ok <- assert_is_module(module),
true <- function_exported?(module, :spark_is, 0),
Resource <- module.spark_is() do
:ok
else
_ ->
{:error, "Module `#{inspect(module)}` is not an Ash resource"}
end
end
@doc """
Asserts that `module` is actually an Ash domain.
"""
@spec assert_is_domain(Domain.t()) :: :ok | {:error, term}
def assert_is_domain(module) do
with :ok <- assert_is_module(module),
true <- function_exported?(module, :spark_is, 0),
Domain <- module.spark_is() do
:ok
else
_ -> {:error, "Module `#{inspect(module)}` is not an Ash domain"}
end
end
@doc """
Asserts that `module` is a Spark DSL extension.
"""
@spec assert_is_extension(Spark.Dsl.Extension.t()) :: :ok | {:error, term}
def assert_is_extension(extension) do
with :ok <- assert_is_module(extension) do
assert_has_behaviour(extension, Spark.Dsl.Extension)
end
end
@doc """
Asserts that `module` is actually a module.
"""
@spec assert_is_module(module) :: :ok | {:error, term}
def assert_is_module(module) when is_atom(module) do
case Code.ensure_compiled(module) do
{:module, _} -> :ok
_ -> {:error, "Argument `#{inspect(module)}` is not a valid module"}
end
end
def assert_is_module(module),
do: {:error, "Argument `#{inspect(module)}` is not a valid module"}
@doc """
Asserts that `module` is extended by `extension`.
"""
@spec assert_has_extension(Resource.t(), Spark.Dsl.Extension.t()) :: :ok | {:error, term}
def assert_has_extension(module, extension) do
if extension in Spark.extensions(module) do
:ok
else
{:error, "Module `#{inspect(module)}` is not extended by `#{inspect(extension)}`"}
end
end
@doc """
Asserts that `module` implements `behaviour`.
"""
@spec assert_has_behaviour(module, module) :: :ok | {:error, term}
def assert_has_behaviour(module, behaviour) do
if Spark.implements_behaviour?(module, behaviour) do
:ok
else
{:error,
"Module `#{inspect(module)}` does not implement the `#{inspect(behaviour)}` behaviour"}
end
end
@doc """
Convert a lifetime to seconds.
"""
@spec lifetime_to_seconds(
lifetime ::
integer
| {integer, :seconds}
| {integer, :minutes}
| {integer, :hours}
| {integer, :days}
) :: integer
def lifetime_to_seconds(seconds) when is_integer(seconds), do: seconds
def lifetime_to_seconds({seconds, :seconds}), do: seconds
def lifetime_to_seconds({minutes, :minutes}), do: minutes * 60
def lifetime_to_seconds({hours, :hours}), do: hours * 60 * 60
def lifetime_to_seconds({days, :days}), do: days * 60 * 60 * 24
@doc """
Normalize claim tenant :null atom to nil.
> To support JOSE 1.11.11+ and Erlang's built-in JSON module, it sends :null instead of nil.
"""
@spec normalize_null(term()) :: term()
def normalize_null(:null), do: nil
def normalize_null(map) when is_map(map),
do: Map.new(map, fn {k, v} -> {k, normalize_null(v)} end)
def normalize_null(value), do: value
@doc """
Apply `Ash.Query.lock/2` only when the resource's data layer supports the
given lock type. On data layers that don't support locking (e.g. ETS),
returns the query unchanged.
This is intentionally a soft fallback: data layers without locking are
typically single-process and serialise writes by other means, so the
atomicity that the lock provides on SQL-backed data layers is already
guaranteed there. Resources that genuinely need lock-based safety should
surface a compile-time warning via the relevant verifier.
"""
@spec maybe_lock(Ash.Query.t(), Ash.DataLayer.lock_type()) :: Ash.Query.t()
def maybe_lock(query, lock_type) do
if Ash.DataLayer.data_layer_can?(query.resource, {:lock, lock_type}) do
Ash.Query.lock(query, lock_type)
else
query
end
end
@doc """
Add a `:lock` keyword option to `opts` only when the resource's data
layer supports the given lock type.
Counterpart to `maybe_lock/2` for code paths that pass options directly
to `Ash.get/3` and friends rather than building a query.
"""
@spec maybe_lock_opt(keyword, Ash.Resource.t(), Ash.DataLayer.lock_type()) :: keyword
def maybe_lock_opt(opts, resource, lock_type) do
if Ash.DataLayer.data_layer_can?(resource, {:lock, lock_type}) do
Keyword.put(opts, :lock, lock_type)
else
opts
end
end
end