Module: Clickwrap::Retention::Disposition
- Defined in:
- lib/clickwrap/retention/disposition.rb
Overview
The two destructive operations in the whole gem, in one place.
Clickwrap::Retention::Disposition.delete_field!(receipt, :ip_address, because: "...")
Clickwrap::Retention::Disposition.dispose_core_event!(event, because: "...")
Both name exactly what they remove, both require a reason in plain
English, both refuse while a legal hold is in effect, and both append
their own event saying what went and why. There is deliberately no
delete_personal_data!, no purge_network_context!, and no method that
takes out three categories at once: someone reading a disposition report
a year from now has to be able to see which field disappeared, not a
euphemism covering several.
These back the public Clickwrap.delete_recorded_ip_address!,
Clickwrap.delete_recorded_browser_user_agent!, and
Clickwrap.delete_recorded_ip_geolocation!.
Constant Summary collapse
- FIELDS =
%i[ip_address browser_user_agent ip_geolocation].freeze
- IP_GEOLOCATION_VALUE_COLUMNS =
Everything an IP-geolocation deletion nulls: the estimated values themselves, and nothing else.
What deliberately stays is the provenance of what was there and of its removal — which provider estimated it, from which database version, how uncertain it was, when it was recorded, under which rule it was due, and when it was deleted. Those columns are not the personal estimate; they are the record that an estimate existed and was disposed of on schedule. Erasing them too would turn a documented deletion into a gap, which is the one outcome a retention process must never produce.
RequestEvidence::IP_GEOLOCATION_VALUE_COLUMNS
- COLUMNS_FOR_FIELD =
One category, one set of columns. The raw values are the only things that go; the reader name, the trusted-proxy configuration digest, and the recorded-at timestamp explain where the deleted value came from.
RequestEvidence::VALUE_COLUMNS_BY_CATEGORY
Class Method Summary collapse
-
.delete_field!(receipt_or_event, field, because:) ⇒ Object
Deletes one recorded request-evidence field from the annex attached to this event, and records that it did.
-
.dispose_core_event!(event, because:) ⇒ Object
Marks a core event disposed of under its retention rule.
-
.event_for(receipt_or_event) ⇒ Object
Resolves a receipt, an event, or an event id to the event itself, so the public methods read naturally from either end.
-
.legal_hold_in_effect?(event) ⇒ Boolean
Whether disposition is currently paused for this event by a hold on the event itself, on the actor, or on the policy.
Class Method Details
.delete_field!(receipt_or_event, field, because:) ⇒ Object
Deletes one recorded request-evidence field from the annex attached to this event, and records that it did.
Returns the appended disposition event, or nil when there was nothing to delete — no annex, nothing recorded for that field, or the field already deleted. A no-op appends no event, because an event saying "deleted" where nothing was ever recorded would be false.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/clickwrap/retention/disposition.rb', line 49 def delete_field!(receipt_or_event, field, because:) field = normalize_field!(field) require_reason!(because, "Deleting the recorded #{field}") event_id = event_for(receipt_or_event).id ::ActiveRecord::Base.transaction do event = Event.lock.find(event_id) refuse_while_on_legal_hold!(event, "the recorded #{field}") annex = RequestEvidence.lock.find_by(event_id: event.id) return nil if annex.nil? return nil if annex.deleted_for?(field) return nil if annex.public_send(:"#{field}_recorded_at").nil? status = event.request_evidence_binding_status unless %i[verified disposed_with_documented_events].include?(status) raise ImmutableEvidenceError, "Request evidence for event #{event.id} failed its binding check (#{status}); " \ "Clickwrap refused to delete it because disposition must not hide an integrity problem." end delete_annex_field!(annex, event, field, because) end end |
.dispose_core_event!(event, because:) ⇒ Object
Marks a core event disposed of under its retention rule.
The row stays. What changes is core_event_disposed_at, which is one
of the named columns the Event model permits for disposition, and a linked
disposition event that explains it. An auditor then reads a
documented disposition rather than finding a hole where an agreement
used to be, and verification of the surrounding chain still works.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/clickwrap/retention/disposition.rb', line 82 def dispose_core_event!(event, because:) event = event_for(event) require_reason!(because, "Disposing of the core event") ::ActiveRecord::Base.transaction do event = Event.lock.find(event.id) return nil if event.disposed? refuse_while_on_legal_hold!(event, "the core event") disposed_at = Clickwrap.now disposition = Lifecycle.append_lifecycle_event!( event: event, event_type: "disposition", reason: "Disposed of the core event under its retention rule. #{because}", extra: { protected_outcome: { "core_event_disposition" => { "event_id" => event.id, "original_event_digest" => event.event_digest, "disposed_at" => Receipt.format_time(disposed_at), "removed_statement_count" => event.statements.size, "removed_document_binding_count" => event.documents.size, "removed_fields" => core_payload_field_names } } } ) event.dispose_core_payload!(disposition_event: disposition, at: disposed_at) disposition end end |
.event_for(receipt_or_event) ⇒ Object
Resolves a receipt, an event, or an event id to the event itself, so the public methods read naturally from either end.
131 132 133 134 135 136 137 138 139 |
# File 'lib/clickwrap/retention/disposition.rb', line 131 def event_for(receipt_or_event) return receipt_or_event if receipt_or_event.is_a?(Event) return receipt_or_event.event if receipt_or_event.respond_to?(:event) return Receipt.find(receipt_or_event.to_s).event if receipt_or_event.is_a?(String) raise ArgumentError, "Disposition needs a Clickwrap receipt, event, or event id, got " \ "#{receipt_or_event.class}." end |
.legal_hold_in_effect?(event) ⇒ Boolean
Whether disposition is currently paused for this event by a hold on the event itself, on the actor, or on the policy. All three scopes count: a hold placed on an actor's whole file is not weaker than one placed on a single receipt.
118 119 120 121 122 123 124 125 126 127 |
# File 'lib/clickwrap/retention/disposition.rb', line 118 def legal_hold_in_effect?(event) return true if event.on_legal_hold? holds = LegalHold.in_effect return true if holds.where(hold_scope: "event", event_id: event.id).exists? return true if event.actor_reference.present? && holds.where(hold_scope: "actor", actor_reference: event.actor_reference).exists? holds.where(hold_scope: "policy", policy_key: event.policy_key).exists? end |