Packages
ash_authentication
4.6.1
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/token_resource/transformer.ex
defmodule AshAuthentication.TokenResource.Transformer do
@moduledoc """
The token resource transformer.
Sets up the default schema and actions for the token resource.
"""
use Spark.Dsl.Transformer
require Ash.Expr
alias Ash.{Resource, Type}
alias AshAuthentication.{TokenResource, TokenResource.Info}
alias Spark.Dsl.Transformer
alias Spark.Error.DslError
import AshAuthentication.Utils
import AshAuthentication.Validations
import AshAuthentication.Validations.Action
import AshAuthentication.Validations.Attribute
@doc false
@impl true
@spec after?(any) :: boolean()
def after?(_), do: false
@doc false
@impl true
@spec before?(any) :: boolean
def before?(Resource.Transformers.CachePrimaryKey), do: true
def before?(Resource.Transformers.DefaultAccept), do: true
def before?(_), do: false
@doc false
@impl true
@spec transform(map) ::
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
def transform(dsl_state) do
with {:ok, dsl_state} <- maybe_set_domain(dsl_state, :token),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :jti, :string,
primary_key?: true,
allow_nil?: false,
sensitive?: true,
writable?: true,
public?: true
),
:ok <- validate_jti_field(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :subject, :string,
allow_nil?: false,
writable?: true,
public?: true
),
:ok <- validate_subject_field(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :expires_at, :utc_datetime,
allow_nil?: false,
writable?: true,
public?: true
),
:ok <- validate_expires_at_field(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :purpose, :string,
allow_nil?: false,
writable?: true,
public?: true
),
:ok <- validate_purpose_field(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :extra_data, :map,
allow_nil?: true,
writable?: true,
public?: true
),
:ok <- validate_extra_data_field(dsl_state),
{:ok, created_at} <-
TokenResource.Info.token_created_at_attribute_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, created_at, :utc_datetime_usec,
allow_nil?: false,
public?: false,
default: &DateTime.utc_now/0
),
{:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :updated_at, :utc_datetime_usec,
allow_nil?: false,
public?: false,
default: &DateTime.utc_now/0,
update_default: &DateTime.utc_now/0
),
{:ok, expunge_expired_action_name} <- Info.token_expunge_expired_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
expunge_expired_action_name,
&build_expunge_expired_action(&1, expunge_expired_action_name)
),
:ok <- validate_expunge_expired_action(dsl_state, expunge_expired_action_name),
{:ok, read_expired_action_name} <- Info.token_read_expired_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
read_expired_action_name,
&build_read_expired_action(&1, read_expired_action_name)
),
:ok <- validate_read_expired_action(dsl_state, read_expired_action_name),
{:ok, revoke_token_action_name} <-
Info.token_revocation_revoke_token_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
revoke_token_action_name,
&build_revoke_token_action(&1, revoke_token_action_name)
),
:ok <- validate_revoke_token_action(dsl_state, revoke_token_action_name),
{:ok, revoke_all_stored_for_subject_action_name} <-
Info.token_revocation_revoke_all_stored_for_subject_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
revoke_all_stored_for_subject_action_name,
&build_revoke_all_stored_for_subject_action(
&1,
revoke_all_stored_for_subject_action_name
)
),
:ok <-
validate_revoke_all_stored_for_subject_action(
dsl_state,
revoke_all_stored_for_subject_action_name
),
{:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
is_revoked_action_name,
&build_is_revoked_action(&1, is_revoked_action_name)
),
:ok <- validate_is_revoked_action(dsl_state, is_revoked_action_name),
{:ok, get_confirmation_changes_action_name} <-
Info.token_confirmation_get_changes_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
get_confirmation_changes_action_name,
&build_get_confirmation_changes_action(&1, get_confirmation_changes_action_name)
),
:ok <-
validate_get_confirmation_changes_action(
dsl_state,
get_confirmation_changes_action_name
),
{:ok, store_confirmation_changes_action_name} <-
Info.token_confirmation_store_changes_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
store_confirmation_changes_action_name,
&build_store_confirmation_changes_action(&1, store_confirmation_changes_action_name)
),
:ok <-
validate_store_confirmation_changes_action(
dsl_state,
store_confirmation_changes_action_name
),
{:ok, store_token_action_name} <-
Info.token_store_token_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
store_token_action_name,
&build_store_token_action(&1, store_token_action_name)
),
:ok <- validate_store_token_action(dsl_state, store_token_action_name),
{:ok, get_token_action_name} <- Info.token_get_token_action_name(dsl_state),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
get_token_action_name,
&build_get_token_action(&1, get_token_action_name)
),
:ok <- validate_get_token_action(dsl_state, get_token_action_name),
:ok <- validate_primary_key(dsl_state) do
{:ok, dsl_state}
end
end
defp validate_primary_key(dsl_state) do
dsl_state
|> Ash.Resource.Info.attributes()
|> Enum.filter(& &1.primary_key?)
|> Enum.map(& &1.name)
|> case do
[:jti] ->
:ok
fields ->
module = Transformer.get_persisted(dsl_state, :module)
{:error,
DslError.exception(
module: module,
path: [:primary_key],
message: """
The token resource must only have `:jti` as a primary key attribute.
Found: #{inspect(fields)}
You are likely seeing this as a by-product of an error with the generators
that added a `uuid_primary_key :id` to the token resource.
This is **not a security issue**, because previous versions of AshAuthentication
checked for revocation tokens as a separate check. In the future, however,
we will not perform this check, which means that all tokens must be guaranteed
to be unique on `jti`.
To address this:
1. Make sure that you are on `ash_postgres` >= 2.5.6`
2. remove `uuid_primary_key :id` from `#{inspect(module)}`
3. run `mix ash.codegen remove_id_from_tokens`
4. Add the following code to the *top* of the `up` function in the generated migration.
execute(\"""
WITH duplicate_tokens AS (
SELECT jti
FROM tokens
GROUP BY jti
HAVING COUNT(*) > 1
),
revocation_tokens AS (
SELECT DISTINCT ON (t.jti) t.id
FROM tokens t
JOIN duplicate_tokens d ON t.jti = d.jti
WHERE t.purpose = 'revocation'
),
other_tokens AS (
SELECT t.*
FROM tokens t
JOIN duplicate_tokens d ON t.jti = d.jti
WHERE t.id NOT IN (SELECT id FROM revocation_tokens)
)
DELETE FROM tokens
WHERE id IN (SELECT id FROM other_tokens);
\""")
"""
)}
end
end
defp validate_subject_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :subject),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]) do
validate_attribute_option(attribute, resource, :writable?, [true])
end
end
defp validate_get_token_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <-
validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]),
:ok <- validate_action_argument_option(action, :token, :allow_nil?, [true]),
:ok <- validate_action_argument_option(action, :token, :sensitive?, [true]),
:ok <- validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]),
:ok <- validate_action_argument_option(action, :jti, :allow_nil?, [true]),
:ok <-
validate_action_argument_option(action, :purpose, :type, [Ash.Type.String, :string]) do
validate_action_has_preparation(action, TokenResource.GetTokenPreparation)
end
end
defp build_get_token_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :token,
type: :string,
sensitive?: true
),
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :jti,
type: :string,
sensitive?: true
),
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :purpose,
type: :string,
sensitive?: false
)
]
preparations = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: TokenResource.GetTokenPreparation
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: action_name,
arguments: arguments,
preparations: preparations,
get?: true
)
end
defp validate_store_token_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <- validate_token_argument(action) do
validate_action_has_change(action, TokenResource.StoreTokenChange)
end
end
defp build_store_token_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: :token,
type: :string,
allow_nil?: false,
sensitive?: true
)
]
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: TokenResource.StoreTokenChange
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: action_name,
arguments: arguments,
changes: changes,
accept: [:extra_data, :purpose]
)
end
defp build_store_confirmation_changes_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: :token,
type: :string,
allow_nil?: false,
sensitive?: true
)
]
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: TokenResource.StoreConfirmationChangesChange
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: action_name,
arguments: arguments,
changes: changes,
accept: [:extra_data, :purpose]
)
end
defp validate_store_confirmation_changes_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <- validate_token_argument(action) do
validate_action_has_change(action, TokenResource.StoreConfirmationChangesChange)
end
end
defp build_get_confirmation_changes_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :jti,
type: :string,
allow_nil?: false,
sensitive?: true
)
]
preparations = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: TokenResource.GetConfirmationChangesPreparation
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: action_name,
arguments: arguments,
preparations: preparations,
get?: true
)
end
defp validate_get_confirmation_changes_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
:ok <- validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]),
:ok <-
validate_action_has_preparation(
action,
TokenResource.GetConfirmationChangesPreparation
) do
validate_field_in_values(action, :type, [:read])
end
end
defp build_read_expired_action(_dsl_state, action_name) do
import Ash.Expr
filter =
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :filter,
filter: expr(expires_at < now())
)
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: action_name,
filters: [filter]
)
end
defp validate_read_expired_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name) do
validate_field_in_values(action, :type, [:read])
end
end
defp validate_is_revoked_action(dsl_state, action_name) do
case validate_action_exists(dsl_state, action_name) do
{:ok, %{type: :read} = action} ->
with :ok <-
validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]),
:ok <-
validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]) do
validate_action_has_preparation(action, TokenResource.IsRevokedPreparation)
end
{:ok, %{type: :action} = action} ->
with :ok <-
validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]) do
validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string])
end
{:ok, _} ->
{:error,
DslError.exception(
module: Transformer.get_persisted(dsl_state, :module),
path: [:actions, :is_revoked],
message: "The action `:is_revoked` must be a read action or a generic action"
)}
_ ->
:ok
end
end
defp build_is_revoked_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :token,
type: :string,
allow_nil?: true,
sensitive?: true
),
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: :jti,
type: :string,
allow_nil?: true,
sensitive?: true
)
]
preparations = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: TokenResource.IsRevokedPreparation
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: action_name,
get?: true,
preparations: preparations,
arguments: arguments
)
end
defp validate_subject_argument(action) do
with :ok <-
validate_action_argument_option(action, :subject, :type, [Ash.Type.String, :string]),
:ok <- validate_action_argument_option(action, :subject, :allow_nil?, [false]) do
validate_action_argument_option(action, :subject, :sensitive?, [true])
end
end
defp validate_token_argument(action) do
with :ok <-
validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]),
:ok <- validate_action_argument_option(action, :token, :allow_nil?, [false]) do
validate_action_argument_option(action, :token, :sensitive?, [true])
end
end
defp validate_revoke_token_action(dsl_state, revoke_token_action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, revoke_token_action_name),
:ok <- validate_token_argument(action) do
validate_action_has_change(action, TokenResource.RevokeTokenChange)
end
end
defp build_revoke_token_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: :token,
type: :string,
allow_nil?: false,
sensitive?: true
)
]
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: TokenResource.RevokeTokenChange
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: action_name,
arguments: arguments,
changes: changes,
upsert?: true,
accept: [:extra_data]
)
end
defp validate_revoke_all_stored_for_subject_action(
dsl_state,
revoke_all_stored_for_subject_action_name
) do
with {:ok, action} <-
validate_action_exists(dsl_state, revoke_all_stored_for_subject_action_name),
:ok <- validate_subject_argument(action) do
validate_action_has_change(action, TokenResource.RevokeAllStoredForSubjectChange)
end
end
defp build_revoke_all_stored_for_subject_action(_dsl_state, action_name) do
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :update], :argument,
name: :subject,
type: :string,
allow_nil?: false,
sensitive?: true
)
]
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: TokenResource.RevokeAllStoredForSubjectChange
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :update,
name: action_name,
arguments: arguments,
changes: changes,
accept: [:extra_data]
)
end
defp validate_expunge_expired_action(dsl_state, action_name) do
with {:ok, action} <- validate_action_exists(dsl_state, action_name) do
validate_field_in_values(action, :type, [:destroy])
end
end
defp build_expunge_expired_action(_dsl_state, action_name) do
import Ash.Expr
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :destroy], :change,
change: {Ash.Resource.Change.Filter, filter: expr(expires_at < now())}
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :destroy,
name: action_name,
changes: changes
)
end
defp validate_jti_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :jti),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
:ok <- validate_attribute_option(attribute, resource, :sensitive?, [true]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
:ok <- validate_attribute_option(attribute, resource, :primary_key?, [true]) do
validate_attribute_option(attribute, resource, :public?, [true])
end
end
defp validate_expires_at_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :expires_at),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do
validate_attribute_option(attribute, resource, :writable?, [true])
end
end
defp validate_purpose_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :purpose),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
validate_attribute_option(attribute, resource, :public?, [true])
end
end
defp validate_extra_data_field(dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :extra_data),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.Map, :map]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [true]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
validate_attribute_option(attribute, resource, :public?, [true])
end
end
end