clickwrap 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +90 -0
- data/CHANGELOG.md +612 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +241 -0
- data/app/controllers/clickwrap/application_controller.rb +79 -0
- data/app/controllers/clickwrap/captures_controller.rb +145 -0
- data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
- data/app/controllers/clickwrap/receipts_controller.rb +115 -0
- data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
- data/app/helpers/clickwrap/engine_helper.rb +97 -0
- data/app/views/clickwrap/captures/show.html.erb +34 -0
- data/app/views/clickwrap/receipts/index.html.erb +38 -0
- data/app/views/clickwrap/receipts/show.html.erb +91 -0
- data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
- data/app/views/clickwrap/shared/_fields.html.erb +100 -0
- data/app/views/clickwrap/shared/_statement.html.erb +105 -0
- data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
- data/config/locales/en.yml +160 -0
- data/config/locales/es.yml +138 -0
- data/config/routes.rb +41 -0
- data/exe/clickwrap +374 -0
- data/guides/README.md +30 -0
- data/guides/accessibility.md +249 -0
- data/guides/consent-and-lifecycle.md +299 -0
- data/guides/integrating.md +610 -0
- data/guides/integrity.md +212 -0
- data/guides/migrating.md +335 -0
- data/guides/naming.md +320 -0
- data/guides/organizations.md +320 -0
- data/guides/receipts-and-verification.md +415 -0
- data/guides/request-evidence.md +512 -0
- data/guides/retention-and-legal-holds.md +438 -0
- data/lib/clickwrap/actor_proxy.rb +147 -0
- data/lib/clickwrap/anonymous_actor.rb +47 -0
- data/lib/clickwrap/authority.rb +174 -0
- data/lib/clickwrap/canonical_json.rb +216 -0
- data/lib/clickwrap/capture/event_builder.rb +220 -0
- data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
- data/lib/clickwrap/capture.rb +650 -0
- data/lib/clickwrap/configuration.rb +1129 -0
- data/lib/clickwrap/controller_helpers.rb +758 -0
- data/lib/clickwrap/current_state.rb +282 -0
- data/lib/clickwrap/digest.rb +125 -0
- data/lib/clickwrap/doctor.rb +418 -0
- data/lib/clickwrap/document_definition.rb +255 -0
- data/lib/clickwrap/document_renderer.rb +83 -0
- data/lib/clickwrap/document_renderers/markdown.rb +175 -0
- data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
- data/lib/clickwrap/dsl/policy_builder.rb +462 -0
- data/lib/clickwrap/dsl/retention_builder.rb +89 -0
- data/lib/clickwrap/durable_commit_callback.rb +37 -0
- data/lib/clickwrap/engine.rb +184 -0
- data/lib/clickwrap/errors.rb +181 -0
- data/lib/clickwrap/form_builder_extensions.rb +341 -0
- data/lib/clickwrap/front_matter.rb +67 -0
- data/lib/clickwrap/identifier.rb +112 -0
- data/lib/clickwrap/import/external_receipt.rb +241 -0
- data/lib/clickwrap/import/fine_print.rb +290 -0
- data/lib/clickwrap/import/legacy.rb +450 -0
- data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
- data/lib/clickwrap/integrity/anchor.rb +130 -0
- data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
- data/lib/clickwrap/integrity/attestor.rb +221 -0
- data/lib/clickwrap/integrity/chain.rb +313 -0
- data/lib/clickwrap/integrity/timestamp.rb +143 -0
- data/lib/clickwrap/ip_geolocation/location.rb +112 -0
- data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
- data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
- data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
- data/lib/clickwrap/ip_geolocation.rb +16 -0
- data/lib/clickwrap/lifecycle.rb +534 -0
- data/lib/clickwrap/linter.rb +382 -0
- data/lib/clickwrap/localized_text.rb +101 -0
- data/lib/clickwrap/macros.rb +203 -0
- data/lib/clickwrap/models/application_record.rb +20 -0
- data/lib/clickwrap/models/chain_head.rb +79 -0
- data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
- data/lib/clickwrap/models/disposition_plan.rb +208 -0
- data/lib/clickwrap/models/document.rb +46 -0
- data/lib/clickwrap/models/document_version.rb +163 -0
- data/lib/clickwrap/models/event.rb +743 -0
- data/lib/clickwrap/models/event_document.rb +79 -0
- data/lib/clickwrap/models/event_statement.rb +92 -0
- data/lib/clickwrap/models/external_action.rb +150 -0
- data/lib/clickwrap/models/integrity_attestation.rb +90 -0
- data/lib/clickwrap/models/legal_hold.rb +81 -0
- data/lib/clickwrap/models/policy_revision.rb +115 -0
- data/lib/clickwrap/models/presentation.rb +59 -0
- data/lib/clickwrap/models/receipt_access.rb +53 -0
- data/lib/clickwrap/models/recording_sequence.rb +21 -0
- data/lib/clickwrap/models/request_evidence.rb +378 -0
- data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
- data/lib/clickwrap/models/statement_state.rb +130 -0
- data/lib/clickwrap/pending_receipt.rb +177 -0
- data/lib/clickwrap/policy.rb +283 -0
- data/lib/clickwrap/presentation_manifest.rb +210 -0
- data/lib/clickwrap/presenter.rb +716 -0
- data/lib/clickwrap/privacy.rb +419 -0
- data/lib/clickwrap/protected_outcome.rb +120 -0
- data/lib/clickwrap/receipt.rb +606 -0
- data/lib/clickwrap/receipt_html.rb +235 -0
- data/lib/clickwrap/receipt_verifier.rb +978 -0
- data/lib/clickwrap/reference.rb +44 -0
- data/lib/clickwrap/registration.rb +236 -0
- data/lib/clickwrap/registry.rb +54 -0
- data/lib/clickwrap/remediation_token.rb +155 -0
- data/lib/clickwrap/request_evidence_extractor.rb +590 -0
- data/lib/clickwrap/request_evidence_policy.rb +261 -0
- data/lib/clickwrap/retention/applier.rb +231 -0
- data/lib/clickwrap/retention/disposition.rb +221 -0
- data/lib/clickwrap/retention/planner.rb +502 -0
- data/lib/clickwrap/retention_class.rb +97 -0
- data/lib/clickwrap/reviewed_text.rb +28 -0
- data/lib/clickwrap/schema_requirements.rb +196 -0
- data/lib/clickwrap/services/authorize_external_action.rb +149 -0
- data/lib/clickwrap/services/load_policies.rb +69 -0
- data/lib/clickwrap/services/publish_documents.rb +251 -0
- data/lib/clickwrap/services/validate_policy_references.rb +166 -0
- data/lib/clickwrap/statement.rb +248 -0
- data/lib/clickwrap/subject_fingerprint.rb +28 -0
- data/lib/clickwrap/submission.rb +169 -0
- data/lib/clickwrap/system_actor.rb +31 -0
- data/lib/clickwrap/test_helpers.rb +676 -0
- data/lib/clickwrap/testing.rb +211 -0
- data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
- data/lib/clickwrap/verification.rb +504 -0
- data/lib/clickwrap/version.rb +12 -1
- data/lib/clickwrap/view_helpers.rb +190 -0
- data/lib/clickwrap/vocabulary.rb +294 -0
- data/lib/clickwrap.rb +497 -7
- data/lib/generators/clickwrap/document_generator.rb +164 -0
- data/lib/generators/clickwrap/hardening_generator.rb +177 -0
- data/lib/generators/clickwrap/install_generator.rb +1287 -0
- data/lib/generators/clickwrap/link_generator.rb +56 -0
- data/lib/generators/clickwrap/policy_generator.rb +118 -0
- data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
- data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
- data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
- data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
- data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
- data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
- data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
- data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
- data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
- data/lib/generators/clickwrap/views_generator.rb +101 -0
- data/lib/tasks/clickwrap.rake +569 -0
- metadata +311 -16
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickwrap
|
|
4
|
+
module Retention
|
|
5
|
+
# The two destructive operations in the whole gem, in one place.
|
|
6
|
+
#
|
|
7
|
+
# Clickwrap::Retention::Disposition.delete_field!(receipt, :ip_address, because: "...")
|
|
8
|
+
# Clickwrap::Retention::Disposition.dispose_core_event!(event, because: "...")
|
|
9
|
+
#
|
|
10
|
+
# Both name exactly what they remove, both require a reason in plain
|
|
11
|
+
# English, both refuse while a legal hold is in effect, and both append
|
|
12
|
+
# their own event saying what went and why. There is deliberately no
|
|
13
|
+
# `delete_personal_data!`, no `purge_network_context!`, and no method that
|
|
14
|
+
# takes out three categories at once: someone reading a disposition report
|
|
15
|
+
# a year from now has to be able to see which field disappeared, not a
|
|
16
|
+
# euphemism covering several.
|
|
17
|
+
#
|
|
18
|
+
# These back the public `Clickwrap.delete_recorded_ip_address!`,
|
|
19
|
+
# `Clickwrap.delete_recorded_browser_user_agent!`, and
|
|
20
|
+
# `Clickwrap.delete_recorded_ip_geolocation!`.
|
|
21
|
+
module Disposition
|
|
22
|
+
FIELDS = %i[ip_address browser_user_agent ip_geolocation].freeze
|
|
23
|
+
|
|
24
|
+
# Everything an IP-geolocation deletion nulls: the estimated values
|
|
25
|
+
# themselves, and nothing else.
|
|
26
|
+
#
|
|
27
|
+
# What deliberately stays is the provenance of what was there and of its
|
|
28
|
+
# removal — which provider estimated it, from which database version, how
|
|
29
|
+
# uncertain it was, when it was recorded, under which rule it was due, and
|
|
30
|
+
# when it was deleted. Those columns are not the personal estimate; they
|
|
31
|
+
# are the record that an estimate existed and was disposed of on schedule.
|
|
32
|
+
# Erasing them too would turn a documented deletion into a gap, which is
|
|
33
|
+
# the one outcome a retention process must never produce.
|
|
34
|
+
IP_GEOLOCATION_VALUE_COLUMNS = RequestEvidence::IP_GEOLOCATION_VALUE_COLUMNS
|
|
35
|
+
|
|
36
|
+
# One category, one set of columns. The raw values are the only things
|
|
37
|
+
# that go; the reader name, the trusted-proxy configuration digest, and
|
|
38
|
+
# the recorded-at timestamp explain where the deleted value came from.
|
|
39
|
+
COLUMNS_FOR_FIELD = RequestEvidence::VALUE_COLUMNS_BY_CATEGORY
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Deletes one recorded request-evidence field from the annex attached to
|
|
43
|
+
# this event, and records that it did.
|
|
44
|
+
#
|
|
45
|
+
# Returns the appended disposition event, or nil when there was nothing
|
|
46
|
+
# to delete — no annex, nothing recorded for that field, or the field
|
|
47
|
+
# already deleted. A no-op appends no event, because an event saying
|
|
48
|
+
# "deleted" where nothing was ever recorded would be false.
|
|
49
|
+
def delete_field!(receipt_or_event, field, because:)
|
|
50
|
+
field = normalize_field!(field)
|
|
51
|
+
require_reason!(because, "Deleting the recorded #{field}")
|
|
52
|
+
|
|
53
|
+
event_id = event_for(receipt_or_event).id
|
|
54
|
+
|
|
55
|
+
::ActiveRecord::Base.transaction do
|
|
56
|
+
event = Event.lock.find(event_id)
|
|
57
|
+
refuse_while_on_legal_hold!(event, "the recorded #{field}")
|
|
58
|
+
|
|
59
|
+
annex = RequestEvidence.lock.find_by(event_id: event.id)
|
|
60
|
+
return nil if annex.nil?
|
|
61
|
+
return nil if annex.deleted_for?(field)
|
|
62
|
+
return nil if annex.public_send(:"#{field}_recorded_at").nil?
|
|
63
|
+
|
|
64
|
+
status = event.request_evidence_binding_status
|
|
65
|
+
unless %i[verified disposed_with_documented_events].include?(status)
|
|
66
|
+
raise ImmutableEvidenceError,
|
|
67
|
+
"Request evidence for event #{event.id} failed its binding check (#{status}); " \
|
|
68
|
+
"Clickwrap refused to delete it because disposition must not hide an integrity problem."
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
delete_annex_field!(annex, event, field, because)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Marks a core event disposed of under its retention rule.
|
|
76
|
+
#
|
|
77
|
+
# The row stays. What changes is `core_event_disposed_at`, which is one
|
|
78
|
+
# of the named columns the Event model permits for disposition, and a linked
|
|
79
|
+
# `disposition` event that explains it. An auditor then reads a
|
|
80
|
+
# documented disposition rather than finding a hole where an agreement
|
|
81
|
+
# used to be, and verification of the surrounding chain still works.
|
|
82
|
+
def dispose_core_event!(event, because:)
|
|
83
|
+
event = event_for(event)
|
|
84
|
+
require_reason!(because, "Disposing of the core event")
|
|
85
|
+
|
|
86
|
+
::ActiveRecord::Base.transaction do
|
|
87
|
+
event = Event.lock.find(event.id)
|
|
88
|
+
return nil if event.disposed?
|
|
89
|
+
|
|
90
|
+
refuse_while_on_legal_hold!(event, "the core event")
|
|
91
|
+
disposed_at = Clickwrap.now
|
|
92
|
+
disposition = Lifecycle.append_lifecycle_event!(
|
|
93
|
+
event: event,
|
|
94
|
+
event_type: "disposition",
|
|
95
|
+
reason: "Disposed of the core event under its retention rule. #{because}",
|
|
96
|
+
extra: {
|
|
97
|
+
protected_outcome: {
|
|
98
|
+
"core_event_disposition" => {
|
|
99
|
+
"event_id" => event.id,
|
|
100
|
+
"original_event_digest" => event.event_digest,
|
|
101
|
+
"disposed_at" => Receipt.format_time(disposed_at),
|
|
102
|
+
"removed_statement_count" => event.statements.size,
|
|
103
|
+
"removed_document_binding_count" => event.documents.size,
|
|
104
|
+
"removed_fields" => core_payload_field_names
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
event.dispose_core_payload!(disposition_event: disposition, at: disposed_at)
|
|
110
|
+
disposition
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Whether disposition is currently paused for this event by a hold on
|
|
115
|
+
# the event itself, on the actor, or on the policy. All three scopes
|
|
116
|
+
# count: a hold placed on an actor's whole file is not weaker than one
|
|
117
|
+
# placed on a single receipt.
|
|
118
|
+
def legal_hold_in_effect?(event)
|
|
119
|
+
return true if event.on_legal_hold?
|
|
120
|
+
|
|
121
|
+
holds = LegalHold.in_effect
|
|
122
|
+
return true if holds.where(hold_scope: "event", event_id: event.id).exists?
|
|
123
|
+
return true if event.actor_reference.present? &&
|
|
124
|
+
holds.where(hold_scope: "actor", actor_reference: event.actor_reference).exists?
|
|
125
|
+
|
|
126
|
+
holds.where(hold_scope: "policy", policy_key: event.policy_key).exists?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Resolves a receipt, an event, or an event id to the event itself, so
|
|
130
|
+
# the public methods read naturally from either end.
|
|
131
|
+
def event_for(receipt_or_event)
|
|
132
|
+
return receipt_or_event if receipt_or_event.is_a?(Event)
|
|
133
|
+
return receipt_or_event.event if receipt_or_event.respond_to?(:event)
|
|
134
|
+
return Receipt.find(receipt_or_event.to_s).event if receipt_or_event.is_a?(String)
|
|
135
|
+
|
|
136
|
+
raise ArgumentError,
|
|
137
|
+
"Disposition needs a Clickwrap receipt, event, or event id, got " \
|
|
138
|
+
"#{receipt_or_event.class}."
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def core_payload_field_names
|
|
144
|
+
%w[
|
|
145
|
+
actor actor_reference actor_snapshot represented_party authority tenant subject
|
|
146
|
+
authentication_context idempotency_key http_request presentation protected_outcome
|
|
147
|
+
provider_receipt provider_verification reason statements document_bindings
|
|
148
|
+
]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The deletion itself, plus the event that documents it, in one
|
|
152
|
+
# transaction. Either both happen or neither does: a value that
|
|
153
|
+
# disappeared with no record of who removed it and why is exactly the
|
|
154
|
+
# thing this gem exists to prevent.
|
|
155
|
+
#
|
|
156
|
+
# Two things this deliberately does NOT touch:
|
|
157
|
+
#
|
|
158
|
+
# * the core event's columns, and
|
|
159
|
+
# * the event's `request_evidence_digest`.
|
|
160
|
+
#
|
|
161
|
+
# The event's canonical body excludes every annex value on purpose (see
|
|
162
|
+
# Clickwrap::Receipt), so an ordinary retention run cannot make a
|
|
163
|
+
# verified event stop verifying — deletion changes what a receipt can
|
|
164
|
+
# show, never what it says happened. The keyed binding digest recorded
|
|
165
|
+
# at capture stays as written; after this runs it no longer recomputes
|
|
166
|
+
# from the annex, and that is the expected, documented consequence of a
|
|
167
|
+
# permitted deletion rather than a sign of tampering. The receipt
|
|
168
|
+
# reports the field as `deleted_after_retention` with the timestamp, so
|
|
169
|
+
# a reader is told which it is.
|
|
170
|
+
def delete_annex_field!(annex, event, field, because)
|
|
171
|
+
disposed_at = Clickwrap.now
|
|
172
|
+
annex.dispose_category!(field, at: disposed_at)
|
|
173
|
+
|
|
174
|
+
Lifecycle.append_lifecycle_event!(
|
|
175
|
+
event: event,
|
|
176
|
+
event_type: "disposition",
|
|
177
|
+
reason: "Deleted the recorded #{field}. #{because}",
|
|
178
|
+
extra: {
|
|
179
|
+
protected_outcome: {
|
|
180
|
+
"request_evidence_disposition" => {
|
|
181
|
+
"category" => field.to_s,
|
|
182
|
+
"annex_id" => annex.id.to_s,
|
|
183
|
+
"disposed_at" => Receipt.format_time(disposed_at)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def normalize_field!(field)
|
|
191
|
+
normalized = field.to_s.to_sym
|
|
192
|
+
return normalized if FIELDS.include?(normalized)
|
|
193
|
+
|
|
194
|
+
raise ArgumentError,
|
|
195
|
+
"#{field.inspect} is not a request-evidence field Clickwrap can delete. " \
|
|
196
|
+
"Choose one of: #{FIELDS.join(", ")}. Each one is deleted by name so a " \
|
|
197
|
+
"disposition report can say which value went."
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def require_reason!(because, what)
|
|
201
|
+
return unless because.to_s.strip.empty?
|
|
202
|
+
|
|
203
|
+
raise LifecycleError,
|
|
204
|
+
"#{what} needs a `because:` in plain English. It is stored on the disposition " \
|
|
205
|
+
"event, and it is the only thing that will explain this deletion to someone " \
|
|
206
|
+
"reading the record years from now."
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def refuse_while_on_legal_hold!(event, what)
|
|
210
|
+
return unless legal_hold_in_effect?(event)
|
|
211
|
+
|
|
212
|
+
raise LegalHoldInEffect,
|
|
213
|
+
"A legal hold is in effect for event #{event.id}, so #{what} was not deleted. " \
|
|
214
|
+
"Release the hold with a reason and an owner first, or exclude this event from " \
|
|
215
|
+
"the disposition. A hold that scheduled deletion could quietly step over would " \
|
|
216
|
+
"not be a hold."
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|