Current section

Files

Jump to
ash_authentication lib ash_authentication add_ons audit_log verifier.ex
Raw

lib/ash_authentication/add_ons/audit_log/verifier.ex

# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.AddOn.AuditLog.Verifier do
@moduledoc """
Provides configuration validation for the AuditLog add-on.
"""
alias Spark.Error.DslError
@doc false
def verify(strategy, _dsl) do
with :ok <- verify_audit_log_resource(strategy),
:ok <- verify_exclude_strategies(strategy),
:ok <- verify_exclude_actions(strategy),
:ok <- verify_truncation_masks(strategy) do
verify_sensitive_fields(strategy)
end
end
defp verify_audit_log_resource(strategy) do
cond do
!Spark.Dsl.is?(strategy.audit_log_resource, Ash.Resource) ->
{:error,
DslError.exception(
module: strategy.resource,
path: [:authentication, :add_ons, :audit_log, strategy.name, :audit_log_resource],
message: "The module `#{inspect(strategy.audit_log_resource)}` is not an Ash resource."
)}
AshAuthentication.AuditLogResource not in Spark.extensions(strategy.audit_log_resource) ->
{:error,
DslError.exception(
module: strategy.resource,
path: [:authentication, :add_ons, :audit_log, strategy.name, :audit_log_resource],
message:
"The resource `#{inspect(strategy.audit_log_resource)}` must use the `AshAuthentication.AuditLogResource` extension."
)}
true ->
:ok
end
end
defp verify_exclude_strategies(strategy) when strategy.exclude_strategies == [], do: :ok
defp verify_exclude_strategies(strategy) do
strategy.exclude_strategies
|> Enum.reject(&AshAuthentication.Info.strategy_present?(strategy.resource, &1))
|> case do
[] ->
:ok
[missing_strategy] ->
{:error,
DslError.exception(
module: strategy.resource,
path: [:authentication, :add_ons, :audit_log, strategy.name, :exclude_strategies],
message:
"The strategy or add-on `#{inspect(missing_strategy)}` is not present on the resource `#{inspect(strategy.resource)}`."
)}
missing_strategies ->
missing_strategies = Enum.map_join(missing_strategies, "\n - ", &"`#{inspect(&1)}`")
{:error,
DslError.exception(
module: strategy.resource,
path: [:authentication, :add_ons, :audit_log, strategy.name, :exclude_strategies],
message: """
The following strategies or add-ons are not present on the resource `#{inspect(strategy.resource)}`:
- #{missing_strategies}
"""
)}
end
end
defp verify_truncation_masks(strategy) do
with :ok <- verify_ipv4_mask(strategy) do
verify_ipv6_mask(strategy)
end
end
defp verify_ipv4_mask(strategy) do
mask = Map.get(strategy, :ipv4_truncation_mask, 24)
if mask >= 0 and mask <= 32 do
:ok
else
{:error,
DslError.exception(
module: strategy.resource,
path: [:authentication, :add_ons, :audit_log, strategy.name, :ipv4_truncation_mask],
message: "IPv4 truncation mask must be between 0 and 32, got #{mask}"
)}
end
end
defp verify_ipv6_mask(strategy) do
mask = Map.get(strategy, :ipv6_truncation_mask, 48)
if mask >= 0 and mask <= 128 do
:ok
else
{:error,
DslError.exception(
module: strategy.resource,
path: [:authentication, :add_ons, :audit_log, strategy.name, :ipv6_truncation_mask],
message: "IPv6 truncation mask must be between 0 and 128, got #{mask}"
)}
end
end
defp verify_exclude_actions(strategy) when strategy.exclude_actions == [], do: :ok
defp verify_exclude_actions(strategy) do
strategy.exclude_actions
|> Enum.reject(&Ash.Resource.Info.action(strategy.resource, &1))
|> case do
[] ->
:ok
[missing_action] ->
{:error,
DslError.exception(
module: strategy.resource,
path: [:authentication, :add_ons, :audit_log, strategy.name, :exclude_actions],
message:
"The action `#{inspect(missing_action)}` is not present on the resource `#{inspect(strategy.resource)}`."
)}
missing_actions ->
missing_actions = Enum.map_join(missing_actions, "\n - ", &"`#{inspect(&1)}`")
{:error,
DslError.exception(
module: strategy.resource,
path: [:authentication, :add_ons, :audit_log, strategy.name, :exclude_actions],
message: """
The following actions are not present on the resource `#{inspect(strategy.resource)}`:
- #{missing_actions}
"""
)}
end
end
defp verify_sensitive_fields(strategy) when strategy.include_fields == [], do: :ok
defp verify_sensitive_fields(strategy) do
# Find all attributes and action arguments that are sensitive
attributes = Ash.Resource.Info.attributes(strategy.resource)
actions = Ash.Resource.Info.actions(strategy.resource)
# Collect all arguments from all actions
all_arguments =
actions
|> Enum.flat_map(& &1.arguments)
|> Enum.uniq_by(& &1.name)
# Find sensitive fields that are being explicitly included
sensitive_fields_included =
strategy.include_fields
|> Enum.filter(fn field_name ->
# Check if field is a sensitive attribute
attribute_sensitive? =
attributes
|> Enum.find(&(&1.name == field_name))
|> case do
nil -> false
attr -> attr.sensitive?
end
# Check if field is a sensitive argument
argument_sensitive? =
all_arguments
|> Enum.find(&(&1.name == field_name))
|> case do
nil -> false
arg -> Map.get(arg, :sensitive?, false)
end
attribute_sensitive? || argument_sensitive?
end)
if sensitive_fields_included != [] &&
!Application.get_env(:ash_authentication, :suppress_sensitive_field_warnings?, false) do
field_list = Enum.map_join(sensitive_fields_included, ", ", &inspect/1)
IO.warn("""
AuditLog is configured to log sensitive fields: [#{field_list}]
Sensitive fields are being explicitly included in audit logs for resource #{inspect(strategy.resource)}.
This may expose sensitive user data in your audit logs.
To suppress this warning, add the following to your config:
config :ash_authentication, suppress_sensitive_field_warnings?: true
Only suppress this warning if you have verified that logging these sensitive fields is intentional and complies with your security policies.
""")
end
:ok
end
end