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.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +90 -0
  3. data/CHANGELOG.md +612 -0
  4. data/README.md +830 -1204
  5. data/SECURITY.md +33 -0
  6. data/app/assets/stylesheets/clickwrap.css +241 -0
  7. data/app/controllers/clickwrap/application_controller.rb +79 -0
  8. data/app/controllers/clickwrap/captures_controller.rb +145 -0
  9. data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
  10. data/app/controllers/clickwrap/receipts_controller.rb +115 -0
  11. data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
  12. data/app/helpers/clickwrap/engine_helper.rb +97 -0
  13. data/app/views/clickwrap/captures/show.html.erb +34 -0
  14. data/app/views/clickwrap/receipts/index.html.erb +38 -0
  15. data/app/views/clickwrap/receipts/show.html.erb +91 -0
  16. data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
  17. data/app/views/clickwrap/shared/_fields.html.erb +100 -0
  18. data/app/views/clickwrap/shared/_statement.html.erb +105 -0
  19. data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
  20. data/config/locales/en.yml +160 -0
  21. data/config/locales/es.yml +138 -0
  22. data/config/routes.rb +41 -0
  23. data/exe/clickwrap +374 -0
  24. data/guides/README.md +30 -0
  25. data/guides/accessibility.md +249 -0
  26. data/guides/consent-and-lifecycle.md +299 -0
  27. data/guides/integrating.md +610 -0
  28. data/guides/integrity.md +212 -0
  29. data/guides/migrating.md +335 -0
  30. data/guides/naming.md +320 -0
  31. data/guides/organizations.md +320 -0
  32. data/guides/receipts-and-verification.md +415 -0
  33. data/guides/request-evidence.md +512 -0
  34. data/guides/retention-and-legal-holds.md +438 -0
  35. data/lib/clickwrap/actor_proxy.rb +147 -0
  36. data/lib/clickwrap/anonymous_actor.rb +47 -0
  37. data/lib/clickwrap/authority.rb +174 -0
  38. data/lib/clickwrap/canonical_json.rb +216 -0
  39. data/lib/clickwrap/capture/event_builder.rb +220 -0
  40. data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
  41. data/lib/clickwrap/capture.rb +650 -0
  42. data/lib/clickwrap/configuration.rb +1129 -0
  43. data/lib/clickwrap/controller_helpers.rb +758 -0
  44. data/lib/clickwrap/current_state.rb +282 -0
  45. data/lib/clickwrap/digest.rb +125 -0
  46. data/lib/clickwrap/doctor.rb +418 -0
  47. data/lib/clickwrap/document_definition.rb +255 -0
  48. data/lib/clickwrap/document_renderer.rb +83 -0
  49. data/lib/clickwrap/document_renderers/markdown.rb +175 -0
  50. data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
  51. data/lib/clickwrap/dsl/policy_builder.rb +462 -0
  52. data/lib/clickwrap/dsl/retention_builder.rb +89 -0
  53. data/lib/clickwrap/durable_commit_callback.rb +37 -0
  54. data/lib/clickwrap/engine.rb +184 -0
  55. data/lib/clickwrap/errors.rb +181 -0
  56. data/lib/clickwrap/form_builder_extensions.rb +341 -0
  57. data/lib/clickwrap/front_matter.rb +67 -0
  58. data/lib/clickwrap/identifier.rb +112 -0
  59. data/lib/clickwrap/import/external_receipt.rb +241 -0
  60. data/lib/clickwrap/import/fine_print.rb +290 -0
  61. data/lib/clickwrap/import/legacy.rb +450 -0
  62. data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
  63. data/lib/clickwrap/integrity/anchor.rb +130 -0
  64. data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
  65. data/lib/clickwrap/integrity/attestor.rb +221 -0
  66. data/lib/clickwrap/integrity/chain.rb +313 -0
  67. data/lib/clickwrap/integrity/timestamp.rb +143 -0
  68. data/lib/clickwrap/ip_geolocation/location.rb +112 -0
  69. data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
  70. data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
  71. data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
  72. data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
  73. data/lib/clickwrap/ip_geolocation.rb +16 -0
  74. data/lib/clickwrap/lifecycle.rb +534 -0
  75. data/lib/clickwrap/linter.rb +382 -0
  76. data/lib/clickwrap/localized_text.rb +101 -0
  77. data/lib/clickwrap/macros.rb +203 -0
  78. data/lib/clickwrap/models/application_record.rb +20 -0
  79. data/lib/clickwrap/models/chain_head.rb +79 -0
  80. data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
  81. data/lib/clickwrap/models/disposition_plan.rb +208 -0
  82. data/lib/clickwrap/models/document.rb +46 -0
  83. data/lib/clickwrap/models/document_version.rb +163 -0
  84. data/lib/clickwrap/models/event.rb +743 -0
  85. data/lib/clickwrap/models/event_document.rb +79 -0
  86. data/lib/clickwrap/models/event_statement.rb +92 -0
  87. data/lib/clickwrap/models/external_action.rb +150 -0
  88. data/lib/clickwrap/models/integrity_attestation.rb +90 -0
  89. data/lib/clickwrap/models/legal_hold.rb +81 -0
  90. data/lib/clickwrap/models/policy_revision.rb +115 -0
  91. data/lib/clickwrap/models/presentation.rb +59 -0
  92. data/lib/clickwrap/models/receipt_access.rb +53 -0
  93. data/lib/clickwrap/models/recording_sequence.rb +21 -0
  94. data/lib/clickwrap/models/request_evidence.rb +378 -0
  95. data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
  96. data/lib/clickwrap/models/statement_state.rb +130 -0
  97. data/lib/clickwrap/pending_receipt.rb +177 -0
  98. data/lib/clickwrap/policy.rb +283 -0
  99. data/lib/clickwrap/presentation_manifest.rb +210 -0
  100. data/lib/clickwrap/presenter.rb +716 -0
  101. data/lib/clickwrap/privacy.rb +419 -0
  102. data/lib/clickwrap/protected_outcome.rb +120 -0
  103. data/lib/clickwrap/receipt.rb +606 -0
  104. data/lib/clickwrap/receipt_html.rb +235 -0
  105. data/lib/clickwrap/receipt_verifier.rb +978 -0
  106. data/lib/clickwrap/reference.rb +44 -0
  107. data/lib/clickwrap/registration.rb +236 -0
  108. data/lib/clickwrap/registry.rb +54 -0
  109. data/lib/clickwrap/remediation_token.rb +155 -0
  110. data/lib/clickwrap/request_evidence_extractor.rb +590 -0
  111. data/lib/clickwrap/request_evidence_policy.rb +261 -0
  112. data/lib/clickwrap/retention/applier.rb +231 -0
  113. data/lib/clickwrap/retention/disposition.rb +221 -0
  114. data/lib/clickwrap/retention/planner.rb +502 -0
  115. data/lib/clickwrap/retention_class.rb +97 -0
  116. data/lib/clickwrap/reviewed_text.rb +28 -0
  117. data/lib/clickwrap/schema_requirements.rb +196 -0
  118. data/lib/clickwrap/services/authorize_external_action.rb +149 -0
  119. data/lib/clickwrap/services/load_policies.rb +69 -0
  120. data/lib/clickwrap/services/publish_documents.rb +251 -0
  121. data/lib/clickwrap/services/validate_policy_references.rb +166 -0
  122. data/lib/clickwrap/statement.rb +248 -0
  123. data/lib/clickwrap/subject_fingerprint.rb +28 -0
  124. data/lib/clickwrap/submission.rb +169 -0
  125. data/lib/clickwrap/system_actor.rb +31 -0
  126. data/lib/clickwrap/test_helpers.rb +676 -0
  127. data/lib/clickwrap/testing.rb +211 -0
  128. data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
  129. data/lib/clickwrap/verification.rb +504 -0
  130. data/lib/clickwrap/version.rb +12 -1
  131. data/lib/clickwrap/view_helpers.rb +190 -0
  132. data/lib/clickwrap/vocabulary.rb +294 -0
  133. data/lib/clickwrap.rb +497 -7
  134. data/lib/generators/clickwrap/document_generator.rb +164 -0
  135. data/lib/generators/clickwrap/hardening_generator.rb +177 -0
  136. data/lib/generators/clickwrap/install_generator.rb +1287 -0
  137. data/lib/generators/clickwrap/link_generator.rb +56 -0
  138. data/lib/generators/clickwrap/policy_generator.rb +118 -0
  139. data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
  140. data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
  141. data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
  142. data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
  143. data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
  144. data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
  145. data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
  146. data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
  147. data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
  148. data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
  149. data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
  150. data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
  151. data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
  152. data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
  153. data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
  154. data/lib/generators/clickwrap/views_generator.rb +101 -0
  155. data/lib/tasks/clickwrap.rake +569 -0
  156. 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