mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-19 12:52:55 +12:00
improvement(Strategy.Custom): handle custom strategies as extensions. (#183)
This means that users can add their own extensions to their resources which patch the strategy (and add ons) DSLs.
This commit is contained in:
parent
e008c7a58e
commit
3bece5f657
25 changed files with 356 additions and 690 deletions
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"defimpl",
|
||||
"defstruct",
|
||||
"ilike",
|
||||
"Marties",
|
||||
"moduledocs"
|
||||
]
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
# Defining Custom Authentication Strategies
|
||||
|
||||
AshAuthentication allows you to bring your own authentication strategy without
|
||||
having to change the Ash Authenticaiton codebase.
|
||||
having to change the Ash Authentication codebase.
|
||||
|
||||
> There is functionally no difference between "add ons" and "strategies" other
|
||||
> than where they appear in the DSL. We invented "add ons" because it felt
|
||||
|
@ -20,8 +20,6 @@ There are several moving parts which must all work together so hold on to your h
|
|||
4. The `AshAuthentication.Strategy` protocol, which provides the glue needed
|
||||
for everything to wire up and wrappers around the actions needed to run on
|
||||
the resource.
|
||||
5. Runtime configuration of `AshAuthentication` to help it find the extra
|
||||
strategies.
|
||||
|
||||
We're going to define an extremely dumb strategy which lets anyone with a name
|
||||
that starts with "Marty" sign in with just their name. Of course you would
|
||||
|
@ -39,52 +37,50 @@ end
|
|||
```
|
||||
|
||||
Sadly, this isn't enough to make the magic happen. We need to define our DSL
|
||||
entity by implementing the `dsl/0` callback:
|
||||
entity by adding it to the `use` statement:
|
||||
|
||||
```elixir
|
||||
defmodule OnlyMartiesAtTheParty do
|
||||
use AshAuthentication.Strategy.Custom
|
||||
|
||||
def dsl do
|
||||
%Spark.Dsl.Entity{
|
||||
name: :only_marty,
|
||||
describe: "Strategy which only allows folks whose name starts with \"Marty\" to sign in.",
|
||||
examples: [
|
||||
"""
|
||||
only_marty do
|
||||
case_sensitive? true
|
||||
name_field :name
|
||||
end
|
||||
"""
|
||||
@entity %Spark.Dsl.Entity{
|
||||
name: :only_marty,
|
||||
describe: "Strategy which only allows folks whose name starts with \"Marty\" to sign in.",
|
||||
examples: [
|
||||
"""
|
||||
only_marty do
|
||||
case_sensitive? true
|
||||
name_field :name
|
||||
end
|
||||
"""
|
||||
],
|
||||
target: __MODULE__,
|
||||
args: [{:optional, :name, :marty}],
|
||||
schema: [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The strategy name.
|
||||
""",
|
||||
required: true
|
||||
],
|
||||
target: __MODULE__,
|
||||
args: [{:optional, :name, :marty}],
|
||||
schema: [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The strategy name.
|
||||
""",
|
||||
required: true
|
||||
],
|
||||
case_sensitive?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Ignore letter case when comparing?
|
||||
""",
|
||||
required: false,
|
||||
default: false
|
||||
],
|
||||
name_field: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The field to check for the users' name.
|
||||
""",
|
||||
required: true
|
||||
]
|
||||
case_sensitive?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Ignore letter case when comparing?
|
||||
""",
|
||||
required: false,
|
||||
default: false
|
||||
],
|
||||
name_field: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The field to check for the users' name.
|
||||
""",
|
||||
required: true
|
||||
]
|
||||
}
|
||||
end
|
||||
]
|
||||
}
|
||||
|
||||
use AshAuthentication.Strategy.Custom, entity: @entity
|
||||
end
|
||||
```
|
||||
|
||||
|
@ -103,6 +99,10 @@ here's a brief overview of what each field we've set does:
|
|||
provides a number of additional types over the default ones though, so check
|
||||
out `Spark.OptionsHelpers` for more information.
|
||||
|
||||
> By default the entity is added to the `authentication / strategy` DSL, however
|
||||
> if you want it in the `authentication / add_ons` DSL instead you can also pass
|
||||
> `style: :add_on` in the `use` statement.
|
||||
|
||||
Next up, we need to define our struct. The struct should have *at least* the
|
||||
fields named in the entity schema. Additionally, Ash Authentication requires
|
||||
that it have a `resource` field which will be set to the module of the resource
|
||||
|
@ -112,22 +112,20 @@ it's attached to during compilation.
|
|||
defmodule OnlyMartiesAtTheParty do
|
||||
defstruct name: :marty, case_sensitive?: false, name_field: nil, resource: nil
|
||||
|
||||
use AshAuthentication.Strategy.Custom
|
||||
# ...
|
||||
|
||||
use AshAuthentication.Strategy.Custom, entity: @entity
|
||||
|
||||
# other code elided ...
|
||||
end
|
||||
```
|
||||
|
||||
Now it would be theoretically possible to add this custom strategies to your app
|
||||
by adding it to the runtime configuration and the user resource:
|
||||
by adding it to the `extensions` section of your resource:
|
||||
|
||||
```elixir
|
||||
# config.exs
|
||||
config :ash_authentication, extra_strategies: [OnlyMartiesAtTheParty]
|
||||
|
||||
# user resource
|
||||
defmodule MyApp.Accounts.User do
|
||||
use Ash.Resource, extensions: [AshAuthentication]
|
||||
use Ash.Resource, extensions: [AshAuthentication, OnlyMartiesAtTheParty]
|
||||
|
||||
authentication do
|
||||
api MyApp.Accounts
|
||||
|
@ -169,7 +167,8 @@ concepts:
|
|||
will generate routes using `Plug.Router` (or `Phoenix.Router`) - the
|
||||
`routes/1` callback is used to retrieve this information from the strategy.
|
||||
|
||||
Given this information, let's implment the strategy. It's quite long, so I'm going to break it up into smaller chunks.
|
||||
Given this information, let's implement the strategy. It's quite long, so I'm
|
||||
going to break it up into smaller chunks.
|
||||
|
||||
```elixir
|
||||
defimpl AshAuthentication.Strategy, for: OnlyMartiesAtTheParty do
|
||||
|
@ -191,12 +190,12 @@ and action.
|
|||
```
|
||||
|
||||
Next we generate the routes for the strategy. Routes *should* contain the
|
||||
subject name of the resource being authenticated in case the implementor is
|
||||
subject name of the resource being authenticated in case the implementer is
|
||||
authenticating multiple different resources - eg `User` and `Admin`.
|
||||
|
||||
```elixir
|
||||
def routes(strategy) do
|
||||
subject_name = Info.authentication_subject_name!(strategy.resource)
|
||||
subject_name = AshAuthentication.Info.authentication_subject_name!(strategy.resource)
|
||||
|
||||
[
|
||||
{"/#{subject_name}/#{strategy.name}", :sign_in}
|
||||
|
@ -307,9 +306,9 @@ end
|
|||
```
|
||||
|
||||
In some cases you may want to modify the strategy and the resources DSL. In
|
||||
this case you can return the newly muted DSL state in an ok tuple or an error
|
||||
tuple, preferably containing a `Spark.Error.DslError`. For example if we
|
||||
wanted to build a sign in action for `OnlyMartiesAtTheParty` to use:
|
||||
this case you can return the newly mutated DSL state in an ok tuple or an error
|
||||
tuple, preferably containing a `Spark.Error.DslError`. For example if we wanted
|
||||
to build a sign in action for `OnlyMartiesAtTheParty` to use:
|
||||
|
||||
```elixir
|
||||
def transform(strategy, dsl_state) do
|
||||
|
@ -341,7 +340,7 @@ to the resource. See the docs for `Spark.Dsl.Transformer` for more information.
|
|||
|
||||
We also support a variant of transformers which run in the new `@after_verify`
|
||||
compile hook provided by Elixir 1.14. This is a great place to put checks
|
||||
to make sure that the user's configuration make sense without adding any
|
||||
to make sure that the user's configuration makes sense without adding any
|
||||
compile-time dependencies between modules which may cause compiler deadlocks.
|
||||
|
||||
For example, verifying that the "name" attribute contains "marty" (why you would
|
||||
|
@ -355,7 +354,7 @@ def verify(strategy, _dsl_state) do
|
|||
{:error,
|
||||
Spark.Error.DslError.exception(
|
||||
path: [:authentication, :strategies, :only_marties],
|
||||
message: "Option `name_field` must contain the \"marty\""
|
||||
message: "Option `name_field` must contain \"marty\""
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -96,12 +96,28 @@ defmodule AshAuthentication do
|
|||
|
||||
<!--- ash-hq-hide-stop --> <!--- -->
|
||||
"""
|
||||
alias Ash.{Api, Error.Query.NotFound, Query, Resource}
|
||||
alias AshAuthentication.Info
|
||||
alias Ash.{
|
||||
Api,
|
||||
Error.Query.NotFound,
|
||||
Query,
|
||||
Resource
|
||||
}
|
||||
|
||||
alias AshAuthentication.{
|
||||
AddOn.Confirmation,
|
||||
Info,
|
||||
Strategy.Auth0,
|
||||
Strategy.Github,
|
||||
Strategy.OAuth2,
|
||||
Strategy.Password
|
||||
}
|
||||
|
||||
alias Spark.Dsl.Extension
|
||||
|
||||
use Spark.Dsl.Extension,
|
||||
sections: dsl(),
|
||||
dsl_patches:
|
||||
Enum.flat_map([Confirmation, Auth0, Github, OAuth2, Password], & &1.dsl_patches()),
|
||||
transformers: [
|
||||
AshAuthentication.Transformer,
|
||||
AshAuthentication.Strategy.Custom.Transformer
|
||||
|
|
|
@ -98,7 +98,9 @@ defmodule AshAuthentication.AddOn.Confirmation do
|
|||
name: :confirm
|
||||
|
||||
alias Ash.{Changeset, Resource}
|
||||
alias AshAuthentication.{AddOn.Confirmation, Jwt}
|
||||
alias AshAuthentication.{AddOn.Confirmation, Jwt, Strategy.Custom}
|
||||
|
||||
use Custom, style: :add_on, entity: Dsl.dsl()
|
||||
|
||||
@type t :: %Confirmation{
|
||||
token_lifetime: hours :: pos_integer,
|
||||
|
@ -114,7 +116,6 @@ defmodule AshAuthentication.AddOn.Confirmation do
|
|||
name: :confirm
|
||||
}
|
||||
|
||||
defdelegate dsl(), to: Dsl
|
||||
defdelegate transform(strategy, dsl_state), to: Transformer
|
||||
defdelegate verify(strategy, dsl_state), to: Verifier
|
||||
|
||||
|
|
|
@ -3,7 +3,12 @@ defmodule AshAuthentication.AddOn.Confirmation.Dsl do
|
|||
Defines the Spark DSL entity for this add on.
|
||||
"""
|
||||
|
||||
alias AshAuthentication.AddOn.Confirmation
|
||||
alias AshAuthentication.{
|
||||
AddOn.Confirmation,
|
||||
Sender,
|
||||
SenderFunction
|
||||
}
|
||||
|
||||
alias Spark.Dsl.Entity
|
||||
|
||||
@default_confirmation_lifetime_days 3
|
||||
|
@ -85,9 +90,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Dsl do
|
|||
default: true
|
||||
],
|
||||
sender: [
|
||||
type:
|
||||
{:spark_function_behaviour, AshAuthentication.Sender,
|
||||
{AshAuthentication.SenderFunction, 3}},
|
||||
type: {:spark_function_behaviour, Sender, {SenderFunction, 3}},
|
||||
doc: """
|
||||
How to send the confirmation instructions to the user.
|
||||
Allows you to glue sending of confirmation instructions to
|
||||
|
|
|
@ -10,14 +10,6 @@ defmodule AshAuthentication.Dsl do
|
|||
|
||||
alias Ash.{Api, Resource}
|
||||
|
||||
alias AshAuthentication.{
|
||||
AddOn.Confirmation,
|
||||
Strategy.Auth0,
|
||||
Strategy.Github,
|
||||
Strategy.OAuth2,
|
||||
Strategy.Password
|
||||
}
|
||||
|
||||
@default_token_lifetime_days 14
|
||||
|
||||
alias Spark.Dsl.Section
|
||||
|
@ -175,37 +167,17 @@ defmodule AshAuthentication.Dsl do
|
|||
%Section{
|
||||
name: :strategies,
|
||||
describe: "Configure authentication strategies on this resource",
|
||||
entities: Enum.map(available_strategies(), & &1.dsl())
|
||||
entities: [],
|
||||
patchable?: true
|
||||
},
|
||||
%Section{
|
||||
name: :add_ons,
|
||||
describe: "Additional add-ons related to, but not providing authentication",
|
||||
entities: Enum.map(available_add_ons(), & &1.dsl())
|
||||
entities: [],
|
||||
patchable?: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return the available strategy modules.
|
||||
|
||||
This is used for DSL generation and transformation.
|
||||
"""
|
||||
@spec available_strategies :: [module]
|
||||
def available_strategies do
|
||||
[Auth0, Github, OAuth2, Password]
|
||||
|> Enum.concat(Application.get_env(:ash_authentication, :extra_strategies, []))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return the available add-on modules.
|
||||
|
||||
This is used for DSL generation and transformation.
|
||||
"""
|
||||
@spec available_add_ons :: [module]
|
||||
def available_add_ons do
|
||||
[Confirmation]
|
||||
|> Enum.concat(Application.get_env(:ash_authentication, :extra_add_ons, []))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule AshAuthentication.Info do
|
|||
Generated configuration functions based on a resource's DSL configuration.
|
||||
"""
|
||||
|
||||
use AshAuthentication.InfoGenerator,
|
||||
use Spark.InfoGenerator,
|
||||
extension: AshAuthentication,
|
||||
sections: [:authentication]
|
||||
|
||||
|
|
|
@ -1,297 +0,0 @@
|
|||
defmodule AshAuthentication.InfoGenerator do
|
||||
@moduledoc """
|
||||
Used to dynamically generate configuration functions for Spark extensions
|
||||
based on their DSL.
|
||||
|
||||
## Usage
|
||||
|
||||
```elixir
|
||||
defmodule MyConfig do
|
||||
use AshAuthentication.InfoGenerator, extension: MyDslExtension, sections: [:my_section]
|
||||
end
|
||||
```
|
||||
"""
|
||||
|
||||
@type options :: [{:extension, module} | {:sections, [atom]}]
|
||||
|
||||
@doc false
|
||||
@spec __using__(options) :: Macro.t()
|
||||
defmacro __using__(opts) do
|
||||
extension = Keyword.fetch!(opts, :extension) |> Macro.expand(__CALLER__)
|
||||
sections = Keyword.get(opts, :sections, [])
|
||||
|
||||
quote do
|
||||
require AshAuthentication.InfoGenerator
|
||||
require unquote(extension)
|
||||
|
||||
AshAuthentication.InfoGenerator.generate_config_functions(
|
||||
unquote(extension),
|
||||
unquote(sections)
|
||||
)
|
||||
|
||||
AshAuthentication.InfoGenerator.generate_options_functions(
|
||||
unquote(extension),
|
||||
unquote(sections)
|
||||
)
|
||||
|
||||
AshAuthentication.InfoGenerator.generate_entity_functions(
|
||||
unquote(extension),
|
||||
unquote(sections)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Given an extension and a list of DSL sections, generate an options function
|
||||
which returns a map of all configured options for a resource (including
|
||||
defaults).
|
||||
"""
|
||||
@spec generate_options_functions(module, [atom]) :: Macro.t()
|
||||
defmacro generate_options_functions(extension, sections) do
|
||||
for {path, options} <- extension_sections_to_option_list(extension, sections) do
|
||||
function_name = :"#{Enum.join(path, "_")}_options"
|
||||
|
||||
quote location: :keep do
|
||||
@doc """
|
||||
#{unquote(Enum.join(path, "."))} DSL options
|
||||
|
||||
Returns a map containing the and any configured or default values.
|
||||
"""
|
||||
@spec unquote(function_name)(dsl_or_resource :: module | map) :: %{required(atom) => any}
|
||||
def unquote(function_name)(dsl_or_resource) do
|
||||
import Spark.Dsl.Extension, only: [get_opt: 4]
|
||||
|
||||
unquote(Macro.escape(options))
|
||||
|> Stream.map(fn option ->
|
||||
value =
|
||||
dsl_or_resource
|
||||
|> get_opt(option.path, option.name, Map.get(option, :default))
|
||||
|
||||
{option.name, value}
|
||||
end)
|
||||
|> Stream.reject(&is_nil(elem(&1, 1)))
|
||||
|> Map.new()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Given an extension and a list of DSL sections, generate an entities function
|
||||
which returns a list of entities.
|
||||
"""
|
||||
@spec generate_entity_functions(module, [atom]) :: Macro.t()
|
||||
defmacro generate_entity_functions(extension, sections) do
|
||||
entity_paths =
|
||||
extension.sections()
|
||||
|> Stream.filter(&(&1.name in sections))
|
||||
|> Stream.flat_map(&explode_section([], &1))
|
||||
|> Stream.filter(fn {_, section} -> Enum.any?(section.entities) end)
|
||||
|> Stream.map(&elem(&1, 0))
|
||||
|
||||
for path <- entity_paths do
|
||||
function_name = path |> Enum.join("_") |> String.to_atom()
|
||||
|
||||
quote location: :keep do
|
||||
@doc """
|
||||
#{unquote(Enum.join(path, "."))} DSL entities
|
||||
"""
|
||||
@spec unquote(function_name)(dsl_or_resource :: module | map) :: [struct]
|
||||
def unquote(function_name)(dsl_or_resource) do
|
||||
import Spark.Dsl.Extension, only: [get_entities: 2]
|
||||
|
||||
get_entities(dsl_or_resource, unquote(path))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Given an extension and a list of DSL sections generate individual config
|
||||
functions for each option.
|
||||
"""
|
||||
@spec generate_config_functions(module, [atom]) :: Macro.t()
|
||||
defmacro generate_config_functions(extension, sections) do
|
||||
for {_, options} <- extension_sections_to_option_list(extension, sections) do
|
||||
for option <- options do
|
||||
generate_config_function(option)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp explode_section(path, %{sections: [], name: name} = section),
|
||||
do: [{path ++ [name], section}]
|
||||
|
||||
defp explode_section(path, %{sections: sections, name: name} = section) do
|
||||
path = path ++ [name]
|
||||
|
||||
head = [{path, section}]
|
||||
tail = Stream.flat_map(sections, &explode_section(path, &1))
|
||||
|
||||
Stream.concat(head, tail)
|
||||
end
|
||||
|
||||
defp extension_sections_to_option_list(extension, sections) do
|
||||
extension.sections()
|
||||
|> Stream.filter(&(&1.name in sections))
|
||||
|> Stream.flat_map(&explode_section([], &1))
|
||||
|> Stream.reject(fn {_, section} -> Enum.empty?(section.schema) end)
|
||||
|> Stream.map(fn {path, section} ->
|
||||
schema =
|
||||
section.schema
|
||||
|> Enum.map(fn {name, opts} ->
|
||||
opts
|
||||
|> Map.new()
|
||||
|> Map.take(~w[type doc default]a)
|
||||
|> Map.update!(:type, &spec_for_type/1)
|
||||
|> Map.put(:pred?, name |> to_string() |> String.ends_with?("?"))
|
||||
|> Map.put(:name, name)
|
||||
|> Map.put(:path, path)
|
||||
|> Map.put(
|
||||
:function_name,
|
||||
path
|
||||
|> Enum.concat([name])
|
||||
|> Enum.join("_")
|
||||
|> String.trim_trailing("?")
|
||||
|> String.to_atom()
|
||||
)
|
||||
end)
|
||||
|
||||
{path, schema}
|
||||
end)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
defp generate_config_function(%{pred?: true} = option) do
|
||||
function_name = :"#{option.function_name}?"
|
||||
|
||||
quote location: :keep do
|
||||
@doc unquote(option.doc)
|
||||
@spec unquote(function_name)(dsl_or_resource :: module | map) ::
|
||||
unquote(option.type)
|
||||
def unquote(function_name)(dsl_or_resource) do
|
||||
import Spark.Dsl.Extension, only: [get_opt: 4]
|
||||
|
||||
get_opt(
|
||||
dsl_or_resource,
|
||||
unquote(option.path),
|
||||
unquote(option.name),
|
||||
unquote(option.default)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_config_function(option) do
|
||||
quote location: :keep do
|
||||
@doc unquote(Map.get(option, :doc, false))
|
||||
@spec unquote(option.function_name)(dsl_or_resource :: module | map) ::
|
||||
{:ok, unquote(option.type)} | :error
|
||||
|
||||
def unquote(option.function_name)(dsl_or_resource) do
|
||||
import Spark.Dsl.Extension, only: [get_opt: 4]
|
||||
|
||||
case get_opt(
|
||||
dsl_or_resource,
|
||||
unquote(option.path),
|
||||
unquote(option.name),
|
||||
unquote(Map.get(option, :default, :error))
|
||||
) do
|
||||
:error -> :error
|
||||
value -> {:ok, value}
|
||||
end
|
||||
end
|
||||
|
||||
@doc unquote(Map.get(option, :doc, false))
|
||||
@spec unquote(:"#{option.function_name}!")(dsl_or_resource :: module | map) ::
|
||||
unquote(option.type) | no_return
|
||||
|
||||
def unquote(:"#{option.function_name}!")(dsl_or_resource) do
|
||||
import Spark.Dsl.Extension, only: [get_opt: 4]
|
||||
|
||||
case get_opt(
|
||||
dsl_or_resource,
|
||||
unquote(option.path),
|
||||
unquote(option.name),
|
||||
unquote(Map.get(option, :default, :error))
|
||||
) do
|
||||
:error ->
|
||||
raise "No configuration for `#{unquote(option.name)}` present on `#{inspect(dsl_or_resource)}`."
|
||||
|
||||
value ->
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp spec_for_type({:behaviour, _module}), do: {:module, [], Elixir}
|
||||
|
||||
defp spec_for_type({:spark_function_behaviour, behaviour, {_, arity}}),
|
||||
do:
|
||||
spec_for_type(
|
||||
{:or,
|
||||
[
|
||||
{:behaviour, behaviour},
|
||||
{{:behaviour, behaviour}, {:keyword, [], Elixir}},
|
||||
{:fun, arity}
|
||||
]}
|
||||
)
|
||||
|
||||
defp spec_for_type({:fun, arity}) do
|
||||
args =
|
||||
0..(arity - 1)
|
||||
|> Enum.map(fn _ -> {:any, [], Elixir} end)
|
||||
|
||||
[{:->, [], [args, {:any, [], Elixir}]}]
|
||||
end
|
||||
|
||||
defp spec_for_type({:or, [type]}), do: spec_for_type(type)
|
||||
|
||||
defp spec_for_type({:or, [next | remaining]}),
|
||||
do: {:|, [], [spec_for_type(next), spec_for_type({:or, remaining})]}
|
||||
|
||||
defp spec_for_type({:in, %Range{first: first, last: last}})
|
||||
when is_integer(first) and is_integer(last),
|
||||
do: {:.., [], [first, last]}
|
||||
|
||||
defp spec_for_type({:in, %Range{first: first, last: last}}),
|
||||
do:
|
||||
{{:., [], [{:__aliases__, [], [:Range]}, :t]}, [],
|
||||
[spec_for_type(first), spec_for_type(last)]}
|
||||
|
||||
defp spec_for_type({:in, [type]}), do: spec_for_type(type)
|
||||
|
||||
defp spec_for_type({:in, [next | remaining]}),
|
||||
do: {:|, [], [spec_for_type(next), spec_for_type({:in, remaining})]}
|
||||
|
||||
defp spec_for_type({:list, subtype}), do: [spec_for_type(subtype)]
|
||||
|
||||
defp spec_for_type({:custom, _, _, _}), do: spec_for_type(:any)
|
||||
|
||||
defp spec_for_type({:tuple, subtypes}) do
|
||||
subtypes
|
||||
|> Enum.map(&spec_for_type/1)
|
||||
|> List.to_tuple()
|
||||
end
|
||||
|
||||
defp spec_for_type(:string),
|
||||
do: {{:., [], [{:__aliases__, [alias: false], [:String]}, :t]}, [], []}
|
||||
|
||||
defp spec_for_type(terminal)
|
||||
when terminal in ~w[any map atom string boolean integer non_neg_integer pos_integer float timeout pid reference mfa]a,
|
||||
do: {terminal, [], Elixir}
|
||||
|
||||
defp spec_for_type(atom) when is_atom(atom), do: atom
|
||||
defp spec_for_type(number) when is_number(number), do: number
|
||||
defp spec_for_type(string) when is_binary(string), do: spec_for_type(:string)
|
||||
|
||||
defp spec_for_type({mod, arg}) when is_atom(mod) and is_list(arg),
|
||||
do: {{:module, [], Elixir}, {:list, [], Elixir}}
|
||||
|
||||
defp spec_for_type(tuple) when is_tuple(tuple),
|
||||
do: tuple |> Tuple.to_list() |> Enum.map(&spec_for_type/1) |> List.to_tuple()
|
||||
|
||||
defp spec_for_type([]), do: []
|
||||
defp spec_for_type([type]), do: [spec_for_type(type)]
|
||||
end
|
|
@ -1,4 +1,6 @@
|
|||
defmodule AshAuthentication.Strategy.Auth0 do
|
||||
alias __MODULE__.Dsl
|
||||
|
||||
@moduledoc """
|
||||
Strategy for authenticating using [Auth0](https://auth0.com).
|
||||
|
||||
|
@ -14,63 +16,16 @@ defmodule AshAuthentication.Strategy.Auth0 do
|
|||
|
||||
See the [Auth0 quickstart guide](/documentation/tutorials/auth0-quickstart.html)
|
||||
for more information.
|
||||
|
||||
## DSL Documentation
|
||||
|
||||
#{Spark.Dsl.Extension.doc_entity(Dsl.dsl())}
|
||||
"""
|
||||
|
||||
alias AshAuthentication.Strategy.{Custom, OAuth2}
|
||||
use Custom
|
||||
|
||||
@doc false
|
||||
# credo:disable-for-next-line Credo.Check.Warning.SpecWithStruct
|
||||
@spec dsl :: Custom.entity()
|
||||
def dsl do
|
||||
OAuth2.dsl()
|
||||
|> Map.merge(%{
|
||||
name: :auth0,
|
||||
args: [{:optional, :name, :auth0}],
|
||||
describe: """
|
||||
Provides a pre-configured authentication strategy for [Auth0](https://auth0.com/).
|
||||
|
||||
This strategy is built using `:oauth2` strategy, and thus provides all the same
|
||||
configuration options should you need them.
|
||||
|
||||
For more information see the [Auth0 Quick Start Guide](/documentation/tutorials/auth0-quickstart.md)
|
||||
in our documentation.
|
||||
|
||||
#### Strategy defaults:
|
||||
|
||||
#{strategy_override_docs(Assent.Strategy.Auth0)}
|
||||
|
||||
#### Schema:
|
||||
""",
|
||||
auto_set_fields: strategy_fields(Assent.Strategy.Auth0, icon: :auth0)
|
||||
})
|
||||
end
|
||||
use Custom, entity: Dsl.dsl()
|
||||
|
||||
defdelegate transform(strategy, dsl_state), to: OAuth2
|
||||
defdelegate verify(strategy, dsl_state), to: OAuth2
|
||||
|
||||
defp strategy_fields(strategy, params) do
|
||||
[]
|
||||
|> strategy.default_config()
|
||||
|> Keyword.put(:assent_strategy, strategy)
|
||||
|> Keyword.merge(params)
|
||||
end
|
||||
|
||||
defp strategy_override_docs(strategy) do
|
||||
defaults =
|
||||
[]
|
||||
|> strategy.default_config()
|
||||
|> Enum.map_join(
|
||||
".\n",
|
||||
fn {key, value} ->
|
||||
" * `#{inspect(key)}` is set to `#{inspect(value)}`"
|
||||
end
|
||||
)
|
||||
|
||||
"""
|
||||
The following defaults are applied:
|
||||
|
||||
#{defaults}.
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
56
lib/ash_authentication/strategies/auth0/dsl.ex
Normal file
56
lib/ash_authentication/strategies/auth0/dsl.ex
Normal file
|
@ -0,0 +1,56 @@
|
|||
defmodule AshAuthentication.Strategy.Auth0.Dsl do
|
||||
@moduledoc false
|
||||
|
||||
alias AshAuthentication.Strategy.{Custom, OAuth2}
|
||||
|
||||
@doc false
|
||||
@spec dsl :: Custom.entity()
|
||||
def dsl do
|
||||
OAuth2.dsl()
|
||||
|> Map.merge(%{
|
||||
name: :auth0,
|
||||
args: [{:optional, :name, :auth0}],
|
||||
describe: """
|
||||
Provides a pre-configured authentication strategy for [Auth0](https://auth0.com/).
|
||||
|
||||
This strategy is built using `:oauth2` strategy, and thus provides all the same
|
||||
configuration options should you need them.
|
||||
|
||||
For more information see the [Auth0 Quick Start Guide](/documentation/tutorials/auth0-quickstart.md)
|
||||
in our documentation.
|
||||
|
||||
#### Strategy defaults:
|
||||
|
||||
#{strategy_override_docs(Assent.Strategy.Auth0)}
|
||||
|
||||
#### Schema:
|
||||
""",
|
||||
auto_set_fields: strategy_fields(Assent.Strategy.Auth0, icon: :auth0)
|
||||
})
|
||||
end
|
||||
|
||||
defp strategy_fields(strategy, params) do
|
||||
[]
|
||||
|> strategy.default_config()
|
||||
|> Keyword.put(:assent_strategy, strategy)
|
||||
|> Keyword.merge(params)
|
||||
end
|
||||
|
||||
defp strategy_override_docs(strategy) do
|
||||
defaults =
|
||||
[]
|
||||
|> strategy.default_config()
|
||||
|> Enum.map_join(
|
||||
".\n",
|
||||
fn {key, value} ->
|
||||
" * `#{inspect(key)}` is set to `#{inspect(value)}`"
|
||||
end
|
||||
)
|
||||
|
||||
"""
|
||||
The following defaults are applied:
|
||||
|
||||
#{defaults}.
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -18,12 +18,6 @@ defmodule AshAuthentication.Strategy.Custom do
|
|||
|
||||
@type strategy :: struct
|
||||
|
||||
@doc """
|
||||
A callback which allows the strategy to provide it's own DSL-based
|
||||
configuration.
|
||||
"""
|
||||
@callback dsl :: entity
|
||||
|
||||
@doc """
|
||||
If your strategy needs to modify either the entity or the parent resource then
|
||||
you can implement this callback.
|
||||
|
@ -61,7 +55,7 @@ defmodule AshAuthentication.Strategy.Custom do
|
|||
|
||||
@doc false
|
||||
@spec __using__(keyword) :: Macro.t()
|
||||
defmacro __using__(_opts) do
|
||||
defmacro __using__(opts) do
|
||||
quote generated: true do
|
||||
@behaviour unquote(__MODULE__)
|
||||
import unquote(__MODULE__).Helpers
|
||||
|
@ -70,6 +64,31 @@ defmodule AshAuthentication.Strategy.Custom do
|
|||
def verify(_entity, _dsl_state), do: :ok
|
||||
|
||||
defoverridable transform: 2, verify: 2
|
||||
|
||||
opts = unquote(opts)
|
||||
|
||||
path =
|
||||
opts
|
||||
|> Keyword.get(:style, :strategy)
|
||||
|> case do
|
||||
:strategy -> ~w[authentication strategies]a
|
||||
:add_on -> ~w[authentication add_ons]a
|
||||
end
|
||||
|
||||
entity =
|
||||
opts
|
||||
|> Keyword.get(:entity)
|
||||
|> case do
|
||||
%Dsl.Entity{} = entity ->
|
||||
entity
|
||||
|
||||
_ ->
|
||||
raise CompileError,
|
||||
"You must provide a `Spark.Dsl.Entity` as the `entity` argument to `use AshAuthentication.Strategy.Custom`."
|
||||
end
|
||||
|
||||
use Spark.Dsl.Extension,
|
||||
dsl_patches: [%Dsl.Patch.AddEntity{section_path: path, entity: entity}]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
16
lib/ash_authentication/strategies/custom/before_compileex
Normal file
16
lib/ash_authentication/strategies/custom/before_compileex
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule AshAuthentication.Strategy.Custom.BeforeCompile do
|
||||
@moduledoc false
|
||||
alias Spark.Dsl
|
||||
|
||||
defmacro __before_compile__(env) do
|
||||
quote generated: true do
|
||||
use Dsl.Extension,
|
||||
dsl_patches: [
|
||||
%Dsl.Patch.AddEntity{
|
||||
section_path: @patch_path,
|
||||
entity: dsl()
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@ defmodule AshAuthentication.Strategy.Custom.Transformer do
|
|||
|
||||
use Spark.Dsl.Transformer
|
||||
|
||||
alias AshAuthentication.{Dsl, Info, Strategy}
|
||||
alias AshAuthentication.{Info, Strategy}
|
||||
alias Spark.{Dsl.Transformer, Error.DslError}
|
||||
import AshAuthentication.Strategy.Custom.Helpers
|
||||
|
||||
|
@ -32,22 +32,33 @@ defmodule AshAuthentication.Strategy.Custom.Transformer do
|
|||
| {:warn, map(), String.t() | [String.t()]}
|
||||
| :halt
|
||||
def transform(dsl_state) do
|
||||
strategy_modules =
|
||||
Dsl.available_add_ons()
|
||||
|> Stream.concat(Dsl.available_strategies())
|
||||
|> Enum.map(&{&1.dsl().target, &1})
|
||||
strategy_to_target =
|
||||
:code.all_available()
|
||||
|> Stream.map(&elem(&1, 0))
|
||||
|> Stream.map(&to_string/1)
|
||||
|> Stream.filter(&String.starts_with?(&1, "Elixir.AshAuthentication"))
|
||||
|> Stream.map(&Module.concat([&1]))
|
||||
|> Stream.concat(Transformer.get_persisted(dsl_state, :extensions, []))
|
||||
|> Stream.filter(&Spark.implements_behaviour?(&1, Strategy.Custom))
|
||||
|> Stream.flat_map(fn strategy ->
|
||||
strategy.dsl_patches()
|
||||
|> Stream.map(&{&1.entity.target, strategy})
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
with {:ok, dsl_state} <- do_strategy_transforms(dsl_state, strategy_modules) do
|
||||
do_add_on_transforms(dsl_state, strategy_modules)
|
||||
dsl_state =
|
||||
Transformer.persist(dsl_state, :ash_authentication_strategy_to_target, strategy_to_target)
|
||||
|
||||
with {:ok, dsl_state} <- do_strategy_transforms(dsl_state, strategy_to_target) do
|
||||
do_add_on_transforms(dsl_state, strategy_to_target)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_strategy_transforms(dsl_state, strategy_modules) do
|
||||
defp do_strategy_transforms(dsl_state, strategy_to_target) do
|
||||
dsl_state
|
||||
|> Info.authentication_strategies()
|
||||
|> Enum.reduce_while({:ok, dsl_state}, fn strategy, {:ok, dsl_state} ->
|
||||
strategy_module = Map.fetch!(strategy_modules, strategy.__struct__)
|
||||
strategy_module = Map.fetch!(strategy_to_target, strategy.__struct__)
|
||||
|
||||
case do_transform(strategy_module, strategy, dsl_state, :strategy) do
|
||||
{:ok, dsl_state} -> {:cont, {:ok, dsl_state}}
|
||||
|
@ -56,11 +67,11 @@ defmodule AshAuthentication.Strategy.Custom.Transformer do
|
|||
end)
|
||||
end
|
||||
|
||||
defp do_add_on_transforms(dsl_state, strategy_modules) do
|
||||
defp do_add_on_transforms(dsl_state, strategy_to_target) do
|
||||
dsl_state
|
||||
|> Info.authentication_add_ons()
|
||||
|> Enum.reduce_while({:ok, dsl_state}, fn strategy, {:ok, dsl_state} ->
|
||||
strategy_module = Map.fetch!(strategy_modules, strategy.__struct__)
|
||||
strategy_module = Map.fetch!(strategy_to_target, strategy.__struct__)
|
||||
|
||||
case do_transform(strategy_module, strategy, dsl_state, :add_on) do
|
||||
{:ok, dsl_state} -> {:cont, {:ok, dsl_state}}
|
||||
|
|
|
@ -7,7 +7,8 @@ defmodule AshAuthentication.Strategy.Custom.Verifier do
|
|||
|
||||
use Spark.Dsl.Verifier
|
||||
|
||||
alias AshAuthentication.{Dsl, Info}
|
||||
alias AshAuthentication.Info
|
||||
alias Spark.Dsl.Transformer
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
|
@ -16,17 +17,15 @@ defmodule AshAuthentication.Strategy.Custom.Verifier do
|
|||
| {:error, term}
|
||||
| {:warn, String.t() | list(String.t())}
|
||||
def verify(dsl_state) do
|
||||
strategy_modules =
|
||||
Dsl.available_add_ons()
|
||||
|> Stream.concat(Dsl.available_strategies())
|
||||
|> Enum.map(&{&1.dsl().target, &1})
|
||||
|> Map.new()
|
||||
strategy_to_target =
|
||||
dsl_state
|
||||
|> Transformer.get_persisted(:ash_authentication_strategy_to_target, %{})
|
||||
|
||||
dsl_state
|
||||
|> Info.authentication_strategies()
|
||||
|> Stream.concat(Info.authentication_add_ons(dsl_state))
|
||||
|> Enum.reduce_while(:ok, fn strategy, :ok ->
|
||||
strategy_module = Map.fetch!(strategy_modules, strategy.__struct__)
|
||||
strategy_module = Map.fetch!(strategy_to_target, strategy.__struct__)
|
||||
|
||||
strategy
|
||||
|> strategy_module.verify(dsl_state)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
defmodule AshAuthentication.Strategy.Github do
|
||||
alias __MODULE__.Dsl
|
||||
|
||||
@moduledoc """
|
||||
Strategy for authenticating using [GitHub](https://github.com)
|
||||
|
||||
|
@ -13,63 +15,16 @@ defmodule AshAuthentication.Strategy.Github do
|
|||
|
||||
See the [GitHub quickstart guide](/documentation/tutorials/github-quickstart.html)
|
||||
for more information.
|
||||
|
||||
## DSL Documentation
|
||||
|
||||
#{Spark.Dsl.Extension.doc_entity(Dsl.dsl())}
|
||||
"""
|
||||
|
||||
alias AshAuthentication.Strategy.{Custom, OAuth2}
|
||||
use Custom
|
||||
|
||||
@doc false
|
||||
# credo:disable-for-next-line Credo.Check.Warning.SpecWithStruct
|
||||
@spec dsl :: Custom.entity()
|
||||
def dsl do
|
||||
OAuth2.dsl()
|
||||
|> Map.merge(%{
|
||||
name: :github,
|
||||
args: [{:optional, :name, :github}],
|
||||
describe: """
|
||||
Provides a pre-configured authentication strategy for [GitHub](https://github.com/).
|
||||
|
||||
This strategy is built using `:oauth2` strategy, and thus provides all the same
|
||||
configuration options should you need them.
|
||||
|
||||
For more information see the [Github Quick Start Guide](/documentation/tutorials/github-quickstart.md)
|
||||
in our documentation.
|
||||
|
||||
#### Strategy defaults:
|
||||
|
||||
#{strategy_override_docs(Assent.Strategy.Github)}
|
||||
|
||||
#### Schema:
|
||||
""",
|
||||
auto_set_fields: strategy_fields(Assent.Strategy.Github, icon: :github)
|
||||
})
|
||||
end
|
||||
use Custom, entity: Dsl.dsl()
|
||||
|
||||
defdelegate transform(strategy, dsl_state), to: OAuth2
|
||||
defdelegate verify(strategy, dsl_state), to: OAuth2
|
||||
|
||||
defp strategy_fields(strategy, params) do
|
||||
[]
|
||||
|> strategy.default_config()
|
||||
|> Keyword.put(:assent_strategy, strategy)
|
||||
|> Keyword.merge(params)
|
||||
end
|
||||
|
||||
defp strategy_override_docs(strategy) do
|
||||
defaults =
|
||||
[]
|
||||
|> strategy.default_config()
|
||||
|> Enum.map_join(
|
||||
".\n",
|
||||
fn {key, value} ->
|
||||
" * `#{inspect(key)}` is set to `#{inspect(value)}`"
|
||||
end
|
||||
)
|
||||
|
||||
"""
|
||||
The following defaults are applied:
|
||||
|
||||
#{defaults}.
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
56
lib/ash_authentication/strategies/github/dsl.ex
Normal file
56
lib/ash_authentication/strategies/github/dsl.ex
Normal file
|
@ -0,0 +1,56 @@
|
|||
defmodule AshAuthentication.Strategy.Github.Dsl do
|
||||
@moduledoc false
|
||||
|
||||
alias AshAuthentication.Strategy.{Custom, OAuth2}
|
||||
|
||||
@doc false
|
||||
@spec dsl :: Custom.entity()
|
||||
def dsl do
|
||||
OAuth2.dsl()
|
||||
|> Map.merge(%{
|
||||
name: :github,
|
||||
args: [{:optional, :name, :github}],
|
||||
describe: """
|
||||
Provides a pre-configured authentication strategy for [GitHub](https://github.com/).
|
||||
|
||||
This strategy is built using `:oauth2` strategy, and thus provides all the same
|
||||
configuration options should you need them.
|
||||
|
||||
For more information see the [Github Quick Start Guide](/documentation/tutorials/github-quickstart.md)
|
||||
in our documentation.
|
||||
|
||||
#### Strategy defaults:
|
||||
|
||||
#{strategy_override_docs(Assent.Strategy.Github)}
|
||||
|
||||
#### Schema:
|
||||
""",
|
||||
auto_set_fields: strategy_fields(Assent.Strategy.Github, icon: :github)
|
||||
})
|
||||
end
|
||||
|
||||
defp strategy_fields(strategy, params) do
|
||||
[]
|
||||
|> strategy.default_config()
|
||||
|> Keyword.put(:assent_strategy, strategy)
|
||||
|> Keyword.merge(params)
|
||||
end
|
||||
|
||||
defp strategy_override_docs(strategy) do
|
||||
defaults =
|
||||
[]
|
||||
|> strategy.default_config()
|
||||
|> Enum.map_join(
|
||||
".\n",
|
||||
fn {key, value} ->
|
||||
" * `#{inspect(key)}` is set to `#{inspect(value)}`"
|
||||
end
|
||||
)
|
||||
|
||||
"""
|
||||
The following defaults are applied:
|
||||
|
||||
#{defaults}.
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -241,9 +241,9 @@ defmodule AshAuthentication.Strategy.OAuth2 do
|
|||
icon: nil,
|
||||
assent_strategy: Assent.Strategy.OAuth2
|
||||
|
||||
use AshAuthentication.Strategy.Custom
|
||||
alias AshAuthentication.Strategy.{Custom, OAuth2}
|
||||
|
||||
alias AshAuthentication.Strategy.OAuth2
|
||||
use Custom, entity: Dsl.dsl()
|
||||
|
||||
@type secret :: nil | String.t() | {module, keyword}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
defmodule AshAuthentication.Strategy.Password do
|
||||
alias __MODULE__.{Dsl, Transformer, Verifier}
|
||||
use AshAuthentication.Strategy.Custom
|
||||
alias __MODULE__.Dsl
|
||||
|
||||
@moduledoc """
|
||||
Strategy for authenticating using local resources as the source of truth.
|
||||
|
@ -110,7 +109,17 @@ defmodule AshAuthentication.Strategy.Password do
|
|||
resource: nil
|
||||
|
||||
alias Ash.Resource
|
||||
alias AshAuthentication.{Jwt, Strategy.Password}
|
||||
|
||||
alias AshAuthentication.{
|
||||
Jwt,
|
||||
Strategy.Custom,
|
||||
Strategy.Password,
|
||||
Strategy.Password.Resettable,
|
||||
Strategy.Password.Transformer,
|
||||
Strategy.Password.Verifier
|
||||
}
|
||||
|
||||
use Custom, entity: Dsl.dsl()
|
||||
|
||||
@type t :: %Password{
|
||||
identity_field: atom,
|
||||
|
@ -121,7 +130,7 @@ defmodule AshAuthentication.Strategy.Password do
|
|||
password_confirmation_field: atom,
|
||||
register_action_name: atom,
|
||||
sign_in_action_name: atom,
|
||||
resettable: [Password.Resettable.t()],
|
||||
resettable: [Resettable.t()],
|
||||
name: atom,
|
||||
provider: atom,
|
||||
resource: module
|
||||
|
@ -138,7 +147,7 @@ defmodule AshAuthentication.Strategy.Password do
|
|||
"""
|
||||
@spec reset_token_for(t(), Resource.record()) :: {:ok, String.t()} | :error
|
||||
def reset_token_for(
|
||||
%Password{resettable: [%Password.Resettable{} = resettable]} = _strategy,
|
||||
%Password{resettable: [%Resettable{} = resettable]} = _strategy,
|
||||
user
|
||||
) do
|
||||
case Jwt.token_for_user(user, %{"act" => resettable.password_reset_action_name},
|
||||
|
|
|
@ -4,7 +4,7 @@ defmodule AshAuthentication.TokenResource.Info do
|
|||
extension.
|
||||
"""
|
||||
|
||||
use AshAuthentication.InfoGenerator,
|
||||
use Spark.InfoGenerator,
|
||||
extension: AshAuthentication.TokenResource,
|
||||
sections: [:token]
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ defmodule AshAuthentication.UserIdentity.Info do
|
|||
extension.
|
||||
"""
|
||||
|
||||
use AshAuthentication.InfoGenerator,
|
||||
use Spark.InfoGenerator,
|
||||
extension: AshAuthentication.UserIdentity,
|
||||
sections: [:user_identity]
|
||||
end
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -143,7 +143,7 @@ defmodule AshAuthentication.MixProject do
|
|||
defp deps do
|
||||
[
|
||||
{:ash, ash_version("~> 2.5 and >= 2.5.11")},
|
||||
{:spark, "~> 0.3.4"},
|
||||
{:spark, "~> 0.4 and >= 0.4.1"},
|
||||
{:jason, "~> 1.4"},
|
||||
{:joken, "~> 2.5"},
|
||||
{:plug, "~> 1.13"},
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -53,7 +53,7 @@
|
|||
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"sourceror": {:hex, :sourceror, "0.12.0", "b0a43f9b91d69fead8ffa307e203fadaf30001d8c2643f597518dd9508d6b32d", [:mix], [], "hexpm", "67dcc48a4d4bb8c397ad0b86cf0d4667e1b50ffdf31cda585524640d1caded85"},
|
||||
"spark": {:hex, :spark, "0.3.12", "6754698b5aa27358365fff88e3c5c3abc138e165fbc394cf482e39779ee92cd6", [:mix], [{:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "27f13b604ddaf1d2b3b6ce5b3a21ed170e2f1235e167ab6fb8c2c33c8165d545"},
|
||||
"spark": {:hex, :spark, "0.4.1", "72b4bdc67fb297dce209501a25db5f9e2c80ae22095d9bcf6acfdc9a445786c2", [:mix], [{:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "83ef6910bd3b47aa33403753c3211ff58f34287ea3e86b62063dfe3e1a1dee99"},
|
||||
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
defmodule Example.CustomStrategy do
|
||||
@moduledoc """
|
||||
An extremely dumb custom strategy that let's anyone with a name that starts
|
||||
with "Marty" sign in.
|
||||
"""
|
||||
|
||||
defstruct case_sensitive?: false, name_field: nil, resource: nil
|
||||
|
||||
use AshAuthentication.Strategy.Custom
|
||||
|
||||
def dsl do
|
||||
%Spark.Dsl.Entity{
|
||||
name: :only_marty,
|
||||
describe: "Strategy which only allows folks whose name starts with \"Marty\" to sign in.",
|
||||
examples: [
|
||||
"""
|
||||
only_marty do
|
||||
case_sensitive? true
|
||||
name_field :name
|
||||
end
|
||||
"""
|
||||
],
|
||||
target: __MODULE__,
|
||||
schema: [
|
||||
case_sensitive?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Ignore letter case when comparing?
|
||||
""",
|
||||
required: false,
|
||||
default: false
|
||||
],
|
||||
name_field: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The field to check for the users' name.
|
||||
""",
|
||||
required: true
|
||||
]
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defimpl AshAuthentication.Strategy do
|
||||
alias AshAuthentication.{Errors.AuthenticationFailed, Info}
|
||||
require Ash.Query
|
||||
import AshAuthentication.Plug.Helpers, only: [store_authentication_result: 2]
|
||||
|
||||
def name(_), do: :marty
|
||||
|
||||
def phases(_), do: [:sign_in]
|
||||
def actions(_), do: [:sign_in]
|
||||
|
||||
def routes(strategy) do
|
||||
subject_name = Info.authentication_subject_name!(strategy.resource)
|
||||
|
||||
[
|
||||
{"/#{subject_name}/marty", :sign_in}
|
||||
]
|
||||
end
|
||||
|
||||
def method_for_phase(_, :sign_in), do: :post
|
||||
|
||||
def plug(strategy, :sign_in, conn) do
|
||||
params = Map.take(conn.params, [to_string(strategy.name_field)])
|
||||
result = AshAuthentication.Strategy.action(strategy, :sign_in, params, [])
|
||||
store_authentication_result(conn, result)
|
||||
end
|
||||
|
||||
def action(strategy, :sign_in, params, _options) do
|
||||
name_field = strategy.name_field
|
||||
name = Map.get(params, to_string(name_field))
|
||||
api = Info.authentication_api!(strategy.resource)
|
||||
|
||||
strategy.resource
|
||||
|> Ash.Query.filter(ref(^name_field) == ^name)
|
||||
|> Ash.Query.after_action(fn
|
||||
query, [user] ->
|
||||
name =
|
||||
user
|
||||
|> Map.get(name_field)
|
||||
|> to_string()
|
||||
|
||||
{name, prefix} =
|
||||
if strategy.case_sensitive? do
|
||||
{name, "Marty"}
|
||||
else
|
||||
{String.downcase(name), "marty"}
|
||||
end
|
||||
|
||||
if String.starts_with?(name, prefix) do
|
||||
{:ok, [user]}
|
||||
else
|
||||
{:error,
|
||||
AuthenticationFailed.exception(query: query, caused_by: %{reason: :not_a_marty})}
|
||||
end
|
||||
|
||||
query, [] ->
|
||||
{:error, AuthenticationFailed.exception(query: query, caused_by: %{reason: :no_user})}
|
||||
|
||||
query, _ ->
|
||||
{:error,
|
||||
AuthenticationFailed.exception(query: query, caused_by: %{reason: :too_many_users})}
|
||||
end)
|
||||
|> api.read()
|
||||
|> case do
|
||||
{:ok, [user]} -> {:ok, user}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,48 +5,46 @@ defmodule Example.OnlyMartiesAtTheParty do
|
|||
|
||||
defstruct name: :marty, case_sensitive?: false, name_field: nil, resource: nil
|
||||
|
||||
use AshAuthentication.Strategy.Custom
|
||||
|
||||
def dsl do
|
||||
%Spark.Dsl.Entity{
|
||||
name: :only_marty,
|
||||
describe: "Strategy which only allows folks whose name starts with \"Marty\" to sign in.",
|
||||
examples: [
|
||||
"""
|
||||
only_marty do
|
||||
case_sensitive? true
|
||||
name_field :name
|
||||
end
|
||||
"""
|
||||
@entity %Spark.Dsl.Entity{
|
||||
name: :only_marty,
|
||||
describe: "Strategy which only allows folks whose name starts with \"Marty\" to sign in.",
|
||||
examples: [
|
||||
"""
|
||||
only_marty do
|
||||
case_sensitive? true
|
||||
name_field :name
|
||||
end
|
||||
"""
|
||||
],
|
||||
target: __MODULE__,
|
||||
args: [{:optional, :name, :marty}],
|
||||
schema: [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The strategy name.
|
||||
""",
|
||||
required: true
|
||||
],
|
||||
target: __MODULE__,
|
||||
args: [{:optional, :name, :marty}],
|
||||
schema: [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The strategy name.
|
||||
""",
|
||||
required: true
|
||||
],
|
||||
case_sensitive?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Ignore letter case when comparing?
|
||||
""",
|
||||
required: false,
|
||||
default: false
|
||||
],
|
||||
name_field: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The field to check for the users' name.
|
||||
""",
|
||||
required: true
|
||||
]
|
||||
case_sensitive?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Ignore letter case when comparing?
|
||||
""",
|
||||
required: false,
|
||||
default: false
|
||||
],
|
||||
name_field: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The field to check for the users' name.
|
||||
""",
|
||||
required: true
|
||||
]
|
||||
}
|
||||
end
|
||||
]
|
||||
}
|
||||
|
||||
use AshAuthentication.Strategy.Custom, entity: @entity
|
||||
|
||||
defimpl AshAuthentication.Strategy do
|
||||
alias AshAuthentication.Errors.AuthenticationFailed
|
||||
|
|
|
@ -5,7 +5,8 @@ defmodule Example.User do
|
|||
extensions: [
|
||||
AshAuthentication,
|
||||
AshGraphql.Resource,
|
||||
AshJsonApi.Resource
|
||||
AshJsonApi.Resource,
|
||||
Example.OnlyMartiesAtTheParty
|
||||
]
|
||||
|
||||
require Logger
|
||||
|
|
Loading…
Reference in a new issue