-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: added cascade option Signed-off-by: Clément Quaresma <[email protected]> * fix: added Ecto.Association.HasThrough Signed-off-by: Clément Quaresma <[email protected]> * Update lib/ecto_anon.ex Co-authored-by: Thomas Battiston <[email protected]> * test: renamed relationship for follower * feat: moved from custom field to config macro * feat: added associations in config for cascade * feat: renamed anon_config to anon_schema * feat: changed how custom function is called * feat: PR comment Co-authored-by: Thomas Battiston <[email protected]>
- Loading branch information
Showing
8 changed files
with
252 additions
and
40 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,28 +5,51 @@ defmodule EctoAnon do | |
|
||
@doc """ | ||
Updates an Ecto struct with anonymized data based on its anon_fields declared in the struct schema. | ||
defmodule User do | ||
use Ecto.Schema | ||
use EctoAnon | ||
use EctoAnon.Schema | ||
anon_schema [ | ||
:firstname, | ||
email: &__MODULE__.anonymized_email/3 | ||
birthdate: [:anonymized_date, options: [:only_year]] | ||
] | ||
schema "user" do | ||
# ... fields ... | ||
anon_field :email, :string | ||
field :fistname, :string | ||
field :email, :string | ||
field :birthdate, :utc_datetime | ||
end | ||
def anonymized_email(_type, _value, _opts) do | ||
"[email protected]" | ||
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. | ||
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. | ||
## Example | ||
defmodule User do | ||
use Ecto.Schema | ||
use EctoAnon | ||
use EctoAnon.Schema | ||
anon_schema [ | ||
] | ||
schema "users" do | ||
field :name, :string | ||
field :age, :integer, default: 0 | ||
anon_field :email, :string | ||
field :email, :string | ||
end | ||
end | ||
|
@@ -41,7 +64,28 @@ defmodule EctoAnon do | |
""" | ||
@spec run(struct(), Ecto.Repo.t(), keyword()) :: | ||
{:ok, Ecto.Schema.t()} | {:error, :non_anonymizable_struct} | ||
def run(struct, repo, _opts \\ []) do | ||
|
||
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 | ||
case EctoAnon.Anonymizer.anonymized_data(struct) do | ||
{:ok, data} -> EctoAnon.Query.run(data, repo, struct) | ||
{:error, error} -> {:error, error} | ||
|
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
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
17 changes: 17 additions & 0 deletions
17
test/db/priv/repo/migrations/20220105150654_create_users_associations.exs
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
defmodule EctoAnon.Repo.Migrations.CreateUsersAssociations do | ||
use Ecto.Migration | ||
|
||
def change do | ||
create table(:comments) do | ||
add(:content, :string) | ||
add(:tag, :string) | ||
add(:author_id, references(:users)) | ||
end | ||
|
||
create table(:followers) do | ||
add(:follower_id, references(:users)) | ||
add(:followee_id, references(:users)) | ||
timestamps() | ||
end | ||
end | ||
end |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,61 @@ | ||
defmodule EctoAnonTest do | ||
use ExUnit.Case, async: true | ||
alias EctoAnon.{Repo, User} | ||
alias EctoAnon.{Repo, User, Comment} | ||
|
||
defmodule UnknownStruct do | ||
defstruct name: "John", age: 27 | ||
end | ||
|
||
describe "run/3" do | ||
setup do | ||
mick = | ||
%User{ | ||
email: "[email protected]", | ||
firstname: "Mick", | ||
lastname: "Rogers", | ||
last_sign_in_at: ~U[2021-01-03 00:00:00Z], | ||
followers: [] | ||
} | ||
|> Repo.insert!() | ||
|
||
fred = | ||
%User{ | ||
email: "[email protected]", | ||
firstname: "Fred", | ||
lastname: "Duncan", | ||
last_sign_in_at: ~U[2023-03-20 00:00:00Z], | ||
followers: [] | ||
} | ||
|> Repo.insert!() | ||
|
||
emilie = | ||
%User{ | ||
email: "[email protected]", | ||
firstname: "Emilie", | ||
lastname: "Duncan", | ||
last_sign_in_at: ~U[2018-09-04 00:00:00Z], | ||
followers: [fred] | ||
} | ||
|> Repo.insert!() | ||
|
||
user = | ||
%User{ | ||
email: "[email protected]", | ||
firstname: "John", | ||
lastname: "Doe", | ||
last_sign_in_at: ~U[2022-05-04 00:00:00Z] | ||
last_sign_in_at: ~U[2022-05-04 00:00:00Z], | ||
followers: [mick, emilie] | ||
} | ||
|> Repo.insert!() | ||
|
||
{:ok, user: user} | ||
%Comment{ | ||
content: "this is a comment", | ||
tag: "tag", | ||
author_id: user.id | ||
} | ||
|> Repo.insert!() | ||
|
||
{:ok, user: user, mick: mick, emilie: emilie} | ||
end | ||
|
||
test "with struct with anonymizable fields, should return anonymized struct", %{user: user} do | ||
|
@@ -32,5 +70,50 @@ defmodule EctoAnonTest do | |
test "with non-ecto struct, should return an error" do | ||
assert {:error, _error} = EctoAnon.run(%UnknownStruct{}, Repo) | ||
end | ||
|
||
test "with cascade option, should anonymize struct and associations", %{ | ||
user: user, | ||
mick: mick, | ||
emilie: emilie | ||
} do | ||
assert {:ok, updated_user} = | ||
Repo.get(User, user.id) | ||
|> EctoAnon.run(Repo, cascade: true) | ||
|
||
assert %User{ | ||
email: "redacted", | ||
firstname: "John", | ||
last_sign_in_at: ~U[2022-01-01 00:00:00Z], | ||
lastname: "redacted" | ||
} = updated_user | ||
|
||
%Comment{ | ||
content: "this is a comment", | ||
tag: "tag" | ||
} = Repo.get_by(Comment, author_id: user.id) | ||
|
||
%User{ | ||
email: "redacted", | ||
firstname: "Mick", | ||
last_sign_in_at: ~U[2021-01-01 00:00:00Z], | ||
lastname: "redacted", | ||
followers: [] | ||
} = Repo.get(User, mick.id) |> Repo.preload(:followers) | ||
|
||
%User{ | ||
email: "redacted", | ||
firstname: "Emilie", | ||
last_sign_in_at: ~U[2018-01-01 00:00:00Z], | ||
lastname: "redacted", | ||
followers: [ | ||
%User{ | ||
email: "redacted", | ||
firstname: "Fred", | ||
last_sign_in_at: ~U[2023-01-01 00:00:00Z], | ||
lastname: "redacted" | ||
} | ||
] | ||
} = Repo.get(User, emilie.id) |> Repo.preload(:followers) | ||
end | ||
end | ||
end |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
defmodule EctoAnon.Comment do | ||
use Ecto.Schema | ||
use EctoAnon.Schema | ||
|
||
alias EctoAnon.User | ||
|
||
anon_schema([ | ||
:content | ||
]) | ||
|
||
schema "comments" do | ||
field(:content, :string) | ||
field(:tag, :string) | ||
belongs_to(:users, User, foreign_key: :author_id) | ||
end | ||
end |
Oops, something went wrong.