clickwrap 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +141 -2
- data/README.md +70 -14
- data/guides/integrating.md +14 -10
- data/guides/naming.md +13 -5
- data/guides/request-evidence.md +57 -13
- data/lib/clickwrap/configuration.rb +169 -40
- data/lib/clickwrap/doctor.rb +7 -2
- data/lib/clickwrap/dsl/retention_builder.rb +15 -0
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +16 -0
- data/lib/clickwrap/privacy.rb +28 -7
- data/lib/clickwrap/request_evidence_extractor.rb +17 -14
- data/lib/clickwrap/request_evidence_policy.rb +40 -36
- data/lib/clickwrap/services/validate_policy_references.rb +7 -15
- data/lib/clickwrap/version.rb +1 -1
- data/lib/clickwrap/vocabulary.rb +27 -0
- data/lib/generators/clickwrap/install_generator.rb +7 -3
- data/lib/generators/clickwrap/templates/initializer.rb.erb +40 -22
- metadata +2 -2
|
@@ -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
|
|
@@ -228,10 +232,14 @@ module Clickwrap
|
|
|
228
232
|
@encrypt_recorded_ip_geolocation = true
|
|
229
233
|
|
|
230
234
|
# nil means "every policy that enables the field must supply its own
|
|
231
|
-
# rule"
|
|
235
|
+
# rule" — or, for by-default recording, that the host has said
|
|
236
|
+
# `keep_recorded_..._indefinitely!(because: "…")` out loud. Keeping
|
|
237
|
+
# forever is never silent: it is either the per-policy retention class's
|
|
238
|
+
# explicit business, or a named, reasoned sentence in the initializer.
|
|
232
239
|
@delete_recorded_ip_addresses_after = nil
|
|
233
240
|
@delete_recorded_browser_user_agents_after = nil
|
|
234
241
|
@delete_recorded_ip_geolocation_after = nil
|
|
242
|
+
@keep_recorded_request_evidence_indefinitely = {}
|
|
235
243
|
|
|
236
244
|
# Rails' request.remote_ip is the conventional reader. The host remains
|
|
237
245
|
# responsible for configuring and testing trusted proxies correctly:
|
|
@@ -242,6 +250,8 @@ module Clickwrap
|
|
|
242
250
|
|
|
243
251
|
@ip_geolocation_resolver = nil
|
|
244
252
|
@ip_geolocation_resolvers = {}
|
|
253
|
+
@automatically_adopted_ip_geolocation_resolver = nil
|
|
254
|
+
@considered_automatic_ip_geolocation_resolver = false
|
|
245
255
|
@fail_capture_when_ip_geolocation_is_unavailable = false
|
|
246
256
|
|
|
247
257
|
# The keyed annex digest carries a key ID so a host can rotate keys
|
|
@@ -608,6 +618,49 @@ module Clickwrap
|
|
|
608
618
|
|
|
609
619
|
# --- Request-evidence setters --------------------------------------------
|
|
610
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
|
+
|
|
611
664
|
def record_ip_address_by_default=(value)
|
|
612
665
|
@record_ip_address_by_default = ensure_boolean(value, "record_ip_address_by_default")
|
|
613
666
|
end
|
|
@@ -711,11 +764,36 @@ module Clickwrap
|
|
|
711
764
|
end
|
|
712
765
|
|
|
713
766
|
def ip_geolocation_resolver_for(name = nil)
|
|
714
|
-
return
|
|
767
|
+
return application_default_ip_geolocation_resolver if name.blank? || name.to_s == "application_default"
|
|
715
768
|
|
|
716
769
|
@ip_geolocation_resolvers[name.to_s]
|
|
717
770
|
end
|
|
718
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
|
+
|
|
719
797
|
def ip_geolocation_resolver_names = @ip_geolocation_resolvers.keys.sort.freeze
|
|
720
798
|
|
|
721
799
|
# Plain-English key-rotation API. The ID is evidence and must stay stable;
|
|
@@ -838,7 +916,6 @@ module Clickwrap
|
|
|
838
916
|
# caught the typos; these are the things that need the whole block resolved.
|
|
839
917
|
def validate!
|
|
840
918
|
validate_request_evidence_defaults!
|
|
841
|
-
validate_trusted_proxy_configuration!
|
|
842
919
|
validate_ip_geolocation_resolver!
|
|
843
920
|
true
|
|
844
921
|
end
|
|
@@ -866,6 +943,15 @@ module Clickwrap
|
|
|
866
943
|
Reference.record(actor)
|
|
867
944
|
end
|
|
868
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.
|
|
869
955
|
def validate_request_evidence_defaults!
|
|
870
956
|
{
|
|
871
957
|
ip_address: [record_ip_address_by_default,
|
|
@@ -880,28 +966,20 @@ module Clickwrap
|
|
|
880
966
|
}.each do |category, (enabled, reason, delete_after)|
|
|
881
967
|
next unless enabled
|
|
882
968
|
|
|
883
|
-
if reason.to_s.strip.empty?
|
|
884
|
-
raise ConfigurationError,
|
|
885
|
-
"Clickwrap is set to record #{category} for every policy by default, but " \
|
|
886
|
-
"`reason_for_recording_#{plural_for(category)}_by_default` is blank. Say in one " \
|
|
887
|
-
"plain sentence why the application needs it. If only some policies need it, " \
|
|
888
|
-
"turn the default off and enable it in those policies instead."
|
|
889
|
-
end
|
|
890
|
-
|
|
891
969
|
if ReviewedText.placeholder?(reason)
|
|
892
970
|
raise ConfigurationError,
|
|
893
971
|
"Clickwrap is set to record #{category} for every policy by default, but its " \
|
|
894
972
|
"reason is still scaffolding text (#{reason.inspect}). Replace it with the " \
|
|
895
|
-
"application's reviewed, present-tense reason, or
|
|
973
|
+
"application's reviewed, present-tense reason, or delete the line and let " \
|
|
974
|
+
"Clickwrap record its own stated purpose."
|
|
896
975
|
end
|
|
897
976
|
|
|
898
|
-
next unless delete_after.
|
|
977
|
+
next unless delete_after.present? && keeps_recorded_request_evidence_indefinitely?(category)
|
|
899
978
|
|
|
900
979
|
raise ConfigurationError,
|
|
901
|
-
"Clickwrap is
|
|
902
|
-
"
|
|
903
|
-
"
|
|
904
|
-
"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."
|
|
905
983
|
end
|
|
906
984
|
end
|
|
907
985
|
|
|
@@ -913,31 +991,27 @@ module Clickwrap
|
|
|
913
991
|
end
|
|
914
992
|
end
|
|
915
993
|
|
|
916
|
-
def
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
return unless records_ip_derived_evidence
|
|
920
|
-
return if trusted_proxy_configuration_digest.present?
|
|
994
|
+
def host_reason_for_recording_by_default(category)
|
|
995
|
+
public_send(:"reason_for_recording_#{plural_for(category.to_sym)}_by_default").presence
|
|
996
|
+
end
|
|
921
997
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
"deployment's trusted-proxy topology, digest that exact configuration, and set " \
|
|
926
|
-
"the complete prefixed SHA-2 digest (for example `sha256:...`). This records which " \
|
|
927
|
-
"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
|
|
928
1001
|
end
|
|
929
1002
|
|
|
930
1003
|
def validate_ip_geolocation_resolver!
|
|
931
|
-
return if ip_geolocation_resolver
|
|
932
1004
|
return if enabled_default_ip_geolocation_fields.empty? && !fail_capture_when_ip_geolocation_is_unavailable
|
|
1005
|
+
return if application_default_ip_geolocation_resolver
|
|
933
1006
|
|
|
934
1007
|
if enabled_default_ip_geolocation_fields.any?
|
|
935
1008
|
raise ConfigurationError,
|
|
936
1009
|
"Clickwrap is set to record the IP-geolocation fields " \
|
|
937
1010
|
"#{enabled_default_ip_geolocation_fields.join(", ")} but no " \
|
|
938
|
-
"`ip_geolocation_resolver` is configured
|
|
939
|
-
"
|
|
940
|
-
"
|
|
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."
|
|
941
1015
|
end
|
|
942
1016
|
|
|
943
1017
|
raise ConfigurationError,
|
|
@@ -945,6 +1019,22 @@ module Clickwrap
|
|
|
945
1019
|
"`ip_geolocation_resolver` is configured, so every capture would fail."
|
|
946
1020
|
end
|
|
947
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
|
+
|
|
948
1038
|
# --- Setter helpers -------------------------------------------------------
|
|
949
1039
|
|
|
950
1040
|
def ensure_callable(value, name)
|
|
@@ -1095,9 +1185,48 @@ module Clickwrap
|
|
|
1095
1185
|
|
|
1096
1186
|
public
|
|
1097
1187
|
|
|
1098
|
-
#
|
|
1099
|
-
#
|
|
1100
|
-
#
|
|
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)
|
|
1199
|
+
declare_indefinite_request_evidence!(:ip_address, because)
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
def keep_recorded_browser_user_agents_indefinitely!(because: nil)
|
|
1203
|
+
declare_indefinite_request_evidence!(:browser_user_agent, because)
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1206
|
+
def keep_recorded_ip_geolocation_indefinitely!(because: nil)
|
|
1207
|
+
declare_indefinite_request_evidence!(:ip_geolocation, because)
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
def keeps_recorded_request_evidence_indefinitely?(category)
|
|
1211
|
+
@keep_recorded_request_evidence_indefinitely.key?(category.to_sym)
|
|
1212
|
+
end
|
|
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
|
+
|
|
1226
|
+
def reason_for_keeping_recorded_request_evidence_indefinitely(category)
|
|
1227
|
+
@keep_recorded_request_evidence_indefinitely[category.to_sym]
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1101
1230
|
def deliberately_store_request_evidence_unencrypted!(because:)
|
|
1102
1231
|
if because.to_s.strip.empty?
|
|
1103
1232
|
raise ConfigurationError,
|
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
|
|
@@ -69,6 +69,21 @@ module Clickwrap
|
|
|
69
69
|
assign_rule!(:ip_geolocation, host_event_name:)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# Keeping the annex as long as the core event it corroborates, said in
|
|
73
|
+
# the retention class itself. A corroboration that expires before the
|
|
74
|
+
# evidence it corroborates is a scheduled weakening of the record.
|
|
75
|
+
def keep_recorded_ip_address_indefinitely
|
|
76
|
+
assign_rule!(:ip_address, indefinite: true)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def keep_recorded_browser_user_agent_indefinitely
|
|
80
|
+
assign_rule!(:browser_user_agent, indefinite: true)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def keep_recorded_ip_geolocation_indefinitely
|
|
84
|
+
assign_rule!(:ip_geolocation, indefinite: true)
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
def compile = RetentionClass.new(key: @key, rules: @rules)
|
|
73
88
|
|
|
74
89
|
private
|
|
@@ -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.
|
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
|
|
@@ -483,10 +486,18 @@ module Clickwrap
|
|
|
483
486
|
|
|
484
487
|
# --- Retention ------------------------------------------------------------
|
|
485
488
|
|
|
486
|
-
# Every recorded field leaves here with a disposal
|
|
487
|
-
# of a host rule that will produce one
|
|
488
|
-
#
|
|
489
|
-
#
|
|
489
|
+
# Every recorded field leaves here with a disposal answer: a date, the name
|
|
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.
|
|
490
501
|
#
|
|
491
502
|
# `retain_until` names a host calculation instead of a duration because real
|
|
492
503
|
# record-keeping schedules are not always durations — "five years, or three
|
|
@@ -500,7 +511,7 @@ module Clickwrap
|
|
|
500
511
|
return { "#{category}_delete_after": now + class_rule.duration } if class_rule&.duration?
|
|
501
512
|
return { "#{category}_retain_until_rule": class_rule.host_event_name.to_s } if class_rule&.host_event?
|
|
502
513
|
|
|
503
|
-
|
|
514
|
+
{}
|
|
504
515
|
end
|
|
505
516
|
|
|
506
517
|
def retention_class_rule_for(category)
|
|
@@ -509,14 +520,6 @@ module Clickwrap
|
|
|
509
520
|
Clickwrap.retention_class!(policy.retention_class_key).rule_for(category)
|
|
510
521
|
end
|
|
511
522
|
|
|
512
|
-
def missing_retention_message(category)
|
|
513
|
-
"Clickwrap is about to record #{category} for policy #{policy_key} and nothing says when " \
|
|
514
|
-
"to delete it. Give the policy a rule — `delete_after:` with a reviewed period, or " \
|
|
515
|
-
"`retain_until:` naming a host retention calculation — or add a #{category} rule to " \
|
|
516
|
-
"retention class #{policy.retention_class_key.inspect}. Clickwrap has no keep-forever " \
|
|
517
|
-
"default and will not choose a period for you."
|
|
518
|
-
end
|
|
519
|
-
|
|
520
523
|
# --- Failing closed -------------------------------------------------------
|
|
521
524
|
|
|
522
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,31 +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
|
-
raise DefinitionError,
|
|
210
|
-
"Policy #{policy_key} records #{category} but never says when to delete it. " \
|
|
211
|
-
"Give it `delete_after:` with a duration, or `retain_until:` naming a host event " \
|
|
212
|
-
"rule, or attach a retention class with a rule for this category. Clickwrap has " \
|
|
213
|
-
"no keep-forever default."
|
|
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."
|
|
214
230
|
end
|
|
215
231
|
|
|
216
232
|
return unless setting.delete_after && setting.delete_after.to_i <= 0
|
|
@@ -245,17 +261,5 @@ module Clickwrap
|
|
|
245
261
|
"Policy #{policy_key} enables the IP-geolocation fields #{enabled.join(", ")} " \
|
|
246
262
|
"without recording IP geolocation."
|
|
247
263
|
end
|
|
248
|
-
|
|
249
|
-
def validate_trusted_proxy_configuration_digest!
|
|
250
|
-
return unless records_ip_address? || records_ip_geolocation?
|
|
251
|
-
return unless trusted_proxy_configuration_digest.to_s.strip.empty?
|
|
252
|
-
|
|
253
|
-
raise DefinitionError,
|
|
254
|
-
"Policy #{policy_key} records an IP address or derives IP geolocation, but " \
|
|
255
|
-
"`config.trusted_proxy_configuration_digest` is blank. Review the deployment's " \
|
|
256
|
-
"trusted-proxy topology, digest that reviewed configuration, and set the prefixed " \
|
|
257
|
-
"digest (for example `sha256:...`). This records which proxy decision produced the " \
|
|
258
|
-
"address; it does not claim that the decision was correct."
|
|
259
|
-
end
|
|
260
264
|
end
|
|
261
265
|
end
|