Packages
ash_authentication
5.0.0-rc.5
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/add_ons/audit_log/auditor.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.AddOn.AuditLog.Auditor do
@moduledoc """
Provides common audit logging behaviour for Ash actions.
"""
alias __MODULE__
alias AshAuthentication.AddOn.AuditLog.IpPrivacy
alias Spark.Dsl.Extension
require Logger
@type input :: Ash.ActionInput.t() | Ash.Changeset.t() | Ash.Query.t()
@type result :: {:ok, Ash.Resource.record()} | {:ok, [Ash.Resource.record()]} | {:error, any()}
defmodule Change do
@moduledoc "Implements the `Ash.Resource.Change` behaviour for audit logging"
use Ash.Resource.Change
@doc false
@impl true
def change(changeset, opts, context) do
tracked_actions = Auditor.get_tracked_actions(changeset.resource, opts[:strategy])
if changeset.action.name in tracked_actions do
changeset
|> Auditor.tag_status_override_ref()
|> Ash.Changeset.after_transaction(
&Auditor.after_transaction(&1, &2, opts[:strategy], context)
)
else
changeset
end
end
@doc false
@impl true
def atomic(changeset, opts, context), do: {:ok, change(changeset, opts, context)}
end
defmodule Preparation do
@moduledoc "Implements the `Ash.Resource.Preparation` behaviour for audit logging"
use Ash.Resource.Preparation
@doc false
@impl true
def prepare(query, opts, context) when is_struct(query, Ash.Query) do
tracked_actions = Auditor.get_tracked_actions(query.resource, opts[:strategy])
if query.action.name in tracked_actions do
query
|> Auditor.tag_status_override_ref()
|> Ash.Query.after_transaction(
&Auditor.after_transaction(&1, &2, opts[:strategy], context)
)
else
query
end
end
def prepare(input, opts, context) when is_struct(input, Ash.ActionInput) do
tracked_actions = Auditor.get_tracked_actions(input.resource, opts[:strategy])
if input.action.name in tracked_actions do
input
|> Auditor.tag_status_override_ref()
|> Ash.ActionInput.after_transaction(
&Auditor.after_transaction(&1, &2, opts[:strategy], context)
)
else
input
end
end
@doc false
@impl true
def supports(_opts), do: [Ash.Query, Ash.ActionInput]
end
@doc false
@spec get_tracked_actions(Ash.Resource.t(), atom) :: [atom]
def get_tracked_actions(resource, strategy_name) do
persisted = Extension.get_persisted(resource, {:audit_log, strategy_name, :actions}) || []
# Extract just the action names from the tuples
Enum.map(persisted, fn
{action_name, _strategy_name} -> action_name
action_name -> action_name
end)
end
@doc false
@spec after_transaction(input, result, atom, map) :: result
def after_transaction(input, result, strategy_name, context) do
audit_strategy = AshAuthentication.Info.strategy!(input.resource, strategy_name)
action_strategy = get_action_strategy(input, audit_strategy)
status = determine_status(input, result)
subject = extract_subject(result, input.resource)
request = extract_request(context)
identity = extract_identity(input, action_strategy)
client_ip = extract_client_ip(request, audit_strategy)
extra_data = build_extra_data(context, request, input, audit_strategy)
params = %{
strategy: action_strategy.name,
subject: subject,
identity: identity,
client_ip: client_ip,
audit_log: audit_strategy.name,
logged_at: DateTime.utc_now(),
action_name: input.action.name,
status: status,
extra_data: extra_data,
resource: input.resource
}
with {:error, reason} <-
AshAuthentication.AuditLogResource.log_activity(audit_strategy, params) do
Logger.error(fn ->
"""
Error writing audit log: #{inspect(reason)}
"""
end)
end
result
end
defp get_action_strategy(input, audit_strategy) do
case AshAuthentication.Info.strategy_for_action(input.resource, input.action.name) do
{:ok, strategy} -> strategy
:error -> audit_strategy
end
end
@doc """
Tag a query/changeset/action input with a unique reference used to bridge
status overrides from downstream `after_action` callbacks into the audit
log's `after_transaction` callback.
Called automatically by the audit-log `Preparation` and `Change` modules;
you should not need to call this directly.
"""
@spec tag_status_override_ref(input) :: input
def tag_status_override_ref(input) do
ref = make_ref()
put_private_context(input, :ash_authentication_audit_log_ref, ref)
end
@doc """
Record a status override for the audit log entry that will be written for
this action.
Called from a preparation's `after_action` (or change's `after_action`) when
the result of the action alone is not sufficient to determine whether the
operation should be recorded as `:success` or `:failure` (for example,
password reset and magic-link request actions return `:ok` regardless of
whether the submitted identity resolved to a known user).
If the audit-log add-on is not configured for this action, this call is a
no-op.
"""
@spec record_status_override(input, :success | :failure | :unknown) :: :ok
def record_status_override(input, status)
when status in [:success, :failure, :unknown] do
case status_override_ref(input) do
nil -> :ok
ref -> Process.put({__MODULE__, :status_override, ref}, status)
end
:ok
end
defp determine_status(input, result) do
case take_status_override(input) do
status when status in [:success, :failure, :unknown] -> status
_ -> determine_status_from_result(result)
end
end
defp take_status_override(input) do
case status_override_ref(input) do
nil ->
nil
ref ->
Process.delete({__MODULE__, :status_override, ref})
end
end
defp status_override_ref(input) do
input
|> Map.get(:context, %{})
|> Kernel.||(%{})
|> Map.get(:private, %{})
|> Map.get(:ash_authentication_audit_log_ref)
end
defp put_private_context(%Ash.Query{} = query, key, value) do
Ash.Query.set_context(query, %{private: %{key => value}})
end
defp put_private_context(%Ash.Changeset{} = changeset, key, value) do
Ash.Changeset.set_context(changeset, %{private: %{key => value}})
end
defp put_private_context(%Ash.ActionInput{} = input, key, value) do
Ash.ActionInput.set_context(input, %{private: %{key => value}})
end
defp determine_status_from_result(result) do
case result do
:ok ->
:success
{:ok, _} ->
:success
{:ok, _, _} ->
:success
{:error, _} ->
:failure
:error ->
:failure
other ->
Logger.warning(
"Auditor after_transaction hook received unexpected result: `#{inspect(other)}`"
)
:unknown
end
end
defp extract_subject(result, resource) do
case result do
{:ok, user} when is_struct(user, resource) ->
AshAuthentication.user_to_subject(user)
# Read actions with get? true return {:ok, [user]}
{:ok, [user]} when is_struct(user, resource) ->
AshAuthentication.user_to_subject(user)
_ ->
nil
end
end
defp extract_request(context) do
context
|> Map.get(:source_context, %{})
|> Map.get(:ash_authentication_request, %{})
|> Map.merge(Map.get(context, :ash_authentication_request, %{}))
end
defp extract_identity(input, action_strategy) do
with identity_field when is_atom(identity_field) <-
Map.get(action_strategy, :identity_field),
value when not is_nil(value) <-
get_argument_or_attribute(input, identity_field) do
to_string(value)
else
_ -> nil
end
end
defp get_argument_or_attribute(input, field) when is_struct(input, Ash.Changeset) do
case Map.get(input.arguments, field) do
nil -> Ash.Changeset.get_attribute(input, field)
value -> value
end
end
defp get_argument_or_attribute(input, field) do
Map.get(input.arguments, field)
end
defp extract_client_ip(request, audit_strategy) do
case Map.get(request, :remote_ip) do
nil ->
nil
ip ->
ip_privacy_mode = Map.get(audit_strategy, :ip_privacy_mode, :none)
truncation_masks = %{
ipv4: Map.get(audit_strategy, :ipv4_truncation_mask, 24),
ipv6: Map.get(audit_strategy, :ipv6_truncation_mask, 48)
}
IpPrivacy.apply_privacy(ip, ip_privacy_mode, %{truncation_masks: truncation_masks})
end
end
defp build_extra_data(context, request, input, audit_strategy) do
# Apply IP privacy transformations to request data
ip_privacy_mode = Map.get(audit_strategy, :ip_privacy_mode, :none)
truncation_masks = %{
ipv4: Map.get(audit_strategy, :ipv4_truncation_mask, 24),
ipv6: Map.get(audit_strategy, :ipv6_truncation_mask, 48)
}
processed_request =
IpPrivacy.apply_to_request(
request,
ip_privacy_mode,
%{truncation_masks: truncation_masks}
)
context
|> Map.take([:actor, :tenant])
|> Map.update(:actor, nil, &serialise_actor/1)
|> Map.put(:request, processed_request)
|> Map.put(:params, get_params(input, audit_strategy))
end
defp get_params(input, audit_strategy) do
argument_names =
Extension.get_persisted(
audit_strategy.resource,
{:audit_log, audit_strategy.name, input.action.name, :arguments}
) || []
attribute_names =
Extension.get_persisted(
audit_strategy.resource,
{:audit_log, audit_strategy.name, input.action.name, :attributes}
) || []
arguments = get_named_arguments(input, argument_names)
attributes = get_named_attributes(input, attribute_names)
attributes
|> Map.merge(arguments)
end
defp get_named_arguments(input, argument_names), do: Map.take(input.arguments, argument_names)
defp get_named_attributes(_input, []), do: %{}
defp get_named_attributes(input, attribute_names) when is_struct(input, Ash.Changeset),
do: Map.new(attribute_names, &{&1, Ash.Changeset.get_attribute(input, &1)})
defp get_named_attributes(_input, _attribute_names), do: %{}
defp serialise_actor(nil), do: nil
defp serialise_actor(actor) when is_struct(actor) do
AshAuthentication.user_to_subject(actor)
rescue
_ -> inspect(actor)
end
defp serialise_actor(actor), do: actor
end