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,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Clickwrap
6
+ # Fault injection, for this gem's suite and for yours.
7
+ #
8
+ # ===========================================================================
9
+ # The central promise of this gem is that required evidence and the protected
10
+ # database action commit together or not at all. A promise like that is worth
11
+ # exactly as much as your ability to prove it in a test, and you cannot prove
12
+ # it by reading the code — you prove it by making the evidence write fail on
13
+ # purpose and watching the account, the payout, or the withdrawal fail with
14
+ # it.
15
+ #
16
+ # Clickwrap::Testing.fail_next_event_write do
17
+ # assert_raises(Clickwrap::EventWriteFailed) { perform_signup }
18
+ # end
19
+ #
20
+ # assert_not User.exists?(email: "person@example.com")
21
+ # assert_no_clickwrap_event :signup
22
+ #
23
+ # The failure is injected INSIDE the `Clickwrap::Event` create, which means
24
+ # inside the capture's transaction, which means the host's protected action
25
+ # rolls back with it. Raising before the transaction opened would prove
26
+ # nothing at all: of course the domain action does not happen if the capture
27
+ # never started.
28
+ #
29
+ # Everything here installs on entry and removes on exit, in an `ensure`, so a
30
+ # failing assertion inside the block cannot leave a sabotage hook attached to
31
+ # `Clickwrap::Event` for the rest of the suite. Nothing here is left resident
32
+ # in a production process: the callbacks exist only while a block is running,
33
+ # and this file is only loaded if something references `Clickwrap::Testing`.
34
+ # ===========================================================================
35
+ module Testing
36
+ # Raised by `fail_next_domain_write`. Deliberately not one of the gem's
37
+ # real errors: it stands in for the host's own domain failure, and a test
38
+ # that rescued a Clickwrap error here would be testing the wrong thing.
39
+ class DomainWriteFailed < StandardError; end
40
+
41
+ EVENT_WRITE_KEY = :clickwrap_testing_fail_next_event_write
42
+ DOMAIN_WRITE_KEY = :clickwrap_testing_fail_next_domain_write
43
+ FROZEN_TIME_KEY = :clickwrap_testing_frozen_time
44
+
45
+ # The injected callbacks, held as constants so `skip_callback` can find the
46
+ # same object it was given. An anonymous lambda created per call would
47
+ # install fine and never come off.
48
+ FAIL_EVENT_WRITE = lambda do |_event|
49
+ next unless Testing.consume_flag!(EVENT_WRITE_KEY)
50
+
51
+ raise EventWriteFailed,
52
+ "Clickwrap::Testing.fail_next_event_write made this evidence write fail on purpose. " \
53
+ "Whatever your protected action did in this transaction must roll back with it — " \
54
+ "that is the property this helper exists to let you assert."
55
+ end
56
+
57
+ FAIL_DOMAIN_WRITE = lambda do |record|
58
+ next if record.is_a?(Clickwrap::ApplicationRecord)
59
+ next unless Testing.consume_flag!(DOMAIN_WRITE_KEY)
60
+
61
+ raise DomainWriteFailed,
62
+ "Clickwrap::Testing.fail_next_domain_write made this domain write fail on purpose. " \
63
+ "The Clickwrap evidence in the same transaction must roll back with it, so that a " \
64
+ "failed action never leaves behind a receipt saying it succeeded."
65
+ end
66
+
67
+ # Reads the frozen server clock, if a `freeze_time_at` block is running.
68
+ # Prepended onto `Clickwrap`'s singleton rather than redefining `now`, so
69
+ # the original method stays reachable through `super` and restoring it is
70
+ # not a matter of remembering to.
71
+ module FrozenClock
72
+ def now
73
+ Testing.frozen_time || super
74
+ end
75
+ end
76
+
77
+ class << self
78
+ # Makes the NEXT `Clickwrap::Event` insert raise
79
+ # `Clickwrap::EventWriteFailed`, from inside the create, and therefore
80
+ # from inside whatever transaction the capture is running in.
81
+ #
82
+ # Only the next one: a capture that legitimately retries, or a test that
83
+ # goes on to record a control event afterwards, is not sabotaged twice.
84
+ def fail_next_event_write
85
+ install_event_callback!
86
+ Thread.current[EVENT_WRITE_KEY] = true
87
+
88
+ yield
89
+ ensure
90
+ Thread.current[EVENT_WRITE_KEY] = nil
91
+ remove_event_callback!
92
+ end
93
+
94
+ # The mirror image: makes the next non-Clickwrap `ActiveRecord` save
95
+ # raise, so you can prove the other direction — that a domain action
96
+ # blowing up takes its evidence down with it, and never leaves a receipt
97
+ # describing something that did not happen.
98
+ def fail_next_domain_write
99
+ install_domain_callback!
100
+ Thread.current[DOMAIN_WRITE_KEY] = true
101
+
102
+ yield
103
+ ensure
104
+ Thread.current[DOMAIN_WRITE_KEY] = nil
105
+ remove_domain_callback!
106
+ end
107
+
108
+ # Freezes the server clock Clickwrap records and evaluates expiry
109
+ # against, so a test about a declaration that expired last Tuesday does
110
+ # not have to sleep until next Tuesday.
111
+ #
112
+ # It moves `Clickwrap.now` only. `Time.now` is left alone on purpose:
113
+ # this gem's evidentiary time is the value it writes into
114
+ # `recorded_at_by_server`, and a helper that quietly moved the whole
115
+ # process clock would make it much harder to tell which of the two a test
116
+ # actually depends on.
117
+ def freeze_time_at(moment)
118
+ moment = Time.parse(moment.to_s) if moment.is_a?(String)
119
+ previous = Thread.current[FROZEN_TIME_KEY]
120
+
121
+ install_clock!
122
+ Thread.current[FROZEN_TIME_KEY] = moment.utc
123
+
124
+ block_given? ? yield(moment.utc) : moment.utc
125
+ ensure
126
+ Thread.current[FROZEN_TIME_KEY] = previous
127
+ end
128
+
129
+ def frozen_time = Thread.current[FROZEN_TIME_KEY]
130
+
131
+ # Clears every flag and detaches every injected callback. Safe to call
132
+ # when nothing was ever installed, which is the point: a suite calls it
133
+ # in `setup` and in `teardown` without having to know whether the test
134
+ # that just ran used any of this.
135
+ def reset!
136
+ Thread.current[EVENT_WRITE_KEY] = nil
137
+ Thread.current[DOMAIN_WRITE_KEY] = nil
138
+ Thread.current[FROZEN_TIME_KEY] = nil
139
+
140
+ # Unconditional, and `raise: false` throughout: `reset!` is called from
141
+ # `setup` and `teardown` in suites that mostly never touch fault
142
+ # injection, and it must be a quiet no-op there rather than an
143
+ # ArgumentError about a callback nobody installed.
144
+ @event_depth = 0
145
+ @domain_depth = 0
146
+ Event.skip_callback(:create, :before, FAIL_EVENT_WRITE, raise: false)
147
+ ::ActiveRecord::Base.skip_callback(:save, :before, FAIL_DOMAIN_WRITE, raise: false)
148
+
149
+ self
150
+ end
151
+
152
+ # Reads a one-shot flag and clears it in the same breath, so the sabotage
153
+ # applies to exactly one write.
154
+ def consume_flag!(key)
155
+ return false unless Thread.current[key]
156
+
157
+ Thread.current[key] = nil
158
+ true
159
+ end
160
+
161
+ private
162
+
163
+ # Installed only while a block is active. There is no permanent
164
+ # production hook on `Clickwrap::Event`: a gem whose whole subject is
165
+ # trustworthy writes has no business leaving a callback in the write path
166
+ # whose only job is to break it.
167
+ #
168
+ # The depth counters are not ceremony. Nested blocks are ordinary in a
169
+ # suite that tests one fault inside another, and an inner `ensure` that
170
+ # detached the callback would silently disarm the outer one.
171
+ def install_event_callback!
172
+ @event_depth = @event_depth.to_i + 1
173
+ return if @event_depth > 1
174
+
175
+ Event.set_callback(:create, :before, FAIL_EVENT_WRITE)
176
+ end
177
+
178
+ def remove_event_callback!
179
+ return if @event_depth.to_i.zero?
180
+
181
+ @event_depth -= 1
182
+ return if @event_depth.positive?
183
+
184
+ Event.skip_callback(:create, :before, FAIL_EVENT_WRITE, raise: false)
185
+ end
186
+
187
+ def install_domain_callback!
188
+ @domain_depth = @domain_depth.to_i + 1
189
+ return if @domain_depth > 1
190
+
191
+ ::ActiveRecord::Base.set_callback(:save, :before, FAIL_DOMAIN_WRITE)
192
+ end
193
+
194
+ def remove_domain_callback!
195
+ return if @domain_depth.to_i.zero?
196
+
197
+ @domain_depth -= 1
198
+ return if @domain_depth.positive?
199
+
200
+ ::ActiveRecord::Base.skip_callback(:save, :before, FAIL_DOMAIN_WRITE, raise: false)
201
+ end
202
+
203
+ # `prepend` with an already-prepended module is a no-op, so this is safe
204
+ # to call from every `freeze_time_at`. The module falls straight through
205
+ # to `super` whenever no time is frozen, which is always outside a block.
206
+ def install_clock!
207
+ Clickwrap.singleton_class.prepend(FrozenClock)
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module Clickwrap
6
+ # Produces a stable digest from the effective trusted-proxy rules themselves.
7
+ # A sentence such as "Cloudflare proxies" is not configuration evidence; the
8
+ # normalized address ranges and regular expressions are.
9
+ module TrustedProxyConfiguration
10
+ SCHEMA = "clickwrap.trusted-proxy-configuration.v1"
11
+
12
+ class << self
13
+ def digest_for(trusted_proxies, source: "host_configuration")
14
+ entries = collection(trusted_proxies).map { |entry| normalize(entry) }
15
+ entries.sort_by! { |entry| CanonicalJson.generate(entry) }
16
+
17
+ payload = {
18
+ "schema" => SCHEMA,
19
+ "source" => source.to_s,
20
+ "entries" => entries
21
+ }
22
+ Digest.digest_canonical(payload)
23
+ end
24
+
25
+ def digest_for_rails_application(application)
26
+ configured = application.config.action_dispatch.trusted_proxies
27
+ if configured.nil?
28
+ require "action_dispatch/middleware/remote_ip"
29
+ configured = ActionDispatch::RemoteIp::TRUSTED_PROXIES
30
+ source = "rails_default_trusted_proxies"
31
+ else
32
+ source = "rails_application_config_action_dispatch_trusted_proxies"
33
+ end
34
+
35
+ digest_for(configured, source: source)
36
+ end
37
+
38
+ private
39
+
40
+ def collection(value)
41
+ return value.to_a if value.is_a?(Array) || value.is_a?(Set)
42
+ return [value] if proxy_entry?(value)
43
+ return value.to_a if value.respond_to?(:to_a) && !value.is_a?(Hash)
44
+
45
+ raise ConfigurationError,
46
+ "Trusted proxy configuration must be a proxy rule or a collection of proxy rules; " \
47
+ "got #{value.class}. Pass the same IP ranges or regular expressions Rails uses."
48
+ end
49
+
50
+ def proxy_entry?(value)
51
+ value.is_a?(IPAddr) || value.is_a?(Regexp) || value.is_a?(Range) ||
52
+ value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Hash)
53
+ end
54
+
55
+ def normalize(value)
56
+ case value
57
+ when IPAddr
58
+ range = value.to_range
59
+ { "type" => "ip_address_range", "first" => range.begin.to_s, "last" => range.end.to_s }
60
+ when Regexp
61
+ { "type" => "regular_expression", "source" => value.source, "options" => value.options }
62
+ when Range
63
+ {
64
+ "type" => "range",
65
+ "first" => value.begin.to_s,
66
+ "last" => value.end.to_s,
67
+ "excludes_last" => value.exclude_end?
68
+ }
69
+ when String, Symbol
70
+ { "type" => "literal", "value" => value.to_s }
71
+ when Hash
72
+ normalize_hash(value)
73
+ else
74
+ raise ConfigurationError,
75
+ "Trusted proxy rule #{value.inspect} (#{value.class}) cannot be serialized " \
76
+ "deterministically. Use explicit IPAddr ranges, regular expressions, ranges, " \
77
+ "strings, or a canonical Hash describing the effective rule."
78
+ end
79
+ end
80
+
81
+ def normalize_hash(value)
82
+ normalized = value.deep_stringify_keys
83
+ CanonicalJson.generate(normalized)
84
+ { "type" => "configuration", "value" => normalized }
85
+ rescue CanonicalJson::SerializationError => error
86
+ raise ConfigurationError,
87
+ "Trusted proxy configuration contains a value that cannot be canonicalized: " \
88
+ "#{error.message}"
89
+ end
90
+ end
91
+ end
92
+ end