Packages
ash_authentication
4.13.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.
Retired package: incorrectly pinned assent version
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
Ash.Changeset.after_transaction(
changeset,
&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
Ash.Query.after_transaction(
query,
&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
Ash.ActionInput.after_transaction(
input,
&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(result)
subject = extract_subject(result, input.resource)
request = extract_request(context)
extra_data = build_extra_data(context, request, input, audit_strategy)
params = %{
strategy: action_strategy.name,
subject: subject,
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
defp determine_status(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 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.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: %{}
end