Skip to content

Instantly share code, notes, and snippets.

@kerryb
Created Oct 17, 2019
Embed
What would you like to do?
defmodule MyApp.Repo do
require Logger
alias Ecto.Association.NotLoaded
alias Ecto.{Changeset, Schema}
alias MyApp.Auth.User
...
@doc """
Wrapper round `Ecto.Repo.insert/2`, which logs the fields of the inserted
record to the audit log.
"""
@spec audited_insert(Changeset.t(), User.t(), function() | nil) ::
{:ok, Schema.t()} | {:error, Changeset.t()}
def audited_insert(changeset, user, logger \\ &Logger.info/2) do
changeset
|> insert
|> case do
{:ok, _} = resp ->
log_insert(logger, changeset, user)
resp
resp ->
resp
end
end
@doc """
Wrapper round `Ecto.Repo.update/2`, which logs changed fields to the audit
log.
"""
@spec audited_update(Changeset.t(), String.t(), User.t(), function() | nil) ::
{:ok, Schema.t()} | {:error, Changeset.t()}
def audited_update(changeset, label, user, logger \\ &Logger.info/2) do
changeset
|> update
|> case do
{:ok, _} = resp ->
log_update(logger, changeset, label, user)
resp
resp ->
resp
end
end
@doc """
Wrapper round `Ecto.Repo.delete/2`, which logs the fields of the deleted
record to the audit log.
"""
@spec audited_delete(Changeset.t(), String.t(), User.t(), function() | nil) ::
{:ok, Schema.t()} | {:error, Changeset.t()}
def audited_delete(record, label, user, logger \\ &Logger.info/2) do
record
|> delete
|> case do
{:ok, _} = resp ->
log_delete(logger, record, label, user)
resp
resp ->
resp
end
end
defp log_update(logger, changeset, label, user) do
logger.(
fn -> "#{user.email} updated #{label}: #{change_info(changeset)}" end,
audit: true
)
end
defp log_insert(logger, changeset, user) do
with label <-
changeset.data.__struct__
|> to_string
|> String.replace(~r/.*\./, "")
|> Macro.underscore() do
logger.(
fn ->
"#{user.email} inserted #{label}: #{changeset.changes |> filter_passwords |> inspect}"
end,
audit: true
)
end
end
defp log_delete(logger, record, label, user) do
logger.(
fn ->
"#{user.email} deleted #{label}: #{
record
|> Map.from_struct()
|> remove_metadata
|> remove_associations
|> filter_passwords
|> inspect
}"
end,
audit: true
)
end
defp change_info(changeset) do
changeset.changes |> old_and_new_values(changeset) |> filter_passwords |> inspect
end
defp old_and_new_values(params, changeset) do
params
|> Enum.map(&old_and_new_value(&1, changeset))
end
defp old_and_new_value({key, value}, changeset) do
{key, [from: changeset.data |> Map.get(key), to: value]}
end
defp remove_metadata(params), do: params |> Enum.reject(fn {k, _v} -> k == :__meta__ end)
defp remove_associations(params) do
params |> Enum.reject(fn {_k, v} -> match?(%NotLoaded{}, v) end)
end
defp filter_passwords(params), do: params |> Enum.map(&filter_password/1)
defp filter_password({key, value} = param) when is_binary(value) do
if to_string(key) =~ ~r/password/, do: {key, "[FILTERED]"}, else: param
end
defp filter_password({key, [from: from, to: to]} = param)
when is_binary(from) or is_binary(to) do
if to_string(key) =~ ~r/password/, do: {key, "[FILTERED]"}, else: param
end
defp filter_password(param), do: param
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment