Module: ConcernsOnRails::Models::Anonymizable
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/concerns_on_rails/models/anonymizable.rb
Overview
Declarative right-to-erasure ("GDPR-lite") for personal data. The fourth member of the sensitive-data suite: Maskable masks display, Sanitizable strips HTML, Encryptable protects at rest — Anonymizable DESTROYS.
class User < ApplicationRecord
include ConcernsOnRails::Models::Anonymizable
anonymizable :email, with: :email # unique fake address
anonymizable :first_name, :last_name, with: :redact
anonymizable :ssn, with: :nullify
anonymizable :bio, with: ->(value) { value && "removed by user request" }
end
user.anonymize! # one UPDATE: strategies + anonymized_at stamp
user.anonymized? # => true
User.not_anonymized # scope (and .anonymized)
User.where(...).anonymize_all! # batch; returns the count
HOW IT WRITES — deliberately update_columns (single UPDATE, no
validations, no callbacks): erasure must not be blocked by a presence/
format validation, and must not run callbacks that would copy the OLD
values somewhere new (Auditable's capture hook is the canonical example).
update_columns serializes each value through the model's attribute types
— Encryptable's custom type included — so a field that is also
encryptable stores a fresh ciphertext envelope of the anonymized
value, never plaintext. The record is reloaded afterwards so in-memory
readers see the anonymized values through the types.
Strategy presets (with:):
:nullify — nil
:redact — "[REDACTED]"
:hash — SHA-256 hex of the value (deterministic pseudonymization:
the same input digests the same, so datasets keyed on the
value still join — NOT full anonymization)
:email — "anon-<random-hex>@anonymized.invalid" (random + unique,
so NOT NULL / unique-index email columns survive erasure;
.invalid is an RFC 2606 reserved TLD — it can never send)
:random_hex — 32 random hex chars (unique tokens/usernames)
a callable — ->(value) { ... } or ->(value, record) { ... }; nil-in
nil-out is the preset convention, custom callables choose
Notes:
* The stamp column (default :anonymized_at, `stamp: false` to opt out)
is what makes `anonymized?`, the scopes, and anonymize_all!'s
idempotency work — add it (a datetime) unless you truly can't.
* Auditable interaction: if any anonymized field is also audited, the
trail already holds historical plaintext, so anonymize! clears the
audit column in the SAME update (opt out per-macro with
`clear_audit_trail: false`). The trail is one column — clearing is
all-or-nothing.
* Encryptable interaction: works transparently (see HOW IT WRITES).
* Erasure is terminal: unsaved changes on the instance are discarded by
the post-write reload.
Defined Under Namespace
Modules: ClassMethods
Constant Summary collapse
- LABEL =
"ConcernsOnRails::Models::Anonymizable".freeze
- DEFAULT_STAMP =
:anonymized_at- UNSET =
Distinguishes "option not passed" from an explicit value, so repeat macro calls merge fields without silently resetting earlier options.
Object.new
- PRESETS =
{ nullify: ->(_value) {}, redact: ->(value) { value.nil? ? nil : "[REDACTED]" }, hash: ->(value) { value.nil? ? nil : Digest::SHA256.hexdigest(value.to_s) }, email: ->(value) { value.nil? ? nil : "anon-#{SecureRandom.hex(10)}@anonymized.invalid" }, random_hex: ->(value) { value.nil? ? nil : SecureRandom.hex(16) } }.freeze
Instance Method Summary collapse
- #after_anonymize ⇒ Object
-
#anonymize! ⇒ Object
Erase the configured fields in a single UPDATE (see the module docs for why validations and callbacks are deliberately skipped).
-
#anonymized? ⇒ Boolean
True when the stamp column is set; always false with
stamp: false(there is nothing to observe). -
#before_anonymize ⇒ Object
Lifecycle hooks — override in the model.
Instance Method Details
#after_anonymize ⇒ Object
157 |
# File 'lib/concerns_on_rails/models/anonymizable.rb', line 157 def after_anonymize; end |
#anonymize! ⇒ Object
Erase the configured fields in a single UPDATE (see the module docs for why validations and callbacks are deliberately skipped). Returns true.
161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/concerns_on_rails/models/anonymizable.rb', line 161 def anonymize! raise ArgumentError, "#{LABEL}: anonymize! cannot be called on a new record" if new_record? payload = anonymizable_payload transaction do before_anonymize update_columns(payload) after_anonymize end # update_columns leaves DB-serialized values (e.g. ciphertext) in the # in-memory attributes; reload so readers decode through the types. reload true end |
#anonymized? ⇒ Boolean
True when the stamp column is set; always false with stamp: false
(there is nothing to observe).
178 179 180 181 |
# File 'lib/concerns_on_rails/models/anonymizable.rb', line 178 def anonymized? stamp = self.class.anonymizable_stamp stamp ? self[stamp].present? : false end |
#before_anonymize ⇒ Object
Lifecycle hooks — override in the model. Run inside the anonymize! transaction, so a raising hook rolls the erasure back.
156 |
# File 'lib/concerns_on_rails/models/anonymizable.rb', line 156 def before_anonymize; end |