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.
@@ -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 — unless the initializer or a policy names that exact field. There is no
9
- category switch, no profile, and no option that turns one of these on as a side effect of
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
- That default is evidence design, not squeamishness. Three things follow from it:
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.** Each field is its own setting, and each is `false`:
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
- Turning a default on without a purpose or without a deletion period is a `ConfigurationError`
246
- at the end of the `configure` block, not a warning. Enabling any
247
- `record_ip_geolocation_*_by_default` without an `ip_geolocation_resolver` is likewise a
248
- `ConfigurationError` — there would be nothing to resolve them.
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, printed by `bin/rails clickwrap:privacy:inventory`, and required.
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 endorse them.
300
- - **`delete_after:`** or **`retain_until:`** is not optional. If a policy records a field and
301
- neither the policy nor the configuration says when it goes away, capture raises a
302
- `ConfigurationError` before the row is written. There is no keep-forever default in this gem.
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, and turning one on without a purpose and a retention
25
- # decision is a configuration error, not a warning.
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`. An option that silently enables a category of personal data
30
- # is exactly the thing this gem exists not to do, and no runtime flag can
31
- # make a legal determination on your behalf.
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 ip_geolocation_resolver if name.blank? || name.to_s == "application_default"
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 turn that default off."
900
- end
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 if delete_after.present? || keeps_recorded_request_evidence_indefinitely?(category)
977
+ next unless delete_after.present? && keeps_recorded_request_evidence_indefinitely?(category)
910
978
 
911
979
  raise ConfigurationError,
912
- "Clickwrap is set to record #{category} for every policy by default, but nothing " \
913
- "says how long to keep it. Either set a reviewed period with " \
914
- "`delete_recorded_#{plural_for(category)}_after`, or keep it as long as the " \
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 declare_indefinite_request_evidence!(category, because)
930
- if because.to_s.strip.empty?
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 validate_trusted_proxy_configuration!
940
- records_ip_derived_evidence =
941
- record_ip_address_by_default || enabled_default_ip_geolocation_fields.any?
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, so there is nothing to resolve them. " \
962
- "Set one (for example Clickwrap::IpGeolocation::TrackdownResolver.new) or turn " \
963
- "the fields off."
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
- # The deliberate, named escape hatch referenced by `ensure_encryption_choice`.
1122
- # It exists so that turning encryption off is a sentence a reviewer can find
1123
- # in a diff, with the host's own reason attached, rather than a `false`.
1124
- # The named escape hatch for by-default request evidence with no deletion
1125
- # clock: request evidence exists to corroborate evidence that (since 0.2.0)
1126
- # keeps indefinitely by default, and a corroboration that expires before
1127
- # the thing it corroborates is a scheduled weakening of the record. Same
1128
- # rule as every escape hatch here: keeping forever must be a sentence a
1129
- # reviewer can find in a diff, with the host's own reason attached.
1130
- def keep_recorded_ip_addresses_indefinitely!(because:)
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
- def deliberately_store_request_evidence_unencrypted!(because:)
1151
- if because.to_s.strip.empty?
1152
- raise ConfigurationError,
1153
- "deliberately_store_request_evidence_unencrypted! needs a `because:` explaining " \
1154
- "the reviewed decision."
1155
- end
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 = because
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
@@ -271,7 +271,8 @@ module Clickwrap
271
271
  end
272
272
 
273
273
  def resolver_findings
274
- configured = Clickwrap.config.ip_geolocation_resolver
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 is named separately, because each one is
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
- 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,
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
- @ip_geolocation_fields = {
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.