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/guides/request-evidence.md
CHANGED
|
@@ -5,11 +5,25 @@ configured actor and authentication source, the policy and application version,
|
|
|
5
5
|
request ID when one is available. None of that is derived from the person's network or browser.
|
|
6
6
|
|
|
7
7
|
It records **nothing** about the request itself — no IP address, no browser user-agent, no
|
|
8
|
-
IP-geolocation field —
|
|
9
|
-
|
|
10
|
-
turning on something else.
|
|
8
|
+
IP-geolocation field — until the initializer or a policy says otherwise. Saying otherwise takes
|
|
9
|
+
one line:
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
```ruby
|
|
12
|
+
Clickwrap.configure do |config|
|
|
13
|
+
config.record_request_evidence_by_default = true
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That switch records exactly what its name says and nothing else: the IP address, the browser
|
|
18
|
+
user agent, and a coarse country / region / city estimate. Every finer geolocation field — a
|
|
19
|
+
postal code, coordinates, a timezone, a continent, a metro code, an accuracy radius — remains
|
|
20
|
+
its own separately named line, and no option here turns a category on as a side effect of
|
|
21
|
+
turning on something else. There is still no profile switch and no name that hides its
|
|
22
|
+
contents (`gdpr_compliant_mode`, `maximum_evidence`, `legal_proof`); a switch that reads
|
|
23
|
+
`record_request_evidence_by_default` is the opposite of one.
|
|
24
|
+
|
|
25
|
+
The gem's own default is still record-nothing, and that default is evidence design, not
|
|
26
|
+
squeamishness. Three things follow from it:
|
|
13
27
|
|
|
14
28
|
- An IP address and other online identifiers can be personal data. The CJEU addressed dynamic
|
|
15
29
|
IP addresses in [Breyer, Case C-582/14](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A62014CJ0582)
|
|
@@ -230,7 +244,8 @@ they tell an auditor completely different things:
|
|
|
230
244
|
|
|
231
245
|
Two places, and the policy always wins.
|
|
232
246
|
|
|
233
|
-
**In the initializer, for every policy.**
|
|
247
|
+
**In the initializer, for every policy.** Either the one switch, or the individual settings —
|
|
248
|
+
each field has its own, and each is `false`:
|
|
234
249
|
|
|
235
250
|
```ruby
|
|
236
251
|
Clickwrap.configure do |config|
|
|
@@ -242,10 +257,15 @@ Clickwrap.configure do |config|
|
|
|
242
257
|
end
|
|
243
258
|
```
|
|
244
259
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
260
|
+
Everything below the first line there is optional. A purpose you do not write becomes
|
|
261
|
+
Clickwrap's own stated one (marked `gem_default` in the inventory); a deletion period you do
|
|
262
|
+
not set means the field keeps pace with the evidence it corroborates.
|
|
263
|
+
|
|
264
|
+
Three things are still a `ConfigurationError` at the end of the `configure` block rather than a
|
|
265
|
+
warning: scaffolding text standing in for a purpose, a deletion clock set alongside
|
|
266
|
+
`keep_recorded_..._indefinitely!` for the same category, and enabling a
|
|
267
|
+
`record_ip_geolocation_*_by_default` field with no `ip_geolocation_resolver` configured *and*
|
|
268
|
+
no `trackdown` in the bundle — there would be nothing to resolve them with.
|
|
249
269
|
|
|
250
270
|
**In one policy, for one flow.** This is the shape most applications want: ordinary signup
|
|
251
271
|
inherits nothing, and the one consequential action opts in by name.
|
|
@@ -290,16 +310,43 @@ Clickwrap.policy :regulated_authorization do
|
|
|
290
310
|
end
|
|
291
311
|
```
|
|
292
312
|
|
|
293
|
-
Every keyword there is doing work
|
|
313
|
+
Every keyword there is doing work, and **every one of them is optional**. The same three
|
|
314
|
+
declarations with nothing at all supplied are valid, and record the same fields:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
Clickwrap.policy :frictionless_regulated_authorization do
|
|
318
|
+
authorize :regulated_action, one_time: true, valid_for: 10.minutes
|
|
319
|
+
|
|
320
|
+
record_ip_address
|
|
321
|
+
record_browser_user_agent
|
|
322
|
+
record_ip_geolocation
|
|
323
|
+
retain_with :regulated_evidence
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`record_ip_geolocation` with no field named records the coarse trio — country, region, city —
|
|
328
|
+
and nothing finer. Name even one field and the set is exactly what you named; name every field
|
|
329
|
+
`false` and Clickwrap refuses, because calling `record_ip_geolocation` and disabling everything
|
|
330
|
+
cannot mean anything (`do_not_record_ip_geolocation` is how to say that).
|
|
331
|
+
|
|
332
|
+
What each keyword adds when you do supply it:
|
|
294
333
|
|
|
295
334
|
- **`because:`** is the present purpose, in a sentence someone outside engineering can read. It
|
|
296
|
-
is stored
|
|
335
|
+
is stored and printed by `bin/rails clickwrap:privacy:inventory`. It is optional: a policy
|
|
336
|
+
that omits it records Clickwrap's own stated purpose instead, and the inventory marks that
|
|
337
|
+
entry `"purpose_source": "gem_default"` so nobody mistakes it for a sentence your team
|
|
338
|
+
reviewed. What is refused is scaffolding text — `"TODO: ask legal"` is not a purpose.
|
|
297
339
|
- **`legal_basis_reference:`** and **`data_protection_impact_assessment_reference:`** are
|
|
298
340
|
host-supplied pointers to your own reviewed documents. Clickwrap stores them. It does not
|
|
299
|
-
read them, validate them, or
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
341
|
+
read them, validate them, endorse them, or ever require them — nothing in the gem refuses a
|
|
342
|
+
recorded field for want of either, and nothing ever will. Your privacy policy owns the why;
|
|
343
|
+
the gem records the what.
|
|
344
|
+
- **`delete_after:`** and **`retain_until:`** are both optional. When neither the policy, its
|
|
345
|
+
retention class, nor the configuration names a schedule, the field keeps pace with the
|
|
346
|
+
evidence it corroborates — the same posture the core event has — and the annex is stamped
|
|
347
|
+
with no deadline at all, so the retention planner never lists it. Setting a clock alongside
|
|
348
|
+
an application-wide `keep_recorded_..._indefinitely!` for the same category is still refused:
|
|
349
|
+
those are opposite decisions.
|
|
303
350
|
- **`fail_if_unavailable:`** (default `false`) decides whether evidence you cannot get is worse
|
|
304
351
|
than no capture at all. When it is `true` and the field cannot be resolved, the capture and
|
|
305
352
|
the protected action roll back together.
|
|
@@ -359,6 +406,12 @@ So "we record IP addresses" is not one decision. It is four:
|
|
|
359
406
|
later reader identify which rules were in force — the difference between corroborating
|
|
360
407
|
evidence and a number with no recorded collection context.
|
|
361
408
|
|
|
409
|
+
Since 0.3.0 this step is not a precondition for recording an address. Leave it unset and the
|
|
410
|
+
annex stores `trusted_proxy_configuration_digest` as `nil`, which is the honest reading of
|
|
411
|
+
the situation: nobody recorded having reviewed a proxy topology when this address was
|
|
412
|
+
observed. `bin/rails clickwrap:doctor` warns while it stays that way. Hosts who complete
|
|
413
|
+
step 4 get the stronger record; hosts who do not get an address that says what it is worth.
|
|
414
|
+
|
|
362
415
|
If you replace the reader, you own that decision, and the receipt says so: any host-assigned
|
|
363
416
|
lambda is labeled `host_configured_reader` rather than `rails_request_remote_ip`, even when the
|
|
364
417
|
body is identical. Clickwrap will not claim Rails' spoof checks on your behalf.
|
|
@@ -378,6 +431,18 @@ gem and must never become one.
|
|
|
378
431
|
bundle add trackdown --version ">= 0.4"
|
|
379
432
|
```
|
|
380
433
|
|
|
434
|
+
That is the whole wiring step. When your bundle carries trackdown and you have not named a
|
|
435
|
+
resolver of your own, Clickwrap uses `Clickwrap::IpGeolocation::TrackdownResolver` for any
|
|
436
|
+
policy that records IP geolocation — lazily, only at the moment something actually needs an
|
|
437
|
+
address resolved, and never as a collection decision on its own (nothing is resolved until a
|
|
438
|
+
policy has already enabled a geolocation field). `bin/rails clickwrap:privacy:inventory` reports
|
|
439
|
+
that resolver with `"source": "gem_default"`, and `clickwrap:doctor` names it, so an adopted
|
|
440
|
+
resolver never reads as a host decision. An installed trackdown older than 0.4 is not hidden
|
|
441
|
+
behind "the gem is missing": you get the adapter's own sentence about upgrading.
|
|
442
|
+
|
|
443
|
+
Set it explicitly when you want a different provider per policy, or when you are wiring
|
|
444
|
+
Trackdown's per-request CDN trust:
|
|
445
|
+
|
|
381
446
|
```ruby
|
|
382
447
|
# clickwrap-doc-test: syntax-only — requires the optional trackdown gem installed above
|
|
383
448
|
Trackdown.configure do |trackdown|
|
|
@@ -21,14 +21,18 @@ module Clickwrap
|
|
|
21
21
|
# - Every hook defaults to a no-op, so the gem works untouched and a host
|
|
22
22
|
# wires hooks only as needed.
|
|
23
23
|
# - Nothing here collects personal data by default. Every `record_*` flag
|
|
24
|
-
# starts false
|
|
25
|
-
#
|
|
24
|
+
# starts false. Turning one on takes one line and nothing else; the
|
|
25
|
+
# purpose and the disposal answer have honest gem-supplied defaults, and
|
|
26
|
+
# a host who wants reviewed ones writes them.
|
|
26
27
|
#
|
|
27
28
|
# One setting deserves its own note: there is deliberately no
|
|
28
29
|
# `gdpr_compliant_mode`, `maximum_evidence`, `full_evidence`, or
|
|
29
|
-
# `legal_proof`.
|
|
30
|
-
# is exactly the thing this gem exists not to do
|
|
31
|
-
#
|
|
30
|
+
# `legal_proof`. Those names hide what they collect and pretend to make a
|
|
31
|
+
# legal determination, which is exactly the thing this gem exists not to do.
|
|
32
|
+
# `record_request_evidence_by_default` is the opposite kind of switch: it
|
|
33
|
+
# says out loud what it records (an IP address, a browser user agent, and a
|
|
34
|
+
# coarse country/region/city estimate), it enables nothing finer, and it
|
|
35
|
+
# claims nothing about the law.
|
|
32
36
|
class Configuration
|
|
33
37
|
DOCUMENT_STORES = %i[database active_storage resolver].freeze
|
|
34
38
|
DIGEST_ALGORITHMS = %i[sha256 sha384 sha512].freeze
|
|
@@ -246,6 +250,8 @@ module Clickwrap
|
|
|
246
250
|
|
|
247
251
|
@ip_geolocation_resolver = nil
|
|
248
252
|
@ip_geolocation_resolvers = {}
|
|
253
|
+
@automatically_adopted_ip_geolocation_resolver = nil
|
|
254
|
+
@considered_automatic_ip_geolocation_resolver = false
|
|
249
255
|
@fail_capture_when_ip_geolocation_is_unavailable = false
|
|
250
256
|
|
|
251
257
|
# The keyed annex digest carries a key ID so a host can rotate keys
|
|
@@ -612,6 +618,49 @@ module Clickwrap
|
|
|
612
618
|
|
|
613
619
|
# --- Request-evidence setters --------------------------------------------
|
|
614
620
|
|
|
621
|
+
# The one switch.
|
|
622
|
+
#
|
|
623
|
+
# config.record_request_evidence_by_default = true
|
|
624
|
+
#
|
|
625
|
+
# Records, on every policy: the IP address the request arrived from, the
|
|
626
|
+
# browser user agent it sent, and a coarse country/region/city estimate for
|
|
627
|
+
# that address. Nothing finer — a postal code, coordinates, a timezone, a
|
|
628
|
+
# metro code — and nothing else at all. The purpose and the disposal answer
|
|
629
|
+
# have honest gem defaults (see `Vocabulary::DEFAULT_REQUEST_EVIDENCE_PURPOSE`
|
|
630
|
+
# and the keep-indefinitely posture below), so this line is genuinely the
|
|
631
|
+
# whole first step.
|
|
632
|
+
#
|
|
633
|
+
# It is a fan-out setter, not a mode: it writes the individual
|
|
634
|
+
# `record_*_by_default` flags, which means it composes with them in reading
|
|
635
|
+
# order. Write the switch first and a narrower flag after it to carve one
|
|
636
|
+
# field back out —
|
|
637
|
+
#
|
|
638
|
+
# config.record_request_evidence_by_default = true
|
|
639
|
+
# config.record_browser_user_agent_by_default = false
|
|
640
|
+
#
|
|
641
|
+
# — and any policy can still override all of it with `record_ip_address`,
|
|
642
|
+
# `do_not_record_ip_address`, and their siblings. Setting it to false turns
|
|
643
|
+
# the same three fields off and leaves the finer geolocation fields alone,
|
|
644
|
+
# because it never turned those on.
|
|
645
|
+
def record_request_evidence_by_default=(value)
|
|
646
|
+
enabled = ensure_boolean(value, "record_request_evidence_by_default")
|
|
647
|
+
|
|
648
|
+
@record_ip_address_by_default = enabled
|
|
649
|
+
@record_browser_user_agent_by_default = enabled
|
|
650
|
+
Vocabulary::COARSE_IP_GEOLOCATION_DATA_FIELDS.each do |field|
|
|
651
|
+
instance_variable_set(:"@record_ip_geolocation_#{field}_by_default", enabled)
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Reads back what the switch describes rather than a remembered assignment:
|
|
656
|
+
# true when all three coarse fields are on, however they were turned on.
|
|
657
|
+
def record_request_evidence_by_default
|
|
658
|
+
record_ip_address_by_default && record_browser_user_agent_by_default &&
|
|
659
|
+
Vocabulary::COARSE_IP_GEOLOCATION_DATA_FIELDS.all? do |field|
|
|
660
|
+
public_send(:"record_ip_geolocation_#{field}_by_default")
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
|
|
615
664
|
def record_ip_address_by_default=(value)
|
|
616
665
|
@record_ip_address_by_default = ensure_boolean(value, "record_ip_address_by_default")
|
|
617
666
|
end
|
|
@@ -715,11 +764,36 @@ module Clickwrap
|
|
|
715
764
|
end
|
|
716
765
|
|
|
717
766
|
def ip_geolocation_resolver_for(name = nil)
|
|
718
|
-
return
|
|
767
|
+
return application_default_ip_geolocation_resolver if name.blank? || name.to_s == "application_default"
|
|
719
768
|
|
|
720
769
|
@ip_geolocation_resolvers[name.to_s]
|
|
721
770
|
end
|
|
722
771
|
|
|
772
|
+
# What a policy gets when it does not name a resolver: the host's own, or —
|
|
773
|
+
# when they never set one and their bundle already carries `trackdown` 0.4
|
|
774
|
+
# or newer — the official adapter for it. That is the whole "trackdown plus
|
|
775
|
+
# Cloudflare just works" path, and it is deliberately not a silent
|
|
776
|
+
# collection decision: nothing calls this until a policy has already
|
|
777
|
+
# enabled an IP-geolocation field.
|
|
778
|
+
#
|
|
779
|
+
# An installed-but-too-old trackdown is NOT hidden here. The adapter's own
|
|
780
|
+
# sentence about upgrading is more useful than pretending the gem is
|
|
781
|
+
# missing.
|
|
782
|
+
def application_default_ip_geolocation_resolver
|
|
783
|
+
@ip_geolocation_resolver || automatically_adopted_ip_geolocation_resolver
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# The resolver actually in force, for anything that only wants to describe
|
|
787
|
+
# the configuration (the privacy inventory, `clickwrap:doctor`). Unlike the
|
|
788
|
+
# reader above it never adopts one as a side effect of being asked.
|
|
789
|
+
def ip_geolocation_resolver_in_force
|
|
790
|
+
@ip_geolocation_resolver || @automatically_adopted_ip_geolocation_resolver
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def ip_geolocation_resolver_was_adopted_automatically?
|
|
794
|
+
@ip_geolocation_resolver.nil? && !@automatically_adopted_ip_geolocation_resolver.nil?
|
|
795
|
+
end
|
|
796
|
+
|
|
723
797
|
def ip_geolocation_resolver_names = @ip_geolocation_resolvers.keys.sort.freeze
|
|
724
798
|
|
|
725
799
|
# Plain-English key-rotation API. The ID is evidence and must stay stable;
|
|
@@ -842,7 +916,6 @@ module Clickwrap
|
|
|
842
916
|
# caught the typos; these are the things that need the whole block resolved.
|
|
843
917
|
def validate!
|
|
844
918
|
validate_request_evidence_defaults!
|
|
845
|
-
validate_trusted_proxy_configuration!
|
|
846
919
|
validate_ip_geolocation_resolver!
|
|
847
920
|
true
|
|
848
921
|
end
|
|
@@ -870,6 +943,15 @@ module Clickwrap
|
|
|
870
943
|
Reference.record(actor)
|
|
871
944
|
end
|
|
872
945
|
|
|
946
|
+
# Enabling a category by default needs nothing else. A purpose the host did
|
|
947
|
+
# not write falls back to Clickwrap's own stated one, and a disposal answer
|
|
948
|
+
# nobody gave means the corroboration keeps pace with the evidence it
|
|
949
|
+
# corroborates — which is what core evidence has done since 0.2.0.
|
|
950
|
+
#
|
|
951
|
+
# Two things still fail here, and both are the host contradicting
|
|
952
|
+
# themselves rather than merely leaving a blank: scaffolding text standing
|
|
953
|
+
# in for a purpose, and a deletion clock set alongside a declaration to
|
|
954
|
+
# keep the same category forever.
|
|
873
955
|
def validate_request_evidence_defaults!
|
|
874
956
|
{
|
|
875
957
|
ip_address: [record_ip_address_by_default,
|
|
@@ -884,37 +966,20 @@ module Clickwrap
|
|
|
884
966
|
}.each do |category, (enabled, reason, delete_after)|
|
|
885
967
|
next unless enabled
|
|
886
968
|
|
|
887
|
-
if reason.to_s.strip.empty?
|
|
888
|
-
raise ConfigurationError,
|
|
889
|
-
"Clickwrap is set to record #{category} for every policy by default, but " \
|
|
890
|
-
"`reason_for_recording_#{plural_for(category)}_by_default` is blank. Say in one " \
|
|
891
|
-
"plain sentence why the application needs it. If only some policies need it, " \
|
|
892
|
-
"turn the default off and enable it in those policies instead."
|
|
893
|
-
end
|
|
894
|
-
|
|
895
969
|
if ReviewedText.placeholder?(reason)
|
|
896
970
|
raise ConfigurationError,
|
|
897
971
|
"Clickwrap is set to record #{category} for every policy by default, but its " \
|
|
898
972
|
"reason is still scaffolding text (#{reason.inspect}). Replace it with the " \
|
|
899
|
-
"application's reviewed, present-tense reason, or
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
if delete_after.present? && keeps_recorded_request_evidence_indefinitely?(category)
|
|
903
|
-
raise ConfigurationError,
|
|
904
|
-
"Clickwrap is told both to delete recorded #{category} after " \
|
|
905
|
-
"#{delete_after.inspect} and to keep it indefinitely. Those are opposite " \
|
|
906
|
-
"decisions — keep exactly one."
|
|
973
|
+
"application's reviewed, present-tense reason, or delete the line and let " \
|
|
974
|
+
"Clickwrap record its own stated purpose."
|
|
907
975
|
end
|
|
908
976
|
|
|
909
|
-
next
|
|
977
|
+
next unless delete_after.present? && keeps_recorded_request_evidence_indefinitely?(category)
|
|
910
978
|
|
|
911
979
|
raise ConfigurationError,
|
|
912
|
-
"Clickwrap is
|
|
913
|
-
"
|
|
914
|
-
"
|
|
915
|
-
"evidence it corroborates with " \
|
|
916
|
-
"`keep_recorded_#{plural_for(category)}_indefinitely!(because: \"…\")` — or turn " \
|
|
917
|
-
"the default off and let each policy choose its own retention rule."
|
|
980
|
+
"Clickwrap is told both to delete recorded #{category} after " \
|
|
981
|
+
"#{delete_after.inspect} and to keep it indefinitely. Those are opposite " \
|
|
982
|
+
"decisions — keep exactly one."
|
|
918
983
|
end
|
|
919
984
|
end
|
|
920
985
|
|
|
@@ -926,41 +991,27 @@ module Clickwrap
|
|
|
926
991
|
end
|
|
927
992
|
end
|
|
928
993
|
|
|
929
|
-
def
|
|
930
|
-
|
|
931
|
-
raise ConfigurationError,
|
|
932
|
-
"keep_recorded_#{plural_for(category)}_indefinitely! needs a `because:` " \
|
|
933
|
-
"explaining the reviewed decision."
|
|
934
|
-
end
|
|
935
|
-
|
|
936
|
-
@keep_recorded_request_evidence_indefinitely[category] = because
|
|
994
|
+
def host_reason_for_recording_by_default(category)
|
|
995
|
+
public_send(:"reason_for_recording_#{plural_for(category.to_sym)}_by_default").presence
|
|
937
996
|
end
|
|
938
997
|
|
|
939
|
-
def
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
return unless records_ip_derived_evidence
|
|
943
|
-
return if trusted_proxy_configuration_digest.present?
|
|
944
|
-
|
|
945
|
-
raise ConfigurationError,
|
|
946
|
-
"Clickwrap is set to record an IP address or derive IP geolocation for every " \
|
|
947
|
-
"policy, but `trusted_proxy_configuration_digest` is blank. Review and test the " \
|
|
948
|
-
"deployment's trusted-proxy topology, digest that exact configuration, and set " \
|
|
949
|
-
"the complete prefixed SHA-2 digest (for example `sha256:...`). This records which " \
|
|
950
|
-
"proxy decision produced the address; it does not claim that decision was correct."
|
|
998
|
+
def declare_indefinite_request_evidence!(category, because)
|
|
999
|
+
@keep_recorded_request_evidence_indefinitely[category] =
|
|
1000
|
+
because.presence || Vocabulary::DEFAULT_REASON_FOR_KEEPING_REQUEST_EVIDENCE_INDEFINITELY
|
|
951
1001
|
end
|
|
952
1002
|
|
|
953
1003
|
def validate_ip_geolocation_resolver!
|
|
954
|
-
return if ip_geolocation_resolver
|
|
955
1004
|
return if enabled_default_ip_geolocation_fields.empty? && !fail_capture_when_ip_geolocation_is_unavailable
|
|
1005
|
+
return if application_default_ip_geolocation_resolver
|
|
956
1006
|
|
|
957
1007
|
if enabled_default_ip_geolocation_fields.any?
|
|
958
1008
|
raise ConfigurationError,
|
|
959
1009
|
"Clickwrap is set to record the IP-geolocation fields " \
|
|
960
1010
|
"#{enabled_default_ip_geolocation_fields.join(", ")} but no " \
|
|
961
|
-
"`ip_geolocation_resolver` is configured
|
|
962
|
-
"
|
|
963
|
-
"
|
|
1011
|
+
"`ip_geolocation_resolver` is configured and the `trackdown` gem is not " \
|
|
1012
|
+
"installed, so there is nothing to resolve them. Run " \
|
|
1013
|
+
"`bundle add trackdown --version \">= 0.4\"` and Clickwrap will use it, set " \
|
|
1014
|
+
"`config.ip_geolocation_resolver` to your own adapter, or turn the fields off."
|
|
964
1015
|
end
|
|
965
1016
|
|
|
966
1017
|
raise ConfigurationError,
|
|
@@ -968,6 +1019,22 @@ module Clickwrap
|
|
|
968
1019
|
"`ip_geolocation_resolver` is configured, so every capture would fail."
|
|
969
1020
|
end
|
|
970
1021
|
|
|
1022
|
+
# Considered once, at the first moment something actually needs geolocation
|
|
1023
|
+
# resolved, and remembered either way — including the "no trackdown here"
|
|
1024
|
+
# answer, so a host without it does not pay for a failed `require` on every
|
|
1025
|
+
# policy compile.
|
|
1026
|
+
def automatically_adopted_ip_geolocation_resolver
|
|
1027
|
+
return @automatically_adopted_ip_geolocation_resolver if @considered_automatic_ip_geolocation_resolver
|
|
1028
|
+
|
|
1029
|
+
# Remembered only once the adapter has actually been built. An installed
|
|
1030
|
+
# trackdown too old to use raises out of here, and a host who fixes their
|
|
1031
|
+
# bundle and asks again must not be told the gem is missing because a
|
|
1032
|
+
# failed attempt got memoized as "no".
|
|
1033
|
+
resolver = (IpGeolocation::TrackdownResolver.new if IpGeolocation::TrackdownResolver.installed?)
|
|
1034
|
+
@considered_automatic_ip_geolocation_resolver = true
|
|
1035
|
+
@automatically_adopted_ip_geolocation_resolver = resolver
|
|
1036
|
+
end
|
|
1037
|
+
|
|
971
1038
|
# --- Setter helpers -------------------------------------------------------
|
|
972
1039
|
|
|
973
1040
|
def ensure_callable(value, name)
|
|
@@ -1118,24 +1185,25 @@ module Clickwrap
|
|
|
1118
1185
|
|
|
1119
1186
|
public
|
|
1120
1187
|
|
|
1121
|
-
#
|
|
1122
|
-
#
|
|
1123
|
-
#
|
|
1124
|
-
#
|
|
1125
|
-
#
|
|
1126
|
-
#
|
|
1127
|
-
#
|
|
1128
|
-
#
|
|
1129
|
-
#
|
|
1130
|
-
|
|
1188
|
+
# Says out loud what an absent deletion clock already means: this category
|
|
1189
|
+
# keeps pace with the evidence it corroborates. Request evidence exists to
|
|
1190
|
+
# corroborate evidence that (since 0.2.0) keeps indefinitely by default,
|
|
1191
|
+
# and a corroboration that expires before the thing it corroborates is a
|
|
1192
|
+
# scheduled weakening of the record.
|
|
1193
|
+
#
|
|
1194
|
+
# Saying it explicitly is worth doing — it puts the decision and its reason
|
|
1195
|
+
# in the initializer where a reviewer finds them — but since 0.3.0 it is no
|
|
1196
|
+
# longer the price of admission, and `because:` is optional. What a host
|
|
1197
|
+
# writes is kept as their own words; what they leave out gets Clickwrap's.
|
|
1198
|
+
def keep_recorded_ip_addresses_indefinitely!(because: nil)
|
|
1131
1199
|
declare_indefinite_request_evidence!(:ip_address, because)
|
|
1132
1200
|
end
|
|
1133
1201
|
|
|
1134
|
-
def keep_recorded_browser_user_agents_indefinitely!(because:)
|
|
1202
|
+
def keep_recorded_browser_user_agents_indefinitely!(because: nil)
|
|
1135
1203
|
declare_indefinite_request_evidence!(:browser_user_agent, because)
|
|
1136
1204
|
end
|
|
1137
1205
|
|
|
1138
|
-
def keep_recorded_ip_geolocation_indefinitely!(because:)
|
|
1206
|
+
def keep_recorded_ip_geolocation_indefinitely!(because: nil)
|
|
1139
1207
|
declare_indefinite_request_evidence!(:ip_geolocation, because)
|
|
1140
1208
|
end
|
|
1141
1209
|
|
|
@@ -1143,19 +1211,35 @@ module Clickwrap
|
|
|
1143
1211
|
@keep_recorded_request_evidence_indefinitely.key?(category.to_sym)
|
|
1144
1212
|
end
|
|
1145
1213
|
|
|
1214
|
+
# The purpose that will actually be recorded for a category enabled
|
|
1215
|
+
# application-wide, and which of the two wrote it. The inventory reports
|
|
1216
|
+
# both, so a reviewer can tell a sentence their team signed off on from the
|
|
1217
|
+
# one the gem supplied.
|
|
1218
|
+
def reason_for_recording_by_default(category)
|
|
1219
|
+
host_reason_for_recording_by_default(category) || Vocabulary::DEFAULT_REQUEST_EVIDENCE_PURPOSE
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
def reason_for_recording_by_default_source(category)
|
|
1223
|
+
host_reason_for_recording_by_default(category) ? "host" : "gem_default"
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1146
1226
|
def reason_for_keeping_recorded_request_evidence_indefinitely(category)
|
|
1147
1227
|
@keep_recorded_request_evidence_indefinitely[category.to_sym]
|
|
1148
1228
|
end
|
|
1149
1229
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1230
|
+
# The named escape hatch for turning encryption off. The ceremony is the
|
|
1231
|
+
# method: you cannot reach `encrypt_recorded_* = false` without writing a
|
|
1232
|
+
# line that says out loud what you are doing, and that line is what a
|
|
1233
|
+
# reviewer finds in a diff. Since 0.3.1 the `because:` is optional — the
|
|
1234
|
+
# gem records its own sentence when you do not write one — because the
|
|
1235
|
+
# host's privacy policy owns the why, and demanding it twice never stopped
|
|
1236
|
+
# anybody who had already typed this method name.
|
|
1237
|
+
#
|
|
1238
|
+
# Encryption itself is unchanged: on by default, for all three categories.
|
|
1239
|
+
def deliberately_store_request_evidence_unencrypted!(because: nil)
|
|
1157
1240
|
@deliberately_storing_request_evidence_unencrypted = true
|
|
1158
|
-
@reason_for_storing_request_evidence_unencrypted =
|
|
1241
|
+
@reason_for_storing_request_evidence_unencrypted =
|
|
1242
|
+
because.presence || Vocabulary::DEFAULT_REASON_FOR_STORING_REQUEST_EVIDENCE_UNENCRYPTED
|
|
1159
1243
|
end
|
|
1160
1244
|
|
|
1161
1245
|
def storing_request_evidence_unencrypted? = @deliberately_storing_request_evidence_unencrypted == true
|
data/lib/clickwrap/doctor.rb
CHANGED
|
@@ -271,7 +271,8 @@ module Clickwrap
|
|
|
271
271
|
end
|
|
272
272
|
|
|
273
273
|
def resolver_findings
|
|
274
|
-
|
|
274
|
+
config = Clickwrap.config
|
|
275
|
+
configured = config.ip_geolocation_resolver_in_force
|
|
275
276
|
wanted = Clickwrap.policies.values.select { |policy| policy.request_evidence.records_ip_geolocation? }
|
|
276
277
|
|
|
277
278
|
if configured.nil?
|
|
@@ -285,7 +286,11 @@ module Clickwrap
|
|
|
285
286
|
"as unavailable")]
|
|
286
287
|
end
|
|
287
288
|
|
|
288
|
-
[ok("an IP-geolocation resolver is configured (#{configured.class.name})")]
|
|
289
|
+
return [ok("an IP-geolocation resolver is configured (#{configured.class.name})")] unless
|
|
290
|
+
config.ip_geolocation_resolver_was_adopted_automatically?
|
|
291
|
+
|
|
292
|
+
[ok("IP geolocation resolves through #{configured.class.name}, which Clickwrap adopted " \
|
|
293
|
+
"because this application bundles trackdown and named no resolver of its own")]
|
|
289
294
|
end
|
|
290
295
|
|
|
291
296
|
# An IP address read from a forwarded header is only as good as the proxy
|
|
@@ -239,24 +239,34 @@ module Clickwrap
|
|
|
239
239
|
@request_evidence[:browser_user_agent] = RequestEvidencePolicy::NOT_RECORDED
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
-
# Each IP-geolocation data field
|
|
243
|
-
# a separate decision about what to keep about a person's network
|
|
242
|
+
# Each IP-geolocation data field can be named separately, because each one
|
|
243
|
+
# is a separate decision about what to keep about a person's network
|
|
244
244
|
# context. `latitude_and_longitude` is one coupled choice: half a
|
|
245
245
|
# coordinate is not a result. Whatever is enabled, the provider name,
|
|
246
246
|
# source, estimated status, resolution time, and any accuracy or database
|
|
247
247
|
# provenance the resolver supplies are stored with it automatically — a
|
|
248
248
|
# policy cannot keep the coordinates and drop the uncertainty needed to
|
|
249
249
|
# read them.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
#
|
|
251
|
+
# Naming no field at all is the frictionless form:
|
|
252
|
+
#
|
|
253
|
+
# record_ip_geolocation
|
|
254
|
+
#
|
|
255
|
+
# It records the same coarse trio as
|
|
256
|
+
# `config.record_request_evidence_by_default` — country, region, city —
|
|
257
|
+
# and nothing finer, because that is what "IP geolocation" means in this
|
|
258
|
+
# gem when nobody narrows it. Naming even one field means you are
|
|
259
|
+
# choosing the set yourself, and then the set is exactly what you named.
|
|
260
|
+
def record_ip_geolocation(country: nil, region: nil, city: nil, postal_code: nil,
|
|
261
|
+
latitude_and_longitude: nil, timezone: nil, continent: nil,
|
|
262
|
+
metro_code: nil, accuracy_radius_in_kilometers: nil,
|
|
253
263
|
using: nil, encrypted: nil, delete_after: nil, retain_until: nil,
|
|
254
264
|
fail_if_unavailable: false, because: nil,
|
|
255
265
|
legal_basis_reference: nil,
|
|
256
266
|
data_protection_impact_assessment_reference: nil,
|
|
257
267
|
**unknown_options)
|
|
258
268
|
refuse_unknown_options!("record_ip_geolocation", unknown_options)
|
|
259
|
-
|
|
269
|
+
named = {
|
|
260
270
|
"country" => country,
|
|
261
271
|
"region" => region,
|
|
262
272
|
"city" => city,
|
|
@@ -267,6 +277,7 @@ module Clickwrap
|
|
|
267
277
|
"metro_code" => metro_code,
|
|
268
278
|
"accuracy_radius_in_kilometers" => accuracy_radius_in_kilometers
|
|
269
279
|
}
|
|
280
|
+
@ip_geolocation_fields = default_ip_geolocation_fields_when_none_named(named)
|
|
270
281
|
@ip_geolocation_resolver_name = using
|
|
271
282
|
|
|
272
283
|
@request_evidence[:ip_geolocation] = RequestEvidencePolicy::Setting.new(
|
|
@@ -404,6 +415,20 @@ module Clickwrap
|
|
|
404
415
|
end
|
|
405
416
|
end
|
|
406
417
|
|
|
418
|
+
# `nil` means "the policy did not mention this field"; `false` means "the
|
|
419
|
+
# policy named it and turned it off". The distinction is the whole reason
|
|
420
|
+
# the keywords default to nil: a policy that mentions nothing gets the
|
|
421
|
+
# coarse trio, and a policy that explicitly sets every field to false
|
|
422
|
+
# still reaches the coherence check that tells it to say
|
|
423
|
+
# `do_not_record_ip_geolocation` instead.
|
|
424
|
+
def default_ip_geolocation_fields_when_none_named(named)
|
|
425
|
+
return named.transform_values { |value| value == true } if named.any? { |_, value| !value.nil? }
|
|
426
|
+
|
|
427
|
+
Vocabulary::IP_GEOLOCATION_DATA_FIELDS.to_h do |field|
|
|
428
|
+
[field, Vocabulary::COARSE_IP_GEOLOCATION_DATA_FIELDS.include?(field)]
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
407
432
|
def resolved_ip_geolocation_fields(setting)
|
|
408
433
|
return {} unless setting.record?
|
|
409
434
|
return @ip_geolocation_fields if @request_evidence.key?(:ip_geolocation)
|
|
@@ -83,6 +83,22 @@ module Clickwrap
|
|
|
83
83
|
|
|
84
84
|
attr_reader :capabilities
|
|
85
85
|
|
|
86
|
+
# Whether the host's bundle carries `trackdown` at all. The configuration
|
|
87
|
+
# asks this before adopting the adapter for a policy that enabled
|
|
88
|
+
# IP-geolocation fields without naming a resolver, so the common case —
|
|
89
|
+
# trackdown plus Cloudflare, already bundled — needs no wiring line.
|
|
90
|
+
#
|
|
91
|
+
# It answers the narrow question it is named after and nothing else. An
|
|
92
|
+
# installed release older than 0.4 answers `true` here and then fails in
|
|
93
|
+
# the constructor with the sentence about upgrading, which is far more
|
|
94
|
+
# useful to that host than being told the gem is missing.
|
|
95
|
+
def self.installed?
|
|
96
|
+
require "trackdown" unless defined?(::Trackdown)
|
|
97
|
+
true
|
|
98
|
+
rescue ::LoadError
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
|
|
86
102
|
# Trust is per request in Trackdown 0.4. A host registers its verifier with
|
|
87
103
|
# Trackdown, Trackdown runs it against the same request that supplied the
|
|
88
104
|
# CDN headers, and this adapter copies the result's explicit trust state.
|