From d2642e828aac5e57f242f773636f089d6791fb80 Mon Sep 17 00:00:00 2001 From: Noah Betzen Date: Wed, 2 Apr 2025 10:34:26 -0700 Subject: [PATCH 1/3] Add @redact_schema: :all_except_primary_keys --- lib/ecto/schema.ex | 16 +++++++++++++++- test/ecto/changeset_test.exs | 18 ++++++++++++++++++ test/ecto/schema_test.exs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/lib/ecto/schema.ex b/lib/ecto/schema.ex index c8768f1471..6442da4d83 100644 --- a/lib/ecto/schema.ex +++ b/lib/ecto/schema.ex @@ -130,6 +130,9 @@ defmodule Ecto.Schema do inspect on the schema unless the schema module is tagged with the option `@derive_inspect_for_redacted_fields false`. + A schema module tagged with `@schema_redact :all_except_primary_keys` will + redact all fields except primary keys. + ## Schema attributes Supported attributes for configuring the defined schema. They must @@ -155,6 +158,10 @@ defmodule Ecto.Schema do which generates structs and queries without context. Context are not used by the built-in SQL adapters. + * `@schema_redact` - If set to `:all_except_primary_keys`, Ecto will + treat all non-primary key fields as if they were individually marked + as redacted. + * `@foreign_key_type` - configures the default foreign key type used by `belongs_to` associations. It must be set in the same module that defines the `belongs_to`. Defaults to `:id`; @@ -1992,7 +1999,14 @@ defmodule Ecto.Schema do writable = opts[:writable] || :always put_struct_field(mod, name, Keyword.get(opts, :default)) - if Keyword.get(opts, :redact, false) do + redact_field? = Keyword.get_lazy(opts, :redact, fn -> + case Module.get_attribute(mod, :schema_redact) do + :all_except_primary_keys -> not pk? + _ -> false + end + end) + + if redact_field? do Module.put_attribute(mod, :ecto_redact_fields, name) end diff --git a/test/ecto/changeset_test.exs b/test/ecto/changeset_test.exs index ede164927d..b70216f04f 100644 --- a/test/ecto/changeset_test.exs +++ b/test/ecto/changeset_test.exs @@ -3607,6 +3607,17 @@ defmodule Ecto.ChangesetTest do end end + defmodule RedactAllExceptPrimaryKeysSchema do + use Ecto.Schema + + @schema_redact :all_except_primary_keys + schema "redacted_schema" do + field :password, :string + field :username, :string + field :virtual_pass, :string, virtual: true + end + end + defmodule RedactedEmbeddedSchema do use Ecto.Schema @@ -3683,5 +3694,12 @@ defmodule Ecto.ChangesetTest do assert inspect(changeset) =~ "hunter2" refute inspect(changeset) =~ "**redacted**" end + + test "redacts all non-primary-key fields when schema sets @schema_redact :all_except_primary_keys" do + changeset = Ecto.Changeset.cast(%RedactAllExceptPrimaryKeysSchema{}, %{username: "Hunter", password: "hunter2"}, [:username, :password]) + assert inspect(changeset) =~ "id" + refute inspect(changeset) =~ "hunter2" + assert inspect(changeset) =~ "**redacted**" + end end end diff --git a/test/ecto/schema_test.exs b/test/ecto/schema_test.exs index 15e9c27b96..d84d78a78e 100644 --- a/test/ecto/schema_test.exs +++ b/test/ecto/schema_test.exs @@ -192,6 +192,37 @@ defmodule Ecto.SchemaTest do refute inspect(%Schema{temp: "hunter2"}) =~ "hunter2" end + defmodule SchemaWithRedactAllExceptPrimaryKeys do + use Ecto.Schema + + @schema_redact :all_except_primary_keys + schema "my_schema" do + field :password, :string + field :temp, :any, default: "temp", virtual: true + end + end + + test "schema with @schema_redact: :all_except_primary_keys derives inspect for all non-primary-key fields" do + inspected_schema = inspect(%SchemaWithRedactAllExceptPrimaryKeys{password: "hunter2"}) + + assert inspected_schema =~ "id" + refute inspected_schema =~ "hunter2" + refute inspected_schema =~ "temp" + end + + defmodule SchemaWithRedactFalse do + use Ecto.Schema + + @schema_redact false + schema "my_schema" do + field :name, :string + end + end + + test "schema with @schema_redact: false doesn't derive inspect" do + assert inspect(%SchemaWithRedactFalse{name: "Hunter"}) =~ "Hunter" + end + defmodule SchemaWithoutDeriveInspect do use Ecto.Schema From 1c8aefe9d956ec70bfb183c4c234c9b90572cff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 3 Apr 2025 17:33:13 +0200 Subject: [PATCH 2/3] Update lib/ecto/schema.ex --- lib/ecto/schema.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ecto/schema.ex b/lib/ecto/schema.ex index 6442da4d83..2d925680b1 100644 --- a/lib/ecto/schema.ex +++ b/lib/ecto/schema.ex @@ -2002,7 +2002,7 @@ defmodule Ecto.Schema do redact_field? = Keyword.get_lazy(opts, :redact, fn -> case Module.get_attribute(mod, :schema_redact) do :all_except_primary_keys -> not pk? - _ -> false + nil -> false end end) From 7f81ec8982a20beda1c6f5a3515296d856b15692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 3 Apr 2025 17:44:09 +0200 Subject: [PATCH 3/3] Apply suggestions from code review --- lib/ecto/schema.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ecto/schema.ex b/lib/ecto/schema.ex index 2d925680b1..6fd48378a3 100644 --- a/lib/ecto/schema.ex +++ b/lib/ecto/schema.ex @@ -160,7 +160,8 @@ defmodule Ecto.Schema do * `@schema_redact` - If set to `:all_except_primary_keys`, Ecto will treat all non-primary key fields as if they were individually marked - as redacted. + as redacted. Defaults to `false`, as no fields are redacted by default. + The value set here can be changed per field through the `:redact` option. * `@foreign_key_type` - configures the default foreign key type used by `belongs_to` associations. It must be set in the same @@ -2000,9 +2001,9 @@ defmodule Ecto.Schema do put_struct_field(mod, name, Keyword.get(opts, :default)) redact_field? = Keyword.get_lazy(opts, :redact, fn -> - case Module.get_attribute(mod, :schema_redact) do + case Module.get_attribute(mod, :schema_redact, false) do :all_except_primary_keys -> not pk? - nil -> false + false -> false end end)