clickwrap 0.0.0 → 0.1.1
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 +650 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +245 -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 +154 -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 +187 -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 +729 -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,462 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickwrap
|
|
4
|
+
module DSL
|
|
5
|
+
# The block passed to `Clickwrap.policy`.
|
|
6
|
+
#
|
|
7
|
+
# Clickwrap.policy :signup do
|
|
8
|
+
# agree_to :terms
|
|
9
|
+
# acknowledge :privacy_notice
|
|
10
|
+
# retain_with :ordinary_agreement_evidence
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# The verbs are deliberate. `agree_to :terms` and `consent_to :marketing`
|
|
14
|
+
# are different sentences because they are different acts with different
|
|
15
|
+
# lifecycles, and a developer choosing between them is doing the one piece
|
|
16
|
+
# of thinking this gem cannot do for them.
|
|
17
|
+
class PolicyBuilder
|
|
18
|
+
STATEMENT_OPTIONS = %i[
|
|
19
|
+
document
|
|
20
|
+
statement
|
|
21
|
+
label
|
|
22
|
+
link_label
|
|
23
|
+
choices
|
|
24
|
+
purpose
|
|
25
|
+
withdrawal_path
|
|
26
|
+
valid_for
|
|
27
|
+
requires
|
|
28
|
+
subject_fingerprint_with
|
|
29
|
+
subject_fingerprint_version
|
|
30
|
+
record_protected_outcome_with
|
|
31
|
+
protected_outcome_version
|
|
32
|
+
optional
|
|
33
|
+
require_an_explicit_choice
|
|
34
|
+
one_time
|
|
35
|
+
require_current_version
|
|
36
|
+
].freeze
|
|
37
|
+
DEFAULT_RETENTION_SETTING_NAMES = {
|
|
38
|
+
ip_address: :delete_recorded_ip_addresses_after,
|
|
39
|
+
browser_user_agent: :delete_recorded_browser_user_agents_after,
|
|
40
|
+
ip_geolocation: :delete_recorded_ip_geolocation_after
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
def initialize(key)
|
|
44
|
+
@key = key.to_s
|
|
45
|
+
@statements = []
|
|
46
|
+
@retention_class_key = nil
|
|
47
|
+
@request_evidence = {}
|
|
48
|
+
@ip_geolocation_fields = {}
|
|
49
|
+
@persist_presentations_for = nil
|
|
50
|
+
@persist_presentations_because = nil
|
|
51
|
+
@capture_channels = nil
|
|
52
|
+
@locales = nil
|
|
53
|
+
@tenant_scope = "optional"
|
|
54
|
+
@options = {}
|
|
55
|
+
@authority_rule = nil
|
|
56
|
+
@ip_geolocation_resolver_name = nil
|
|
57
|
+
@review_request_evidence_configuration_on = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# --- The six verbs --------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
# Assent to contractual terms.
|
|
63
|
+
def agree_to(statement_key, **options)
|
|
64
|
+
add_statement(statement_key, "agreement", options)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Affirmative receipt or awareness of a notice or risk. This is not
|
|
68
|
+
# permission, and it is not consent: a privacy notice is information the
|
|
69
|
+
# person is entitled to, not something they agree to.
|
|
70
|
+
def acknowledge(statement_key, **options)
|
|
71
|
+
add_statement(statement_key, "acknowledgment", options)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Purpose-specific permission, where the host has decided consent is the
|
|
75
|
+
# right basis for this processing. Clickwrap does not make that decision;
|
|
76
|
+
# it makes the grant, the withdrawal, and the scope demonstrable.
|
|
77
|
+
def consent_to(statement_key, **options)
|
|
78
|
+
add_statement(statement_key, "consent", options)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A factual statement made by the actor. A declaration can expire without
|
|
82
|
+
# implying it was false when it was made.
|
|
83
|
+
def declare(statement_key, **options)
|
|
84
|
+
add_statement(statement_key, "declaration", options)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# An operational fact affirmed by an authorized actor, usually an
|
|
88
|
+
# operator rather than an end user.
|
|
89
|
+
def attest(statement_key, **options)
|
|
90
|
+
add_statement(statement_key, "attestation", options)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Narrow permission bound to one protected action. This is the difference
|
|
94
|
+
# between "the user once accepted something" and "this exact evidence
|
|
95
|
+
# authorized this exact operation".
|
|
96
|
+
def authorize(statement_key, **options)
|
|
97
|
+
add_statement(statement_key, "authorization", options)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# --- Policy-level settings ------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def retain_with(retention_class_key)
|
|
103
|
+
@retention_class_key = retention_class_key.to_s
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Keep the presentation manifest for renders that were never submitted.
|
|
107
|
+
# The default path writes nothing on GET; this trades that for a record
|
|
108
|
+
# of display attempts, which some high-assurance flows want. An abandoned
|
|
109
|
+
# GET is labeled `presented_by_server` — never `accepted`, and never
|
|
110
|
+
# `seen_by_human`.
|
|
111
|
+
def persist_presentations_before_submission_for(duration, because: nil, **unknown_options)
|
|
112
|
+
refuse_unknown_options!("persist_presentations_before_submission_for", unknown_options)
|
|
113
|
+
@persist_presentations_for = duration
|
|
114
|
+
@persist_presentations_because = because
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Restrict which capture channels this policy accepts. By default all are
|
|
118
|
+
# allowed and the channel is recorded; a policy that should only ever be
|
|
119
|
+
# completed in a browser can say so.
|
|
120
|
+
def only_capture_from(*channels)
|
|
121
|
+
@capture_channels = channels.flatten.map(&:to_s)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Restrict the policy to locales it can actually present. A required
|
|
125
|
+
# legal statement should not fall back to a language nobody chose.
|
|
126
|
+
def only_present_in(*locales)
|
|
127
|
+
@locales = locales.flatten.map(&:to_s)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Declares whether this policy is personal, tenant-bound, or deliberately
|
|
131
|
+
# usable in either context. The same policy-level decision is applied to
|
|
132
|
+
# presentation, capture, and verification, so ambient organization state
|
|
133
|
+
# cannot appear on only one side of a signed submission.
|
|
134
|
+
#
|
|
135
|
+
# tenant_is :not_applicable # personal evidence; ignore ambient tenant
|
|
136
|
+
# tenant_is :required # every call must resolve a tenant
|
|
137
|
+
# tenant_is :optional # either context is deliberate (the default)
|
|
138
|
+
def tenant_is(scope)
|
|
139
|
+
@tenant_scope = scope.to_s
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Allow explicitly recorded system exemptions for this policy. Even when
|
|
143
|
+
# allowed, an exemption never answers `agreed_to?` — it answers
|
|
144
|
+
# `exempted_from?`. There is no "missing checkbox means system account"
|
|
145
|
+
# inference anywhere in this gem.
|
|
146
|
+
def permit_exemptions(because: nil, **unknown_options)
|
|
147
|
+
refuse_unknown_options!("permit_exemptions", unknown_options)
|
|
148
|
+
@options[:permit_exemptions] = true
|
|
149
|
+
@options[:permit_exemptions_because] = because
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Allow an actor to act for a represented party (an employee signing for
|
|
153
|
+
# an organization, a guardian, a service account). The receipt keeps the
|
|
154
|
+
# authenticated principal, the asserted actor, and the represented party
|
|
155
|
+
# as three separate facts. Clickwrap does not decide whether the
|
|
156
|
+
# authority is sufficient.
|
|
157
|
+
def permit_acting_for(*represented_party_types, using: :host,
|
|
158
|
+
when_actor_is_at_least: nil, when_actor_has_permission: nil,
|
|
159
|
+
including_when_this_action_creates_the_represented_party: false,
|
|
160
|
+
**unknown_options)
|
|
161
|
+
refuse_unknown_options!("permit_acting_for", unknown_options)
|
|
162
|
+
if represented_party_types.compact.all? { |type| type.to_s.strip.empty? }
|
|
163
|
+
raise DefinitionError,
|
|
164
|
+
"permit_acting_for needs at least one represented-party class name. " \
|
|
165
|
+
"Name every type this policy permits so it cannot authorize an unexpected kind of record."
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
@options[:permit_acting_for] = true
|
|
169
|
+
@authority_rule = AuthorityRule.new(
|
|
170
|
+
represented_party_types: represented_party_types,
|
|
171
|
+
adapter_name: using,
|
|
172
|
+
minimum_role: when_actor_is_at_least,
|
|
173
|
+
required_permission: when_actor_has_permission,
|
|
174
|
+
allow_represented_party_creation:
|
|
175
|
+
including_when_this_action_creates_the_represented_party
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# One-line integration with https://github.com/rameerez/organizations.
|
|
180
|
+
# The User remains the human actor; Organizations::Organization is the
|
|
181
|
+
# represented party. A policy must name at least one reviewed authority
|
|
182
|
+
# criterion rather than silently treating every member as able to bind it.
|
|
183
|
+
#
|
|
184
|
+
# permit_acting_for_organization when_actor_is_at_least: :admin
|
|
185
|
+
#
|
|
186
|
+
def permit_acting_for_organization(when_actor_is_at_least: nil,
|
|
187
|
+
when_actor_has_permission: nil,
|
|
188
|
+
including_when_this_action_creates_the_organization: false,
|
|
189
|
+
**unknown_options)
|
|
190
|
+
refuse_unknown_options!("permit_acting_for_organization", unknown_options)
|
|
191
|
+
if when_actor_is_at_least.nil? && when_actor_has_permission.nil?
|
|
192
|
+
raise DefinitionError,
|
|
193
|
+
"permit_acting_for_organization needs `when_actor_is_at_least:` or " \
|
|
194
|
+
"`when_actor_has_permission:`. Organization membership alone does not establish " \
|
|
195
|
+
"legal authority to bind the organization."
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
permit_acting_for(
|
|
199
|
+
"Organizations::Organization",
|
|
200
|
+
using: :organizations_membership,
|
|
201
|
+
when_actor_is_at_least:,
|
|
202
|
+
when_actor_has_permission:,
|
|
203
|
+
including_when_this_action_creates_the_represented_party:
|
|
204
|
+
including_when_this_action_creates_the_organization
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# --- Request evidence -----------------------------------------------------
|
|
209
|
+
|
|
210
|
+
def record_ip_address(encrypted: nil, delete_after: nil, retain_until: nil,
|
|
211
|
+
fail_if_unavailable: false, because: nil, legal_basis_reference: nil,
|
|
212
|
+
**unknown_options)
|
|
213
|
+
refuse_unknown_options!("record_ip_address", unknown_options)
|
|
214
|
+
@request_evidence[:ip_address] = RequestEvidencePolicy::Setting.new(
|
|
215
|
+
record: true, encrypted:, delete_after:, retain_until:, fail_if_unavailable:,
|
|
216
|
+
because:, legal_basis_reference:
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# A policy-level refusal wins over an application-wide default. This is a
|
|
221
|
+
# named method rather than `record: false`: a privacy-reducing decision
|
|
222
|
+
# should read unambiguously in review and must not be confused with an
|
|
223
|
+
# omitted option.
|
|
224
|
+
def do_not_record_ip_address
|
|
225
|
+
@request_evidence[:ip_address] = RequestEvidencePolicy::NOT_RECORDED
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def record_browser_user_agent(encrypted: nil, delete_after: nil, retain_until: nil,
|
|
229
|
+
fail_if_unavailable: false, because: nil,
|
|
230
|
+
legal_basis_reference: nil, **unknown_options)
|
|
231
|
+
refuse_unknown_options!("record_browser_user_agent", unknown_options)
|
|
232
|
+
@request_evidence[:browser_user_agent] = RequestEvidencePolicy::Setting.new(
|
|
233
|
+
record: true, encrypted:, delete_after:, retain_until:, fail_if_unavailable:,
|
|
234
|
+
because:, legal_basis_reference:
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def do_not_record_browser_user_agent
|
|
239
|
+
@request_evidence[:browser_user_agent] = RequestEvidencePolicy::NOT_RECORDED
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Each IP-geolocation data field is named separately, because each one is
|
|
243
|
+
# a separate decision about what to keep about a person's network
|
|
244
|
+
# context. `latitude_and_longitude` is one coupled choice: half a
|
|
245
|
+
# coordinate is not a result. Whatever is enabled, the provider name,
|
|
246
|
+
# source, estimated status, resolution time, and any accuracy or database
|
|
247
|
+
# provenance the resolver supplies are stored with it automatically — a
|
|
248
|
+
# policy cannot keep the coordinates and drop the uncertainty needed to
|
|
249
|
+
# read them.
|
|
250
|
+
def record_ip_geolocation(country: false, region: false, city: false, postal_code: false,
|
|
251
|
+
latitude_and_longitude: false, timezone: false, continent: false,
|
|
252
|
+
metro_code: false, accuracy_radius_in_kilometers: false,
|
|
253
|
+
using: nil, encrypted: nil, delete_after: nil, retain_until: nil,
|
|
254
|
+
fail_if_unavailable: false, because: nil,
|
|
255
|
+
legal_basis_reference: nil,
|
|
256
|
+
data_protection_impact_assessment_reference: nil,
|
|
257
|
+
**unknown_options)
|
|
258
|
+
refuse_unknown_options!("record_ip_geolocation", unknown_options)
|
|
259
|
+
@ip_geolocation_fields = {
|
|
260
|
+
"country" => country,
|
|
261
|
+
"region" => region,
|
|
262
|
+
"city" => city,
|
|
263
|
+
"postal_code" => postal_code,
|
|
264
|
+
"latitude_and_longitude" => latitude_and_longitude,
|
|
265
|
+
"timezone" => timezone,
|
|
266
|
+
"continent" => continent,
|
|
267
|
+
"metro_code" => metro_code,
|
|
268
|
+
"accuracy_radius_in_kilometers" => accuracy_radius_in_kilometers
|
|
269
|
+
}
|
|
270
|
+
@ip_geolocation_resolver_name = using
|
|
271
|
+
|
|
272
|
+
@request_evidence[:ip_geolocation] = RequestEvidencePolicy::Setting.new(
|
|
273
|
+
record: true, encrypted:, delete_after:, retain_until:, fail_if_unavailable:,
|
|
274
|
+
because:, legal_basis_reference:, data_protection_impact_assessment_reference:
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def do_not_record_ip_geolocation
|
|
279
|
+
@request_evidence[:ip_geolocation] = RequestEvidencePolicy::NOT_RECORDED
|
|
280
|
+
@ip_geolocation_fields = {}
|
|
281
|
+
@ip_geolocation_resolver_name = nil
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# A date by which someone should look at this policy's request-evidence
|
|
285
|
+
# configuration again. `clickwrap:doctor` reports policies that collect
|
|
286
|
+
# personal data without one, and policies whose date has passed.
|
|
287
|
+
def review_request_evidence_configuration_on(date)
|
|
288
|
+
@review_request_evidence_configuration_on = date
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# --- Compilation ----------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def compile
|
|
294
|
+
Policy.new(
|
|
295
|
+
key: @key,
|
|
296
|
+
statements: @statements,
|
|
297
|
+
retention_class_key: @retention_class_key,
|
|
298
|
+
request_evidence: build_request_evidence_policy,
|
|
299
|
+
persist_presentations_for: @persist_presentations_for,
|
|
300
|
+
persist_presentations_because: @persist_presentations_because,
|
|
301
|
+
capture_channels: @capture_channels,
|
|
302
|
+
locales: @locales,
|
|
303
|
+
tenant_scope: @tenant_scope,
|
|
304
|
+
authority_rule: @authority_rule,
|
|
305
|
+
options: @options
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
private
|
|
310
|
+
|
|
311
|
+
def add_statement(statement_key, kind, options)
|
|
312
|
+
refuse_unknown_statement_options!(statement_key, options)
|
|
313
|
+
@statements << Statement.new(
|
|
314
|
+
key: statement_key,
|
|
315
|
+
kind: kind,
|
|
316
|
+
ordinal: @statements.length,
|
|
317
|
+
options: default_statement_options(statement_key, kind).merge(options)
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# A statement's document defaults to a document with the same key, and
|
|
322
|
+
# its assertion defaults to a conventional I18n key. Both are overridable;
|
|
323
|
+
# the defaults exist so the five-minute path needs no boilerplate, not so
|
|
324
|
+
# that anything is guessed silently — a missing document or translation
|
|
325
|
+
# still fails loudly.
|
|
326
|
+
def default_statement_options(statement_key, kind)
|
|
327
|
+
{
|
|
328
|
+
document: statement_key,
|
|
329
|
+
statement: :"clickwrap.statements.#{kind}.#{statement_key}"
|
|
330
|
+
}
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def build_request_evidence_policy
|
|
334
|
+
ip_address = resolved_request_evidence_setting(:ip_address)
|
|
335
|
+
browser_user_agent = resolved_request_evidence_setting(:browser_user_agent)
|
|
336
|
+
ip_geolocation = resolved_request_evidence_setting(:ip_geolocation)
|
|
337
|
+
geolocation_fields = resolved_ip_geolocation_fields(ip_geolocation)
|
|
338
|
+
|
|
339
|
+
RequestEvidencePolicy.new(
|
|
340
|
+
policy_key: @key,
|
|
341
|
+
retention_class_key: @retention_class_key,
|
|
342
|
+
ip_address:,
|
|
343
|
+
browser_user_agent:,
|
|
344
|
+
ip_geolocation:,
|
|
345
|
+
ip_geolocation_fields: geolocation_fields,
|
|
346
|
+
ip_geolocation_resolver_name: resolved_ip_geolocation_resolver_name(ip_geolocation),
|
|
347
|
+
trusted_proxy_configuration_digest: resolved_trusted_proxy_configuration_digest(
|
|
348
|
+
ip_address:, ip_geolocation:
|
|
349
|
+
),
|
|
350
|
+
review_configuration_on: @review_request_evidence_configuration_on ||
|
|
351
|
+
Clickwrap.config.review_default_request_evidence_configuration_on
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def resolved_request_evidence_setting(category)
|
|
356
|
+
return @request_evidence.fetch(category) if @request_evidence.key?(category)
|
|
357
|
+
|
|
358
|
+
application_default_setting(category)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def application_default_setting(category)
|
|
362
|
+
enabled = case category
|
|
363
|
+
when :ip_address then Clickwrap.config.record_ip_address_by_default
|
|
364
|
+
when :browser_user_agent then Clickwrap.config.record_browser_user_agent_by_default
|
|
365
|
+
else Clickwrap.config.enabled_default_ip_geolocation_fields.any?
|
|
366
|
+
end
|
|
367
|
+
return RequestEvidencePolicy::NOT_RECORDED unless enabled
|
|
368
|
+
|
|
369
|
+
RequestEvidencePolicy::Setting.new(
|
|
370
|
+
record: true,
|
|
371
|
+
encrypted: application_default_encryption(category),
|
|
372
|
+
delete_after: Clickwrap.config.public_send(
|
|
373
|
+
DEFAULT_RETENTION_SETTING_NAMES.fetch(category)
|
|
374
|
+
),
|
|
375
|
+
fail_if_unavailable: category == :ip_geolocation &&
|
|
376
|
+
Clickwrap.config.fail_capture_when_ip_geolocation_is_unavailable,
|
|
377
|
+
because: application_default_reason(category),
|
|
378
|
+
legal_basis_reference: application_default_legal_basis_reference(category)
|
|
379
|
+
)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def application_default_encryption(category)
|
|
383
|
+
case category
|
|
384
|
+
when :ip_address then Clickwrap.config.encrypt_recorded_ip_addresses
|
|
385
|
+
when :browser_user_agent then Clickwrap.config.encrypt_recorded_browser_user_agents
|
|
386
|
+
else Clickwrap.config.encrypt_recorded_ip_geolocation
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def application_default_reason(category)
|
|
391
|
+
case category
|
|
392
|
+
when :ip_address then Clickwrap.config.reason_for_recording_ip_addresses_by_default
|
|
393
|
+
when :browser_user_agent then Clickwrap.config.reason_for_recording_browser_user_agents_by_default
|
|
394
|
+
else Clickwrap.config.reason_for_recording_ip_geolocation_by_default
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def application_default_legal_basis_reference(category)
|
|
399
|
+
case category
|
|
400
|
+
when :ip_address then Clickwrap.config.legal_basis_reference_for_recording_ip_addresses_by_default
|
|
401
|
+
when :browser_user_agent
|
|
402
|
+
Clickwrap.config.legal_basis_reference_for_recording_browser_user_agents_by_default
|
|
403
|
+
else Clickwrap.config.legal_basis_reference_for_recording_ip_geolocation_by_default
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def resolved_ip_geolocation_fields(setting)
|
|
408
|
+
return {} unless setting.record?
|
|
409
|
+
return @ip_geolocation_fields if @request_evidence.key?(:ip_geolocation)
|
|
410
|
+
|
|
411
|
+
Vocabulary::IP_GEOLOCATION_DATA_FIELDS.to_h do |field|
|
|
412
|
+
[field, Clickwrap.config.public_send(:"record_ip_geolocation_#{field}_by_default")]
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def resolved_ip_geolocation_resolver_name(setting)
|
|
417
|
+
return nil unless setting.record?
|
|
418
|
+
|
|
419
|
+
@ip_geolocation_resolver_name || :application_default
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def resolved_trusted_proxy_configuration_digest(ip_address:, ip_geolocation:)
|
|
423
|
+
return nil unless ip_address.record? || ip_geolocation.record?
|
|
424
|
+
|
|
425
|
+
Clickwrap.config.trusted_proxy_configuration_digest
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# No table of former spellings. `documents:`, `assertion:`, and
|
|
429
|
+
# `link_labels:` were renamed before this gem was ever released, so there
|
|
430
|
+
# is no application anywhere that wrote them — and carrying a migration
|
|
431
|
+
# note for a migration nobody had to make invents a history the gem does
|
|
432
|
+
# not have. The full list of supported options is the answer.
|
|
433
|
+
def refuse_unknown_statement_options!(statement_key, options)
|
|
434
|
+
unknown = options.keys.map(&:to_sym) - STATEMENT_OPTIONS
|
|
435
|
+
return if unknown.empty?
|
|
436
|
+
|
|
437
|
+
raise DefinitionError,
|
|
438
|
+
"Statement #{statement_key} in policy #{@key} has unknown option#{"s" if unknown.many?} " \
|
|
439
|
+
"#{unknown.map { |option| "`#{option}:`" }.join(", ")}. " \
|
|
440
|
+
"Supported options are: #{STATEMENT_OPTIONS.map { |option| "`#{option}:`" }.join(", ")}. " \
|
|
441
|
+
"Clickwrap never ignores policy options."
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def refuse_unknown_options!(method_name, options)
|
|
445
|
+
return if options.empty?
|
|
446
|
+
|
|
447
|
+
raise DefinitionError,
|
|
448
|
+
"Policy #{@key} calls `#{method_name}` with unknown option" \
|
|
449
|
+
"#{"s" if options.many?} #{options.keys.map { |option| "`#{option}:`" }.join(", ")}. " \
|
|
450
|
+
"Check the spelling; Clickwrap never ignores policy options."
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def method_missing(name, *_arguments, **_options)
|
|
454
|
+
raise DefinitionError,
|
|
455
|
+
"Policy #{@key} calls unknown DSL method `#{name}`. Check the spelling; " \
|
|
456
|
+
"Clickwrap never ignores policy declarations."
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def respond_to_missing?(_name, _include_private = false) = false
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickwrap
|
|
4
|
+
module DSL
|
|
5
|
+
# The block passed to `Clickwrap.retention`.
|
|
6
|
+
#
|
|
7
|
+
# Clickwrap.retention :ordinary_agreement_evidence do
|
|
8
|
+
# retain_core_event_for 6.years
|
|
9
|
+
# delete_recorded_ip_address_after 90.days
|
|
10
|
+
# delete_recorded_browser_user_agent_after 90.days
|
|
11
|
+
# delete_recorded_ip_geolocation_after 90.days
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Two vocabularies on purpose. `retain_..._for` and `retain_..._until` say
|
|
15
|
+
# how long evidence is kept; `delete_..._after` says when personal request
|
|
16
|
+
# evidence goes away. They read differently because they are different
|
|
17
|
+
# intentions, and the second one is the destructive one.
|
|
18
|
+
class RetentionBuilder
|
|
19
|
+
def initialize(key)
|
|
20
|
+
@key = key.to_s
|
|
21
|
+
@rules = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# --- The core event -------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
def retain_core_event_for(duration)
|
|
27
|
+
assign_rule!(:core_event, duration:)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# For obligations a duration cannot express — "five years, or three years
|
|
31
|
+
# after this contract is liquidated, whichever is later". The named
|
|
32
|
+
# calculation is registered by the host on the configuration object, and
|
|
33
|
+
# it may legitimately return nil while the triggering event has not
|
|
34
|
+
# happened, in which case the record is simply not due yet.
|
|
35
|
+
def retain_core_event_until(host_event_name)
|
|
36
|
+
assign_rule!(:core_event, host_event_name:)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# --- Optional request evidence -------------------------------------------
|
|
40
|
+
|
|
41
|
+
def delete_recorded_ip_address_after(duration)
|
|
42
|
+
assign_rule!(:ip_address, duration:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete_recorded_browser_user_agent_after(duration)
|
|
46
|
+
assign_rule!(:browser_user_agent, duration:)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def delete_recorded_ip_geolocation_after(duration)
|
|
50
|
+
assign_rule!(:ip_geolocation, duration:)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def retain_recorded_ip_address_until(host_event_name)
|
|
54
|
+
assign_rule!(:ip_address, host_event_name:)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def retain_recorded_browser_user_agent_until(host_event_name)
|
|
58
|
+
assign_rule!(:browser_user_agent, host_event_name:)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def retain_recorded_ip_geolocation_until(host_event_name)
|
|
62
|
+
assign_rule!(:ip_geolocation, host_event_name:)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def compile = RetentionClass.new(key: @key, rules: @rules)
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def assign_rule!(part, duration: nil, host_event_name: nil)
|
|
70
|
+
if @rules.key?(part)
|
|
71
|
+
raise DefinitionError,
|
|
72
|
+
"Retention class #{@key} declares #{part} more than once. Keep one reviewed " \
|
|
73
|
+
"rule for each part; Clickwrap will not let line order silently replace a " \
|
|
74
|
+
"deletion deadline."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@rules[part] = RetentionClass::Rule.new(part:, duration:, host_event_name:)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def method_missing(name, *_arguments, **_options)
|
|
81
|
+
raise DefinitionError,
|
|
82
|
+
"Retention class #{@key} calls unknown DSL method `#{name}`. Check the spelling; " \
|
|
83
|
+
"Clickwrap never ignores retention declarations."
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def respond_to_missing?(_name, _include_private = false) = false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickwrap
|
|
4
|
+
# Rails normally defers a record's `after_commit` callback to the outermost
|
|
5
|
+
# joinable transaction. A non-joinable outer transaction can instead make a
|
|
6
|
+
# nested savepoint run that callback while the outer transaction is still
|
|
7
|
+
# capable of rolling everything back. This tiny transaction record moves
|
|
8
|
+
# Clickwrap's externally visible work to that real outer boundary.
|
|
9
|
+
#
|
|
10
|
+
# The transaction-record protocol is stable across every supported Rails
|
|
11
|
+
# version (7.1 through 8.x), while the public
|
|
12
|
+
# `ActiveRecord.after_all_transactions_commit` helper only exists from 7.2
|
|
13
|
+
# and deliberately ignores non-joinable transactions.
|
|
14
|
+
class DurableCommitCallback
|
|
15
|
+
def self.defer(event)
|
|
16
|
+
callback = new(event)
|
|
17
|
+
::ActiveRecord::Base.connection.add_transaction_record(callback)
|
|
18
|
+
callback
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(event)
|
|
22
|
+
@event = event
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def before_committed!; end
|
|
26
|
+
|
|
27
|
+
def committed!(should_run_callbacks: true)
|
|
28
|
+
@event.finalize_durable_commit! if should_run_callbacks
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rolledback!(force_restore_state: false, should_run_callbacks: true)
|
|
32
|
+
@event.invalidate_pending_receipts_after_rollback! if should_run_callbacks
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def trigger_transactional_callbacks? = true
|
|
36
|
+
end
|
|
37
|
+
end
|