This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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