defmodule EctoAnon do
@moduledoc """
`EctoAnon` provides a simple way to handle data anonymization directly in your Ecto schemas.
"""
@doc """
Updates an Ecto struct with anonymized data based on its anon_schema declaration.
It also updates any embedded schema declared in anon_schema.
defmodule User do
use Ecto.Schema
use EctoAnon.Schema
anon_schema [
:firstname,
email: &__MODULE__.anonymized_email/3
birthdate: [:anonymized_date, options: [:only_year]]
]
schema "user" do
field :fistname, :string
field :email, :string
field :birthdate, :utc_datetime
end
def anonymized_email(_type, _value, _opts) do
"xxx@xxx.com"
end
end
It returns `{:ok, struct}` if the struct has been successfully updated or `{:error, :non_anonymizable_struct}` if the struct has no anonymizable fields.
## Options
* `:cascade` - When set to `true`, allows ecto_anon to preload and anonymize
all associations (and associations of these associations) automatically in cascade.
Could be used to anonymize all data related a struct in a single call.
Note that this won't traverse `belongs_to` associations to avoid infinite and cyclic anonymizations.
Default: false
* `:log`- When set to `true`, it will set `anonymized` field when EctoAnon.run
applies anonymization on a ressource.
Default: true
## Example
defmodule User do
use Ecto.Schema
use EctoAnon.Schema
anon_schema [
:email
]
schema "users" do
field :name, :string
field :age, :integer, default: 0
field :email, :string
end
end
By default, an anon_field will be anonymized with a default value based on its type:
iex> user = Repo.get(User, id)
%User{name: "jane", age: 0, email: "jane@email.com"}
iex> EctoAnon.run(user, Repo)
{:ok, %User{name: "jane", age: 0, email: "redacted"}}
"""
@spec run(struct(), Ecto.Repo.t(), keyword()) ::
{:ok, Ecto.Schema.t()} | {:error, :non_anonymizable_struct}
def run(struct, repo, _opts \\ [])
def run(struct, _repo, _opts) when struct in [[], nil], do: {:error, :non_anonymizable_struct}
def run(struct, repo, opts) when is_list(struct), do: Enum.each(struct, &run(&1, repo, opts))
def run(%mod{} = struct, repo, cascade: true) do
anon_fields = mod.__anon_fields__() |> Enum.map(fn {field, _} -> field end)
associations =
mod.__schema__(:associations)
|> Enum.filter(&(&1 in anon_fields and EctoAnon.Anonymizer.is_association?(mod, &1)))
struct = repo.preload(struct, associations)
associations
|> Enum.each(&run(Map.get(struct, &1), repo, cascade: true))
run(struct, repo)
end
def run(struct, repo, opts) do
with {:ok, data} <- EctoAnon.Anonymizer.anonymized_data(struct),
{:ok, anonymized_data} <- EctoAnon.Query.apply(data, repo, struct) do
if set_anonymized?(struct, opts) do
EctoAnon.Query.set_anonymized(repo, anonymized_data)
else
{:ok, anonymized_data}
end
else
{:error, error} -> {:error, error}
end
end
defp set_anonymized?(%mod{} = _struct, opts) do
Keyword.get(opts, :log, true) and :anonymized in mod.__schema__(:fields)
end
end