clickwrap 0.2.1 → 0.3.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/CHANGELOG.md +212 -2
- data/README.md +64 -24
- data/guides/integrating.md +14 -10
- data/guides/naming.md +14 -6
- data/guides/request-evidence.md +80 -15
- data/lib/clickwrap/configuration.rb +158 -74
- data/lib/clickwrap/doctor.rb +7 -2
- data/lib/clickwrap/dsl/policy_builder.rb +31 -6
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +16 -0
- data/lib/clickwrap/privacy.rb +28 -7
- data/lib/clickwrap/request_evidence_extractor.rb +16 -27
- data/lib/clickwrap/request_evidence_policy.rb +45 -41
- data/lib/clickwrap/services/validate_policy_references.rb +7 -32
- data/lib/clickwrap/version.rb +1 -1
- data/lib/clickwrap/vocabulary.rb +36 -0
- data/lib/generators/clickwrap/install_generator.rb +22 -11
- data/lib/generators/clickwrap/templates/initializer.rb.erb +53 -28
- metadata +2 -2
data/lib/clickwrap/privacy.rb
CHANGED
|
@@ -125,7 +125,7 @@ module Clickwrap
|
|
|
125
125
|
"browser_user_agent" => default_category(config, :browser_user_agent),
|
|
126
126
|
"ip_geolocation" => default_category(config, :ip_geolocation).merge(
|
|
127
127
|
"fields" => config.enabled_default_ip_geolocation_fields,
|
|
128
|
-
"resolver" => describe_resolver(config
|
|
128
|
+
"resolver" => describe_resolver(config),
|
|
129
129
|
"fail_capture_when_unavailable" => config.fail_capture_when_ip_geolocation_is_unavailable
|
|
130
130
|
),
|
|
131
131
|
"review_default_request_evidence_configuration_on" =>
|
|
@@ -138,14 +138,25 @@ module Clickwrap
|
|
|
138
138
|
}
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
# `because` is the purpose that will actually be recorded, and
|
|
142
|
+
# `purpose_source` says who wrote it. A `"gem_default"` purpose is a real
|
|
143
|
+
# purpose and it is stored with the evidence — but nobody should mistake
|
|
144
|
+
# Clickwrap's own sentence for one the host's team reviewed, so the
|
|
145
|
+
# inventory never lets the two look alike.
|
|
141
146
|
def default_category(config, category)
|
|
147
|
+
recorded = default_recorded?(config, category)
|
|
148
|
+
|
|
142
149
|
{
|
|
143
|
-
"recorded_by_default" =>
|
|
144
|
-
"because" => config.
|
|
150
|
+
"recorded_by_default" => recorded,
|
|
151
|
+
"because" => recorded ? config.reason_for_recording_by_default(category) : nil,
|
|
152
|
+
"purpose_source" => (config.reason_for_recording_by_default_source(category) if recorded),
|
|
145
153
|
"legal_basis_reference" =>
|
|
146
154
|
config.public_send(:"legal_basis_reference_for_recording_#{plural_for(category)}_by_default"),
|
|
147
155
|
"encrypted" => config.public_send(:"encrypt_recorded_#{plural_for(category)}"),
|
|
148
|
-
"delete_after_seconds" => config.public_send(:"delete_recorded_#{plural_for(category)}_after")&.to_i
|
|
156
|
+
"delete_after_seconds" => config.public_send(:"delete_recorded_#{plural_for(category)}_after")&.to_i,
|
|
157
|
+
"kept_indefinitely" => config.keeps_recorded_request_evidence_indefinitely?(category),
|
|
158
|
+
"reason_for_keeping_indefinitely" =>
|
|
159
|
+
config.reason_for_keeping_recorded_request_evidence_indefinitely(category)
|
|
149
160
|
}
|
|
150
161
|
end
|
|
151
162
|
|
|
@@ -186,6 +197,7 @@ module Clickwrap
|
|
|
186
197
|
entry = {
|
|
187
198
|
"recorded" => setting.record?,
|
|
188
199
|
"because" => setting.because,
|
|
200
|
+
"purpose_source" => policy.request_evidence.purpose_source_for(category),
|
|
189
201
|
"legal_basis_reference" => setting.legal_basis_reference,
|
|
190
202
|
"data_protection_impact_assessment_reference" =>
|
|
191
203
|
setting.data_protection_impact_assessment_reference,
|
|
@@ -201,7 +213,7 @@ module Clickwrap
|
|
|
201
213
|
entry.merge(
|
|
202
214
|
"fields" => policy.request_evidence.enabled_ip_geolocation_fields,
|
|
203
215
|
"resolver_named_by_policy" => policy.request_evidence.ip_geolocation_resolver_name&.to_s,
|
|
204
|
-
"resolver" => describe_resolver(Clickwrap.config
|
|
216
|
+
"resolver" => describe_resolver(Clickwrap.config)
|
|
205
217
|
)
|
|
206
218
|
end
|
|
207
219
|
|
|
@@ -283,10 +295,19 @@ module Clickwrap
|
|
|
283
295
|
}
|
|
284
296
|
end
|
|
285
297
|
|
|
286
|
-
|
|
298
|
+
# Reports the resolver in force, and whether the host named it or
|
|
299
|
+
# Clickwrap adopted the official trackdown adapter because their bundle
|
|
300
|
+
# already carried it. Asking never causes that adoption to happen — an
|
|
301
|
+
# inventory describes a configuration, it does not make one.
|
|
302
|
+
def describe_resolver(config)
|
|
303
|
+
resolver = config.ip_geolocation_resolver_in_force
|
|
287
304
|
return { "configured" => false } if resolver.nil?
|
|
288
305
|
|
|
289
|
-
{
|
|
306
|
+
{
|
|
307
|
+
"configured" => true,
|
|
308
|
+
"class" => resolver.class.name,
|
|
309
|
+
"source" => config.ip_geolocation_resolver_was_adopted_automatically? ? "gem_default" : "host"
|
|
310
|
+
}
|
|
290
311
|
end
|
|
291
312
|
|
|
292
313
|
def plural_for(category)
|
|
@@ -244,7 +244,10 @@ module Clickwrap
|
|
|
244
244
|
|
|
245
245
|
# Provenance is recorded even when the value is not, because "which reader
|
|
246
246
|
# was asked, under which reviewed proxy configuration" is what tells a
|
|
247
|
-
# later reader how much the address is worth.
|
|
247
|
+
# later reader how much the address is worth. A nil digest is part of
|
|
248
|
+
# that answer rather than a hole in it: it says no reviewed proxy
|
|
249
|
+
# configuration was recorded when this address was observed, which is
|
|
250
|
+
# exactly what a reader should know about it.
|
|
248
251
|
provenance = {
|
|
249
252
|
ip_address_reader_name: ip_address_reader_name,
|
|
250
253
|
trusted_proxy_configuration_digest: policy.trusted_proxy_configuration_digest
|
|
@@ -484,13 +487,17 @@ module Clickwrap
|
|
|
484
487
|
# --- Retention ------------------------------------------------------------
|
|
485
488
|
|
|
486
489
|
# Every recorded field leaves here with a disposal answer: a date, the name
|
|
487
|
-
# of a host rule that will produce one, or
|
|
488
|
-
#
|
|
489
|
-
#
|
|
490
|
-
#
|
|
491
|
-
#
|
|
492
|
-
#
|
|
493
|
-
#
|
|
490
|
+
# of a host rule that will produce one, or no schedule at all — which means
|
|
491
|
+
# it keeps pace with the evidence it corroborates, exactly like an
|
|
492
|
+
# indefinite core event. A blank schedule is the answer, not a gap: the
|
|
493
|
+
# planner never lists these rows, and the receipt reports them as recorded
|
|
494
|
+
# rather than as anything pending.
|
|
495
|
+
#
|
|
496
|
+
# Until 0.3.0 this raised when nothing had named a schedule, on the theory
|
|
497
|
+
# that keeping forever must never be silent. The theory held; the cost did
|
|
498
|
+
# not. It refused captures for hosts who had simply not written a sentence,
|
|
499
|
+
# and pushed integrators toward recording nothing — which is worse evidence
|
|
500
|
+
# than evidence kept under the same posture as the agreement it belongs to.
|
|
494
501
|
#
|
|
495
502
|
# `retain_until` names a host calculation instead of a duration because real
|
|
496
503
|
# record-keeping schedules are not always durations — "five years, or three
|
|
@@ -504,15 +511,7 @@ module Clickwrap
|
|
|
504
511
|
return { "#{category}_delete_after": now + class_rule.duration } if class_rule&.duration?
|
|
505
512
|
return { "#{category}_retain_until_rule": class_rule.host_event_name.to_s } if class_rule&.host_event?
|
|
506
513
|
|
|
507
|
-
|
|
508
|
-
# nothing: the blank schedule plus the recorded declaration IS the
|
|
509
|
-
# disposal answer, exactly like an indefinite core event.
|
|
510
|
-
if class_rule&.indefinite? ||
|
|
511
|
-
Clickwrap.config.keeps_recorded_request_evidence_indefinitely?(category)
|
|
512
|
-
return {}
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
raise ConfigurationError, missing_retention_message(category)
|
|
514
|
+
{}
|
|
516
515
|
end
|
|
517
516
|
|
|
518
517
|
def retention_class_rule_for(category)
|
|
@@ -521,16 +520,6 @@ module Clickwrap
|
|
|
521
520
|
Clickwrap.retention_class!(policy.retention_class_key).rule_for(category)
|
|
522
521
|
end
|
|
523
522
|
|
|
524
|
-
def missing_retention_message(category)
|
|
525
|
-
"Clickwrap is about to record #{category} for policy #{policy_key} and nothing says what " \
|
|
526
|
-
"should ever happen to it. Give the policy a rule — `delete_after:` with a reviewed " \
|
|
527
|
-
"period, or `retain_until:` naming a host retention calculation — add a #{category} " \
|
|
528
|
-
"rule (or `keep_recorded_#{category}_indefinitely`) to retention class " \
|
|
529
|
-
"#{policy.retention_class_key.inspect}, or answer it application-wide with " \
|
|
530
|
-
"`keep_recorded_..._indefinitely!(because: \"…\")`. Keeping forever is never silent, " \
|
|
531
|
-
"and Clickwrap will not choose for you."
|
|
532
|
-
end
|
|
533
|
-
|
|
534
523
|
# --- Failing closed -------------------------------------------------------
|
|
535
524
|
|
|
536
525
|
# A policy can decide that evidence it cannot get is worse than no capture
|
|
@@ -8,9 +8,15 @@ module Clickwrap
|
|
|
8
8
|
# not squeamishness about useful data: it is that high-quality evidence is
|
|
9
9
|
# purpose-specific. An IP address is personal data, and keeping it on your own
|
|
10
10
|
# infrastructure does not remove the duty to have a reason for it, protect it,
|
|
11
|
-
# and stop keeping it eventually. So each field is enabled by name,
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# and stop keeping it eventually. So each field is enabled by name, and the
|
|
12
|
+
# policy that enables it is the server's, never the browser's.
|
|
13
|
+
#
|
|
14
|
+
# Naming a field is the whole requirement. Every recorded category still
|
|
15
|
+
# leaves here carrying a purpose and a disposal posture, because a snapshot
|
|
16
|
+
# read years from now has to answer both questions — but since 0.3.0 those
|
|
17
|
+
# answers have honest gem-supplied defaults instead of being the entry fee. A
|
|
18
|
+
# host who writes their own keeps their own words, and the privacy inventory
|
|
19
|
+
# reports which of the two is looking back at you.
|
|
14
20
|
#
|
|
15
21
|
# None of these fields is identity or physical location. An IP address is a
|
|
16
22
|
# network observation. IP geolocation is a provider's estimate about that
|
|
@@ -61,9 +67,11 @@ module Clickwrap
|
|
|
61
67
|
trusted_proxy_configuration_digest: nil, review_configuration_on: nil)
|
|
62
68
|
@policy_key = policy_key
|
|
63
69
|
@retention_class_key = retention_class_key&.to_s
|
|
70
|
+
@purpose_sources = {}
|
|
64
71
|
@ip_address = normalized_setting(:ip_address, ip_address || NOT_RECORDED)
|
|
65
72
|
@browser_user_agent = normalized_setting(:browser_user_agent, browser_user_agent || NOT_RECORDED)
|
|
66
73
|
@ip_geolocation = normalized_setting(:ip_geolocation, ip_geolocation || NOT_RECORDED)
|
|
74
|
+
@purpose_sources.freeze
|
|
67
75
|
@ip_geolocation_fields = normalize_geolocation_fields(ip_geolocation_fields)
|
|
68
76
|
@ip_geolocation_resolver_name = ip_geolocation_resolver_name&.to_sym
|
|
69
77
|
@trusted_proxy_configuration_digest = trusted_proxy_configuration_digest&.to_s
|
|
@@ -87,6 +95,15 @@ module Clickwrap
|
|
|
87
95
|
|
|
88
96
|
def records_anything? = records_ip_address? || records_browser_user_agent? || records_ip_geolocation?
|
|
89
97
|
|
|
98
|
+
# Who wrote the purpose stored for this category: `"host"` when the policy
|
|
99
|
+
# or the initializer supplied one, `"gem_default"` when Clickwrap filled in
|
|
100
|
+
# its own. Deliberately kept off `to_snapshot`: the snapshot is a released
|
|
101
|
+
# evidence format, and the answer is derivable from the configuration a
|
|
102
|
+
# reader already has.
|
|
103
|
+
def purpose_source_for(category)
|
|
104
|
+
@purpose_sources[category.to_s]
|
|
105
|
+
end
|
|
106
|
+
|
|
90
107
|
def setting_for(category)
|
|
91
108
|
case category.to_sym
|
|
92
109
|
when :ip_address then ip_address
|
|
@@ -132,7 +149,6 @@ module Clickwrap
|
|
|
132
149
|
FIELD_CATEGORIES.each { |category| validate_category!(category) }
|
|
133
150
|
validate_geolocation_coherence!
|
|
134
151
|
validate_named_resolver!
|
|
135
|
-
validate_trusted_proxy_configuration_digest!
|
|
136
152
|
end
|
|
137
153
|
|
|
138
154
|
def normalized_setting(category, setting)
|
|
@@ -153,7 +169,16 @@ module Clickwrap
|
|
|
153
169
|
"`config.encrypt_recorded_*` setting, or omit `encrypted:` to inherit it."
|
|
154
170
|
end
|
|
155
171
|
|
|
156
|
-
|
|
172
|
+
# The compiled revision always carries a purpose, so a reader years from
|
|
173
|
+
# now never finds a recorded field with nothing beside it saying what it
|
|
174
|
+
# was for. Whose sentence it is gets remembered separately.
|
|
175
|
+
@purpose_sources[category.to_s] = setting.because.presence ? "host" : "gem_default"
|
|
176
|
+
|
|
177
|
+
Setting.new(
|
|
178
|
+
**setting.to_h,
|
|
179
|
+
encrypted: configured,
|
|
180
|
+
because: setting.because.presence || Vocabulary::DEFAULT_REQUEST_EVIDENCE_PURPOSE
|
|
181
|
+
)
|
|
157
182
|
end
|
|
158
183
|
|
|
159
184
|
def validate_named_resolver!
|
|
@@ -164,8 +189,9 @@ module Clickwrap
|
|
|
164
189
|
raise DefinitionError,
|
|
165
190
|
"Policy #{policy_key} records IP geolocation using resolver " \
|
|
166
191
|
"#{ip_geolocation_resolver_name.inspect}, but no resolver is registered under " \
|
|
167
|
-
"that name.
|
|
168
|
-
"`:application_default
|
|
192
|
+
"that name. Bundle `trackdown` (>= 0.4) and Clickwrap uses it for " \
|
|
193
|
+
"`:application_default` with no wiring line at all, or set " \
|
|
194
|
+
"`config.ip_geolocation_resolver` yourself, or register the named resolver with " \
|
|
169
195
|
"`config.register_ip_geolocation_resolver`. Registered resolvers: " \
|
|
170
196
|
"#{Clickwrap.config.ip_geolocation_resolver_names.join(", ").presence || "(none)"}."
|
|
171
197
|
end
|
|
@@ -186,34 +212,21 @@ module Clickwrap
|
|
|
186
212
|
"capabilities: #{error.message}"
|
|
187
213
|
end
|
|
188
214
|
|
|
215
|
+
# A missing purpose and a missing disposal answer both have defaults now.
|
|
216
|
+
# What is left refuses only the two things a default cannot honestly stand
|
|
217
|
+
# in for: scaffolding text the host actually wrote, and a deletion clock
|
|
218
|
+
# that is not a period.
|
|
189
219
|
def validate_category!(category)
|
|
190
220
|
setting = setting_for(category)
|
|
191
221
|
return unless setting.record?
|
|
192
222
|
|
|
193
|
-
if setting.because.to_s.strip.empty?
|
|
194
|
-
raise DefinitionError,
|
|
195
|
-
"Policy #{policy_key} records #{category} but gives no `because:`. Say in one " \
|
|
196
|
-
"plain sentence why this policy needs it right now. \"We might need it someday\" " \
|
|
197
|
-
"is not a purpose, and a privacy notice mentioning the field is not one either."
|
|
198
|
-
end
|
|
199
|
-
|
|
200
223
|
if ReviewedText.placeholder?(setting.because)
|
|
201
224
|
raise DefinitionError,
|
|
202
225
|
"Policy #{policy_key} records #{category}, but its `because:` is still " \
|
|
203
226
|
"scaffolding text (#{setting.because.inspect}). Replace it with the " \
|
|
204
|
-
"application's reviewed, present-tense reason
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if setting.delete_after.nil? && setting.retain_until.nil? && retention_class_key.nil? &&
|
|
209
|
-
!Clickwrap.config.keeps_recorded_request_evidence_indefinitely?(category)
|
|
210
|
-
raise DefinitionError,
|
|
211
|
-
"Policy #{policy_key} records #{category} but nothing says what should ever " \
|
|
212
|
-
"happen to it. Give it `delete_after:` with a duration, or `retain_until:` " \
|
|
213
|
-
"naming a host event rule, attach a retention class with a rule for this " \
|
|
214
|
-
"category, or answer it application-wide with " \
|
|
215
|
-
"`keep_recorded_..._indefinitely!(because: \"…\")`. Keeping forever is never " \
|
|
216
|
-
"silent, and Clickwrap will not choose for you."
|
|
227
|
+
"application's reviewed, present-tense reason, or drop the option and let " \
|
|
228
|
+
"Clickwrap record its own stated purpose; a TODO is never a data-collection " \
|
|
229
|
+
"purpose."
|
|
217
230
|
end
|
|
218
231
|
|
|
219
232
|
return unless setting.delete_after && setting.delete_after.to_i <= 0
|
|
@@ -228,8 +241,11 @@ module Clickwrap
|
|
|
228
241
|
|
|
229
242
|
if ip_geolocation.record? && enabled.empty?
|
|
230
243
|
raise DefinitionError,
|
|
231
|
-
"Policy #{policy_key} calls `record_ip_geolocation`
|
|
232
|
-
"Name the fields you
|
|
244
|
+
"Policy #{policy_key} calls `record_ip_geolocation` and then turns every field " \
|
|
245
|
+
"off, which cannot mean anything. Name the fields you want, for example " \
|
|
246
|
+
"`country: true`; call `record_ip_geolocation` with no fields at all for the " \
|
|
247
|
+
"coarse country, region, and city; or say `do_not_record_ip_geolocation` if that " \
|
|
248
|
+
"is what you meant."
|
|
233
249
|
end
|
|
234
250
|
|
|
235
251
|
if ip_geolocation.record? &&
|
|
@@ -248,17 +264,5 @@ module Clickwrap
|
|
|
248
264
|
"Policy #{policy_key} enables the IP-geolocation fields #{enabled.join(", ")} " \
|
|
249
265
|
"without recording IP geolocation."
|
|
250
266
|
end
|
|
251
|
-
|
|
252
|
-
def validate_trusted_proxy_configuration_digest!
|
|
253
|
-
return unless records_ip_address? || records_ip_geolocation?
|
|
254
|
-
return unless trusted_proxy_configuration_digest.to_s.strip.empty?
|
|
255
|
-
|
|
256
|
-
raise DefinitionError,
|
|
257
|
-
"Policy #{policy_key} records an IP address or derives IP geolocation, but " \
|
|
258
|
-
"`config.trusted_proxy_configuration_digest` is blank. Review the deployment's " \
|
|
259
|
-
"trusted-proxy topology, digest that reviewed configuration, and set the prefixed " \
|
|
260
|
-
"digest (for example `sha256:...`). This records which proxy decision produced the " \
|
|
261
|
-
"address; it does not claim that the decision was correct."
|
|
262
|
-
end
|
|
263
267
|
end
|
|
264
268
|
end
|
|
@@ -15,7 +15,6 @@ module Clickwrap
|
|
|
15
15
|
Clickwrap.policies.each do |policy|
|
|
16
16
|
validate_documents!(policy)
|
|
17
17
|
retention_class = validate_retention_class!(policy)
|
|
18
|
-
validate_request_evidence_retention!(policy, retention_class)
|
|
19
18
|
validate_host_calculations!(policy, retention_class)
|
|
20
19
|
validate_authority_adapter!(policy)
|
|
21
20
|
validate_ip_geolocation_resolver!(policy)
|
|
@@ -105,37 +104,13 @@ module Clickwrap
|
|
|
105
104
|
)
|
|
106
105
|
end
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# either a global clock or the explicit, reasoned keep-indefinitely.
|
|
116
|
-
next if config_answers_disposal_for?(category)
|
|
117
|
-
|
|
118
|
-
raise DefinitionError,
|
|
119
|
-
"Policy #{policy.key} records #{category}, but nothing says when to dispose of " \
|
|
120
|
-
"it. Add `delete_after:`/`retain_until:` to the policy, a plain-English " \
|
|
121
|
-
"request-evidence rule (or `keep_recorded_#{category}_indefinitely`) to " \
|
|
122
|
-
"retention class #{retention_class.key}, or answer it application-wide in the " \
|
|
123
|
-
"initializer with `delete_recorded_..._after` or " \
|
|
124
|
-
"`keep_recorded_..._indefinitely!(because: \"…\")`."
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def config_answers_disposal_for?(category)
|
|
129
|
-
clock =
|
|
130
|
-
case category.to_sym
|
|
131
|
-
when :ip_address then Clickwrap.config.delete_recorded_ip_addresses_after
|
|
132
|
-
when :browser_user_agent then Clickwrap.config.delete_recorded_browser_user_agents_after
|
|
133
|
-
else Clickwrap.config.delete_recorded_ip_geolocation_after
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
clock.present? || Clickwrap.config.keeps_recorded_request_evidence_indefinitely?(category)
|
|
137
|
-
end
|
|
138
|
-
|
|
107
|
+
# There is deliberately no disposal check here any more. A recorded
|
|
108
|
+
# category with no clock anywhere — not on the policy, not in its
|
|
109
|
+
# retention class, not in the initializer — means it keeps pace with the
|
|
110
|
+
# evidence it corroborates, which is what the core event does by default
|
|
111
|
+
# too. What still fails below is a rule that names a calculation nobody
|
|
112
|
+
# registered: a deadline waiting on a typo never arrives, and that is a
|
|
113
|
+
# different thing from a deliberate absence of one.
|
|
139
114
|
def validate_host_calculations!(policy, retention_class)
|
|
140
115
|
referenced = retention_class.rules.values.filter_map do |rule|
|
|
141
116
|
rule.host_event_name&.to_sym
|
data/lib/clickwrap/version.rb
CHANGED
data/lib/clickwrap/vocabulary.rb
CHANGED
|
@@ -160,6 +160,42 @@ module Clickwrap
|
|
|
160
160
|
accuracy_radius_in_kilometers
|
|
161
161
|
].freeze
|
|
162
162
|
|
|
163
|
+
# The three coarse fields `config.record_request_evidence_by_default = true`
|
|
164
|
+
# turns on. Coarse means administrative area, not a point: a country, a
|
|
165
|
+
# region, and a city are what a provider can estimate from an address with
|
|
166
|
+
# any confidence at all. Everything finer — a postal code, coordinates, a
|
|
167
|
+
# timezone, a metro code — stays its own separately named decision, because
|
|
168
|
+
# a switch that reads "record request evidence" should not hand somebody
|
|
169
|
+
# coordinates they never asked for.
|
|
170
|
+
COARSE_IP_GEOLOCATION_DATA_FIELDS = %w[country region city].freeze
|
|
171
|
+
|
|
172
|
+
# The purpose Clickwrap records when a host enables request evidence
|
|
173
|
+
# without writing a purpose of their own. It is the gem's own sentence, not
|
|
174
|
+
# a reviewed host decision, and the privacy inventory says which of the two
|
|
175
|
+
# it is looking at. It exists because the alternative — refusing to boot
|
|
176
|
+
# until somebody writes a sentence — was pushing integrators to record
|
|
177
|
+
# nothing at all, and no corroboration is worse evidence than corroboration
|
|
178
|
+
# collected under the gem's stated purpose.
|
|
179
|
+
DEFAULT_REQUEST_EVIDENCE_PURPOSE =
|
|
180
|
+
"Corroborate who performed each recorded act, from where, on what client — to defend " \
|
|
181
|
+
"the recorded agreement itself."
|
|
182
|
+
|
|
183
|
+
# The reason recorded when a host keeps request evidence indefinitely
|
|
184
|
+
# without writing their own. Same posture as the purpose above: the
|
|
185
|
+
# declaration is still recorded and still readable years later; only the
|
|
186
|
+
# obligation to phrase it yourself is gone.
|
|
187
|
+
DEFAULT_REASON_FOR_KEEPING_REQUEST_EVIDENCE_INDEFINITELY =
|
|
188
|
+
"Corroboration lives as long as the evidence it corroborates"
|
|
189
|
+
|
|
190
|
+
# The reason recorded when a host turns off encryption for request evidence
|
|
191
|
+
# without writing their own. Calling the method is still the ceremony —
|
|
192
|
+
# `deliberately_store_request_evidence_unencrypted!` is a sentence a
|
|
193
|
+
# reviewer finds in a diff and cannot misread — but the gem no longer
|
|
194
|
+
# demands the sentence be phrased twice.
|
|
195
|
+
DEFAULT_REASON_FOR_STORING_REQUEST_EVIDENCE_UNENCRYPTED =
|
|
196
|
+
"The application deliberately stores request evidence unencrypted; the reason lives " \
|
|
197
|
+
"outside Clickwrap"
|
|
198
|
+
|
|
163
199
|
# Provenance that travels with any stored IP-geolocation result. A policy
|
|
164
200
|
# cannot keep provider-derived coordinates while stripping the uncertainty
|
|
165
201
|
# needed to interpret them.
|
|
@@ -28,9 +28,13 @@ module Clickwrap
|
|
|
28
28
|
# spelling of the off-by-default posture. It disappears after generation;
|
|
29
29
|
# it is deliberately not a runtime concept. There is no
|
|
30
30
|
# `record_network_context`, `record_everything`, `full_evidence`, or
|
|
31
|
-
# regulation-named mode switch anywhere in this gem, because a flag
|
|
32
|
-
#
|
|
33
|
-
#
|
|
31
|
+
# regulation-named mode switch anywhere in this gem, because a flag whose
|
|
32
|
+
# name hides what it collects is the thing this gem exists not to ship, and
|
|
33
|
+
# no flag can make a legal determination on a host's behalf. The runtime's
|
|
34
|
+
# one grouped switch, `config.record_request_evidence_by_default`, is not
|
|
35
|
+
# one of those: it names its three fields and can never grow a fourth. The
|
|
36
|
+
# installer still asks about them one at a time, because a purpose the host
|
|
37
|
+
# wrote beats the honest default the gem would otherwise record.
|
|
34
38
|
class InstallGenerator < Rails::Generators::Base
|
|
35
39
|
include ActiveRecord::Generators::Migration
|
|
36
40
|
|
|
@@ -1156,23 +1160,30 @@ module Clickwrap
|
|
|
1156
1160
|
validate_ip_geolocation_resolver_class_name!
|
|
1157
1161
|
end
|
|
1158
1162
|
|
|
1163
|
+
# A missing purpose and a missing deletion period are both fine, and the
|
|
1164
|
+
# generated file simply omits those lines: the gem records its own stated
|
|
1165
|
+
# purpose and keeps the field as long as the evidence it corroborates.
|
|
1166
|
+
# Scaffolding text is the one thing still refused, because a `TODO` the
|
|
1167
|
+
# installer writes into a shipped initializer is worse than no line at
|
|
1168
|
+
# all — the gem would reject it at boot anyway.
|
|
1159
1169
|
def validate_enabled_category!(label, enabled:, because:, delete_after_days:,
|
|
1160
1170
|
reason_option:, retention_option:)
|
|
1161
1171
|
return unless enabled
|
|
1162
1172
|
|
|
1163
|
-
|
|
1173
|
+
if Clickwrap::ReviewedText.placeholder?(because)
|
|
1164
1174
|
raise Thor::Error,
|
|
1165
|
-
"Clickwrap cannot enable #{label} with a
|
|
1166
|
-
"Give the application's reviewed, present-tense reason
|
|
1167
|
-
"#{reason_option}=\"...\", or
|
|
1175
|
+
"Clickwrap cannot enable #{label} with a scaffolding reason " \
|
|
1176
|
+
"(#{because.inspect}). Give the application's reviewed, present-tense reason " \
|
|
1177
|
+
"with #{reason_option}=\"...\", or omit it entirely and let Clickwrap record " \
|
|
1178
|
+
"its own stated purpose. No files were written."
|
|
1168
1179
|
end
|
|
1169
1180
|
|
|
1170
|
-
return
|
|
1181
|
+
return unless delete_after_days.negative?
|
|
1171
1182
|
|
|
1172
1183
|
raise Thor::Error,
|
|
1173
|
-
"
|
|
1174
|
-
"
|
|
1175
|
-
"
|
|
1184
|
+
"#{retention_option} cannot be negative (got #{delete_after_days}). Give the " \
|
|
1185
|
+
"number of days your application reviewed, or omit it and #{label} will keep " \
|
|
1186
|
+
"pace with the evidence it corroborates. No files were written."
|
|
1176
1187
|
end
|
|
1177
1188
|
|
|
1178
1189
|
def validate_ip_geolocation_coordinates!
|
|
@@ -28,8 +28,11 @@
|
|
|
28
28
|
#
|
|
29
29
|
# There is deliberately no `maximum_evidence`, `full_evidence`, `legal_proof`,
|
|
30
30
|
# or regulation-named mode setting. No runtime flag can make a legal
|
|
31
|
-
# determination for you, and an option
|
|
32
|
-
#
|
|
31
|
+
# determination for you, and an option whose name does not say what it collects
|
|
32
|
+
# is exactly what this gem exists not to ship. The one grouped switch that does
|
|
33
|
+
# exist, `config.record_request_evidence_by_default`, names its own contents:
|
|
34
|
+
# an IP address, a browser user agent, and a coarse country/region/city
|
|
35
|
+
# estimate — nothing finer, ever.
|
|
33
36
|
<%- if recipe -%>
|
|
34
37
|
#
|
|
35
38
|
# This file was scaffolded with `--request-evidence-recipe=<%= recipe %>`. The
|
|
@@ -251,10 +254,12 @@ Clickwrap.configure do |config|
|
|
|
251
254
|
# proof that anyone was physically anywhere.
|
|
252
255
|
<%- if records_any_request_evidence? -%>
|
|
253
256
|
#
|
|
254
|
-
# You enabled some of these during install
|
|
255
|
-
# purpose and deletion period before writing this file; review both
|
|
256
|
-
#
|
|
257
|
-
#
|
|
257
|
+
# You enabled some of these during install, and the installer asked for an
|
|
258
|
+
# explicit purpose and deletion period before writing this file; review both
|
|
259
|
+
# below. The gem itself would have accepted the fields without them and
|
|
260
|
+
# recorded its own stated purpose instead — the installer asks because a
|
|
261
|
+
# sentence your team wrote is worth more than one the gem supplied. The annex
|
|
262
|
+
# table came with `--with-request-evidence`, which this install added for you.
|
|
258
263
|
<%- else -%>
|
|
259
264
|
#
|
|
260
265
|
# Everything here is off, which is the safe default — and the annex table was
|
|
@@ -272,26 +277,35 @@ Clickwrap.configure do |config|
|
|
|
272
277
|
|
|
273
278
|
# --- Request evidence: why, how long, and how it is protected ---------------
|
|
274
279
|
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
|
|
282
|
-
|
|
280
|
+
# Both of these are optional, and the installer wrote yours because you gave
|
|
281
|
+
# them. A field enabled with no purpose records the one Clickwrap states, and
|
|
282
|
+
# the privacy inventory marks it as the gem's sentence rather than yours; a
|
|
283
|
+
# field with no deletion period keeps pace with the evidence it corroborates.
|
|
284
|
+
# What is refused is a purpose that is still scaffolding text, and a deletion
|
|
285
|
+
# period set alongside `keep_recorded_..._indefinitely!` for the same field.
|
|
286
|
+
# Deleting a value removes the encrypted annex entry and appends a disposition
|
|
287
|
+
# event — the historical agreement, declaration, or authorization stays intact
|
|
288
|
+
# and verifiable without it.
|
|
289
|
+
|
|
290
|
+
<%- if record_ip_addresses? && reason_for_recording_ip_addresses.present? -%>
|
|
283
291
|
config.reason_for_recording_ip_addresses_by_default =
|
|
284
292
|
<%= reason_for_recording_ip_addresses.inspect %>
|
|
293
|
+
<%- end -%>
|
|
294
|
+
<%- if record_ip_addresses? && delete_recorded_ip_addresses_after_days.positive? -%>
|
|
285
295
|
config.delete_recorded_ip_addresses_after = <%= delete_recorded_ip_addresses_after_days %>.days
|
|
286
296
|
<%- end -%>
|
|
287
|
-
<%- if record_browser_user_agents? -%>
|
|
297
|
+
<%- if record_browser_user_agents? && reason_for_recording_browser_user_agents.present? -%>
|
|
288
298
|
config.reason_for_recording_browser_user_agents_by_default =
|
|
289
299
|
<%= reason_for_recording_browser_user_agents.inspect %>
|
|
300
|
+
<%- end -%>
|
|
301
|
+
<%- if record_browser_user_agents? && delete_recorded_browser_user_agents_after_days.positive? -%>
|
|
290
302
|
config.delete_recorded_browser_user_agents_after = <%= delete_recorded_browser_user_agents_after_days %>.days
|
|
291
303
|
<%- end -%>
|
|
292
|
-
<%- if any_ip_geolocation_field? -%>
|
|
304
|
+
<%- if any_ip_geolocation_field? && reason_for_recording_ip_geolocation.present? -%>
|
|
293
305
|
config.reason_for_recording_ip_geolocation_by_default =
|
|
294
306
|
<%= reason_for_recording_ip_geolocation.inspect %>
|
|
307
|
+
<%- end -%>
|
|
308
|
+
<%- if any_ip_geolocation_field? && delete_recorded_ip_geolocation_after_days.positive? -%>
|
|
295
309
|
config.delete_recorded_ip_geolocation_after = <%= delete_recorded_ip_geolocation_after_days %>.days
|
|
296
310
|
<%- end -%>
|
|
297
311
|
<%- if records_any_request_evidence? -%>
|
|
@@ -301,10 +315,10 @@ Clickwrap.configure do |config|
|
|
|
301
315
|
config.review_default_request_evidence_configuration_on = <%= review_date_literal %>
|
|
302
316
|
<%- else -%>
|
|
303
317
|
# Nothing above is enabled, so there is nothing to give a purpose or a
|
|
304
|
-
# deletion period to.
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
# outliving its reason:
|
|
318
|
+
# deletion period to. Turning the ordinary three on later is one line —
|
|
319
|
+
# `config.record_request_evidence_by_default = true` — and these are the
|
|
320
|
+
# settings that upgrade it into a record your own team reviewed, plus a date
|
|
321
|
+
# so the decision gets looked at again instead of outliving its reason:
|
|
308
322
|
#
|
|
309
323
|
# config.reason_for_recording_ip_addresses_by_default = "Investigate disputed submissions"
|
|
310
324
|
# config.delete_recorded_ip_addresses_after = 90.days
|
|
@@ -324,8 +338,9 @@ Clickwrap.configure do |config|
|
|
|
324
338
|
# Turning one of these off puts the raw value in plain text in your database,
|
|
325
339
|
# and therefore in every ordinary backup and database dump. It is allowed,
|
|
326
340
|
# because some applications have a reviewed reason, but it is never a quiet
|
|
327
|
-
# one-character change: Clickwrap refuses `false` until you have
|
|
328
|
-
# `config.deliberately_store_request_evidence_unencrypted
|
|
341
|
+
# one-character change: Clickwrap refuses `false` until you have written
|
|
342
|
+
# `config.deliberately_store_request_evidence_unencrypted!` above it. Writing
|
|
343
|
+
# that line IS the whole ceremony — its `because:` is optional.
|
|
329
344
|
#
|
|
330
345
|
# config.encrypt_recorded_ip_addresses = true
|
|
331
346
|
# config.encrypt_recorded_browser_user_agents = true
|
|
@@ -359,7 +374,9 @@ Clickwrap.configure do |config|
|
|
|
359
374
|
Clickwrap.trusted_proxy_configuration_digest_for_rails_application
|
|
360
375
|
<%- else -%>
|
|
361
376
|
# A digest of the effective trusted-proxy rules, stored beside any recorded
|
|
362
|
-
# address. Nothing here records one, so there is nothing to stamp
|
|
377
|
+
# address. Nothing here records one, so there is nothing to stamp. Set it if
|
|
378
|
+
# you enable IP recording later: it is not required, but without it every
|
|
379
|
+
# recorded address carries a blank where its collection context should be.
|
|
363
380
|
#
|
|
364
381
|
# config.trusted_proxy_configuration_digest =
|
|
365
382
|
# Clickwrap.trusted_proxy_configuration_digest_for_rails_application
|
|
@@ -368,8 +385,9 @@ Clickwrap.configure do |config|
|
|
|
368
385
|
<%- if any_ip_geolocation_field? -%>
|
|
369
386
|
# DECISION — you enabled IP-geolocation fields and explicitly selected this
|
|
370
387
|
# resolver. Make its gem and data source available in every environment:
|
|
371
|
-
# Clickwrap refuses to boot with fields
|
|
372
|
-
# recording blanks that later
|
|
388
|
+
# Clickwrap refuses to boot with fields nothing can resolve — no resolver here
|
|
389
|
+
# and no trackdown in the bundle — rather than recording blanks that later
|
|
390
|
+
# read as "no result".
|
|
373
391
|
#
|
|
374
392
|
# Only the fields authorized above are stored, never the whole result object,
|
|
375
393
|
# and every stored value keeps the provider, source, estimated state,
|
|
@@ -378,11 +396,18 @@ Clickwrap.configure do |config|
|
|
|
378
396
|
# needed to read them.
|
|
379
397
|
config.ip_geolocation_resolver = <%= ip_geolocation_resolver_class_name %>.new
|
|
380
398
|
<%- else -%>
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
#
|
|
399
|
+
# Something has to resolve an IP-geolocation field before it can be enabled,
|
|
400
|
+
# here or in a policy. Bundling trackdown is enough — Clickwrap uses the
|
|
401
|
+
# official adapter for you when you have not named a resolver of your own,
|
|
402
|
+
# and the privacy inventory reports it as the gem's choice rather than yours.
|
|
403
|
+
# It requires trackdown >= 0.4 so the provider that actually answered and
|
|
404
|
+
# per-request source trust are preserved:
|
|
384
405
|
#
|
|
385
406
|
# bundle add trackdown --version ">= 0.4"
|
|
407
|
+
#
|
|
408
|
+
# Name it explicitly to pick a different provider, or when you are wiring
|
|
409
|
+
# Trackdown's per-request CDN trust:
|
|
410
|
+
#
|
|
386
411
|
# config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
|
|
387
412
|
<%- end -%>
|
|
388
413
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clickwrap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-08-
|
|
10
|
+
date: 2026-08-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: actionpack
|