clickwrap 0.0.0 → 0.1.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/.rubocop.yml +90 -0
- data/CHANGELOG.md +612 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +241 -0
- data/app/controllers/clickwrap/application_controller.rb +79 -0
- data/app/controllers/clickwrap/captures_controller.rb +145 -0
- data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
- data/app/controllers/clickwrap/receipts_controller.rb +115 -0
- data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
- data/app/helpers/clickwrap/engine_helper.rb +97 -0
- data/app/views/clickwrap/captures/show.html.erb +34 -0
- data/app/views/clickwrap/receipts/index.html.erb +38 -0
- data/app/views/clickwrap/receipts/show.html.erb +91 -0
- data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
- data/app/views/clickwrap/shared/_fields.html.erb +100 -0
- data/app/views/clickwrap/shared/_statement.html.erb +105 -0
- data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
- data/config/locales/en.yml +160 -0
- data/config/locales/es.yml +138 -0
- data/config/routes.rb +41 -0
- data/exe/clickwrap +374 -0
- data/guides/README.md +30 -0
- data/guides/accessibility.md +249 -0
- data/guides/consent-and-lifecycle.md +299 -0
- data/guides/integrating.md +610 -0
- data/guides/integrity.md +212 -0
- data/guides/migrating.md +335 -0
- data/guides/naming.md +320 -0
- data/guides/organizations.md +320 -0
- data/guides/receipts-and-verification.md +415 -0
- data/guides/request-evidence.md +512 -0
- data/guides/retention-and-legal-holds.md +438 -0
- data/lib/clickwrap/actor_proxy.rb +147 -0
- data/lib/clickwrap/anonymous_actor.rb +47 -0
- data/lib/clickwrap/authority.rb +174 -0
- data/lib/clickwrap/canonical_json.rb +216 -0
- data/lib/clickwrap/capture/event_builder.rb +220 -0
- data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
- data/lib/clickwrap/capture.rb +650 -0
- data/lib/clickwrap/configuration.rb +1129 -0
- data/lib/clickwrap/controller_helpers.rb +758 -0
- data/lib/clickwrap/current_state.rb +282 -0
- data/lib/clickwrap/digest.rb +125 -0
- data/lib/clickwrap/doctor.rb +418 -0
- data/lib/clickwrap/document_definition.rb +255 -0
- data/lib/clickwrap/document_renderer.rb +83 -0
- data/lib/clickwrap/document_renderers/markdown.rb +175 -0
- data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
- data/lib/clickwrap/dsl/policy_builder.rb +462 -0
- data/lib/clickwrap/dsl/retention_builder.rb +89 -0
- data/lib/clickwrap/durable_commit_callback.rb +37 -0
- data/lib/clickwrap/engine.rb +184 -0
- data/lib/clickwrap/errors.rb +181 -0
- data/lib/clickwrap/form_builder_extensions.rb +341 -0
- data/lib/clickwrap/front_matter.rb +67 -0
- data/lib/clickwrap/identifier.rb +112 -0
- data/lib/clickwrap/import/external_receipt.rb +241 -0
- data/lib/clickwrap/import/fine_print.rb +290 -0
- data/lib/clickwrap/import/legacy.rb +450 -0
- data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
- data/lib/clickwrap/integrity/anchor.rb +130 -0
- data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
- data/lib/clickwrap/integrity/attestor.rb +221 -0
- data/lib/clickwrap/integrity/chain.rb +313 -0
- data/lib/clickwrap/integrity/timestamp.rb +143 -0
- data/lib/clickwrap/ip_geolocation/location.rb +112 -0
- data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
- data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
- data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
- data/lib/clickwrap/ip_geolocation.rb +16 -0
- data/lib/clickwrap/lifecycle.rb +534 -0
- data/lib/clickwrap/linter.rb +382 -0
- data/lib/clickwrap/localized_text.rb +101 -0
- data/lib/clickwrap/macros.rb +203 -0
- data/lib/clickwrap/models/application_record.rb +20 -0
- data/lib/clickwrap/models/chain_head.rb +79 -0
- data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
- data/lib/clickwrap/models/disposition_plan.rb +208 -0
- data/lib/clickwrap/models/document.rb +46 -0
- data/lib/clickwrap/models/document_version.rb +163 -0
- data/lib/clickwrap/models/event.rb +743 -0
- data/lib/clickwrap/models/event_document.rb +79 -0
- data/lib/clickwrap/models/event_statement.rb +92 -0
- data/lib/clickwrap/models/external_action.rb +150 -0
- data/lib/clickwrap/models/integrity_attestation.rb +90 -0
- data/lib/clickwrap/models/legal_hold.rb +81 -0
- data/lib/clickwrap/models/policy_revision.rb +115 -0
- data/lib/clickwrap/models/presentation.rb +59 -0
- data/lib/clickwrap/models/receipt_access.rb +53 -0
- data/lib/clickwrap/models/recording_sequence.rb +21 -0
- data/lib/clickwrap/models/request_evidence.rb +378 -0
- data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
- data/lib/clickwrap/models/statement_state.rb +130 -0
- data/lib/clickwrap/pending_receipt.rb +177 -0
- data/lib/clickwrap/policy.rb +283 -0
- data/lib/clickwrap/presentation_manifest.rb +210 -0
- data/lib/clickwrap/presenter.rb +716 -0
- data/lib/clickwrap/privacy.rb +419 -0
- data/lib/clickwrap/protected_outcome.rb +120 -0
- data/lib/clickwrap/receipt.rb +606 -0
- data/lib/clickwrap/receipt_html.rb +235 -0
- data/lib/clickwrap/receipt_verifier.rb +978 -0
- data/lib/clickwrap/reference.rb +44 -0
- data/lib/clickwrap/registration.rb +236 -0
- data/lib/clickwrap/registry.rb +54 -0
- data/lib/clickwrap/remediation_token.rb +155 -0
- data/lib/clickwrap/request_evidence_extractor.rb +590 -0
- data/lib/clickwrap/request_evidence_policy.rb +261 -0
- data/lib/clickwrap/retention/applier.rb +231 -0
- data/lib/clickwrap/retention/disposition.rb +221 -0
- data/lib/clickwrap/retention/planner.rb +502 -0
- data/lib/clickwrap/retention_class.rb +97 -0
- data/lib/clickwrap/reviewed_text.rb +28 -0
- data/lib/clickwrap/schema_requirements.rb +196 -0
- data/lib/clickwrap/services/authorize_external_action.rb +149 -0
- data/lib/clickwrap/services/load_policies.rb +69 -0
- data/lib/clickwrap/services/publish_documents.rb +251 -0
- data/lib/clickwrap/services/validate_policy_references.rb +166 -0
- data/lib/clickwrap/statement.rb +248 -0
- data/lib/clickwrap/subject_fingerprint.rb +28 -0
- data/lib/clickwrap/submission.rb +169 -0
- data/lib/clickwrap/system_actor.rb +31 -0
- data/lib/clickwrap/test_helpers.rb +676 -0
- data/lib/clickwrap/testing.rb +211 -0
- data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
- data/lib/clickwrap/verification.rb +504 -0
- data/lib/clickwrap/version.rb +12 -1
- data/lib/clickwrap/view_helpers.rb +190 -0
- data/lib/clickwrap/vocabulary.rb +294 -0
- data/lib/clickwrap.rb +497 -7
- data/lib/generators/clickwrap/document_generator.rb +164 -0
- data/lib/generators/clickwrap/hardening_generator.rb +177 -0
- data/lib/generators/clickwrap/install_generator.rb +1287 -0
- data/lib/generators/clickwrap/link_generator.rb +56 -0
- data/lib/generators/clickwrap/policy_generator.rb +118 -0
- data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
- data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
- data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
- data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
- data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
- data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
- data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
- data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
- data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
- data/lib/generators/clickwrap/views_generator.rb +101 -0
- data/lib/tasks/clickwrap.rake +569 -0
- metadata +311 -16
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Clickwrap
|
|
6
|
+
# Fault injection, for this gem's suite and for yours.
|
|
7
|
+
#
|
|
8
|
+
# ===========================================================================
|
|
9
|
+
# The central promise of this gem is that required evidence and the protected
|
|
10
|
+
# database action commit together or not at all. A promise like that is worth
|
|
11
|
+
# exactly as much as your ability to prove it in a test, and you cannot prove
|
|
12
|
+
# it by reading the code — you prove it by making the evidence write fail on
|
|
13
|
+
# purpose and watching the account, the payout, or the withdrawal fail with
|
|
14
|
+
# it.
|
|
15
|
+
#
|
|
16
|
+
# Clickwrap::Testing.fail_next_event_write do
|
|
17
|
+
# assert_raises(Clickwrap::EventWriteFailed) { perform_signup }
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# assert_not User.exists?(email: "person@example.com")
|
|
21
|
+
# assert_no_clickwrap_event :signup
|
|
22
|
+
#
|
|
23
|
+
# The failure is injected INSIDE the `Clickwrap::Event` create, which means
|
|
24
|
+
# inside the capture's transaction, which means the host's protected action
|
|
25
|
+
# rolls back with it. Raising before the transaction opened would prove
|
|
26
|
+
# nothing at all: of course the domain action does not happen if the capture
|
|
27
|
+
# never started.
|
|
28
|
+
#
|
|
29
|
+
# Everything here installs on entry and removes on exit, in an `ensure`, so a
|
|
30
|
+
# failing assertion inside the block cannot leave a sabotage hook attached to
|
|
31
|
+
# `Clickwrap::Event` for the rest of the suite. Nothing here is left resident
|
|
32
|
+
# in a production process: the callbacks exist only while a block is running,
|
|
33
|
+
# and this file is only loaded if something references `Clickwrap::Testing`.
|
|
34
|
+
# ===========================================================================
|
|
35
|
+
module Testing
|
|
36
|
+
# Raised by `fail_next_domain_write`. Deliberately not one of the gem's
|
|
37
|
+
# real errors: it stands in for the host's own domain failure, and a test
|
|
38
|
+
# that rescued a Clickwrap error here would be testing the wrong thing.
|
|
39
|
+
class DomainWriteFailed < StandardError; end
|
|
40
|
+
|
|
41
|
+
EVENT_WRITE_KEY = :clickwrap_testing_fail_next_event_write
|
|
42
|
+
DOMAIN_WRITE_KEY = :clickwrap_testing_fail_next_domain_write
|
|
43
|
+
FROZEN_TIME_KEY = :clickwrap_testing_frozen_time
|
|
44
|
+
|
|
45
|
+
# The injected callbacks, held as constants so `skip_callback` can find the
|
|
46
|
+
# same object it was given. An anonymous lambda created per call would
|
|
47
|
+
# install fine and never come off.
|
|
48
|
+
FAIL_EVENT_WRITE = lambda do |_event|
|
|
49
|
+
next unless Testing.consume_flag!(EVENT_WRITE_KEY)
|
|
50
|
+
|
|
51
|
+
raise EventWriteFailed,
|
|
52
|
+
"Clickwrap::Testing.fail_next_event_write made this evidence write fail on purpose. " \
|
|
53
|
+
"Whatever your protected action did in this transaction must roll back with it — " \
|
|
54
|
+
"that is the property this helper exists to let you assert."
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
FAIL_DOMAIN_WRITE = lambda do |record|
|
|
58
|
+
next if record.is_a?(Clickwrap::ApplicationRecord)
|
|
59
|
+
next unless Testing.consume_flag!(DOMAIN_WRITE_KEY)
|
|
60
|
+
|
|
61
|
+
raise DomainWriteFailed,
|
|
62
|
+
"Clickwrap::Testing.fail_next_domain_write made this domain write fail on purpose. " \
|
|
63
|
+
"The Clickwrap evidence in the same transaction must roll back with it, so that a " \
|
|
64
|
+
"failed action never leaves behind a receipt saying it succeeded."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Reads the frozen server clock, if a `freeze_time_at` block is running.
|
|
68
|
+
# Prepended onto `Clickwrap`'s singleton rather than redefining `now`, so
|
|
69
|
+
# the original method stays reachable through `super` and restoring it is
|
|
70
|
+
# not a matter of remembering to.
|
|
71
|
+
module FrozenClock
|
|
72
|
+
def now
|
|
73
|
+
Testing.frozen_time || super
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class << self
|
|
78
|
+
# Makes the NEXT `Clickwrap::Event` insert raise
|
|
79
|
+
# `Clickwrap::EventWriteFailed`, from inside the create, and therefore
|
|
80
|
+
# from inside whatever transaction the capture is running in.
|
|
81
|
+
#
|
|
82
|
+
# Only the next one: a capture that legitimately retries, or a test that
|
|
83
|
+
# goes on to record a control event afterwards, is not sabotaged twice.
|
|
84
|
+
def fail_next_event_write
|
|
85
|
+
install_event_callback!
|
|
86
|
+
Thread.current[EVENT_WRITE_KEY] = true
|
|
87
|
+
|
|
88
|
+
yield
|
|
89
|
+
ensure
|
|
90
|
+
Thread.current[EVENT_WRITE_KEY] = nil
|
|
91
|
+
remove_event_callback!
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The mirror image: makes the next non-Clickwrap `ActiveRecord` save
|
|
95
|
+
# raise, so you can prove the other direction — that a domain action
|
|
96
|
+
# blowing up takes its evidence down with it, and never leaves a receipt
|
|
97
|
+
# describing something that did not happen.
|
|
98
|
+
def fail_next_domain_write
|
|
99
|
+
install_domain_callback!
|
|
100
|
+
Thread.current[DOMAIN_WRITE_KEY] = true
|
|
101
|
+
|
|
102
|
+
yield
|
|
103
|
+
ensure
|
|
104
|
+
Thread.current[DOMAIN_WRITE_KEY] = nil
|
|
105
|
+
remove_domain_callback!
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Freezes the server clock Clickwrap records and evaluates expiry
|
|
109
|
+
# against, so a test about a declaration that expired last Tuesday does
|
|
110
|
+
# not have to sleep until next Tuesday.
|
|
111
|
+
#
|
|
112
|
+
# It moves `Clickwrap.now` only. `Time.now` is left alone on purpose:
|
|
113
|
+
# this gem's evidentiary time is the value it writes into
|
|
114
|
+
# `recorded_at_by_server`, and a helper that quietly moved the whole
|
|
115
|
+
# process clock would make it much harder to tell which of the two a test
|
|
116
|
+
# actually depends on.
|
|
117
|
+
def freeze_time_at(moment)
|
|
118
|
+
moment = Time.parse(moment.to_s) if moment.is_a?(String)
|
|
119
|
+
previous = Thread.current[FROZEN_TIME_KEY]
|
|
120
|
+
|
|
121
|
+
install_clock!
|
|
122
|
+
Thread.current[FROZEN_TIME_KEY] = moment.utc
|
|
123
|
+
|
|
124
|
+
block_given? ? yield(moment.utc) : moment.utc
|
|
125
|
+
ensure
|
|
126
|
+
Thread.current[FROZEN_TIME_KEY] = previous
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def frozen_time = Thread.current[FROZEN_TIME_KEY]
|
|
130
|
+
|
|
131
|
+
# Clears every flag and detaches every injected callback. Safe to call
|
|
132
|
+
# when nothing was ever installed, which is the point: a suite calls it
|
|
133
|
+
# in `setup` and in `teardown` without having to know whether the test
|
|
134
|
+
# that just ran used any of this.
|
|
135
|
+
def reset!
|
|
136
|
+
Thread.current[EVENT_WRITE_KEY] = nil
|
|
137
|
+
Thread.current[DOMAIN_WRITE_KEY] = nil
|
|
138
|
+
Thread.current[FROZEN_TIME_KEY] = nil
|
|
139
|
+
|
|
140
|
+
# Unconditional, and `raise: false` throughout: `reset!` is called from
|
|
141
|
+
# `setup` and `teardown` in suites that mostly never touch fault
|
|
142
|
+
# injection, and it must be a quiet no-op there rather than an
|
|
143
|
+
# ArgumentError about a callback nobody installed.
|
|
144
|
+
@event_depth = 0
|
|
145
|
+
@domain_depth = 0
|
|
146
|
+
Event.skip_callback(:create, :before, FAIL_EVENT_WRITE, raise: false)
|
|
147
|
+
::ActiveRecord::Base.skip_callback(:save, :before, FAIL_DOMAIN_WRITE, raise: false)
|
|
148
|
+
|
|
149
|
+
self
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Reads a one-shot flag and clears it in the same breath, so the sabotage
|
|
153
|
+
# applies to exactly one write.
|
|
154
|
+
def consume_flag!(key)
|
|
155
|
+
return false unless Thread.current[key]
|
|
156
|
+
|
|
157
|
+
Thread.current[key] = nil
|
|
158
|
+
true
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
# Installed only while a block is active. There is no permanent
|
|
164
|
+
# production hook on `Clickwrap::Event`: a gem whose whole subject is
|
|
165
|
+
# trustworthy writes has no business leaving a callback in the write path
|
|
166
|
+
# whose only job is to break it.
|
|
167
|
+
#
|
|
168
|
+
# The depth counters are not ceremony. Nested blocks are ordinary in a
|
|
169
|
+
# suite that tests one fault inside another, and an inner `ensure` that
|
|
170
|
+
# detached the callback would silently disarm the outer one.
|
|
171
|
+
def install_event_callback!
|
|
172
|
+
@event_depth = @event_depth.to_i + 1
|
|
173
|
+
return if @event_depth > 1
|
|
174
|
+
|
|
175
|
+
Event.set_callback(:create, :before, FAIL_EVENT_WRITE)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def remove_event_callback!
|
|
179
|
+
return if @event_depth.to_i.zero?
|
|
180
|
+
|
|
181
|
+
@event_depth -= 1
|
|
182
|
+
return if @event_depth.positive?
|
|
183
|
+
|
|
184
|
+
Event.skip_callback(:create, :before, FAIL_EVENT_WRITE, raise: false)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def install_domain_callback!
|
|
188
|
+
@domain_depth = @domain_depth.to_i + 1
|
|
189
|
+
return if @domain_depth > 1
|
|
190
|
+
|
|
191
|
+
::ActiveRecord::Base.set_callback(:save, :before, FAIL_DOMAIN_WRITE)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def remove_domain_callback!
|
|
195
|
+
return if @domain_depth.to_i.zero?
|
|
196
|
+
|
|
197
|
+
@domain_depth -= 1
|
|
198
|
+
return if @domain_depth.positive?
|
|
199
|
+
|
|
200
|
+
::ActiveRecord::Base.skip_callback(:save, :before, FAIL_DOMAIN_WRITE, raise: false)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# `prepend` with an already-prepended module is a no-op, so this is safe
|
|
204
|
+
# to call from every `freeze_time_at`. The module falls straight through
|
|
205
|
+
# to `super` whenever no time is frozen, which is always outside a block.
|
|
206
|
+
def install_clock!
|
|
207
|
+
Clickwrap.singleton_class.prepend(FrozenClock)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Clickwrap
|
|
6
|
+
# Produces a stable digest from the effective trusted-proxy rules themselves.
|
|
7
|
+
# A sentence such as "Cloudflare proxies" is not configuration evidence; the
|
|
8
|
+
# normalized address ranges and regular expressions are.
|
|
9
|
+
module TrustedProxyConfiguration
|
|
10
|
+
SCHEMA = "clickwrap.trusted-proxy-configuration.v1"
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def digest_for(trusted_proxies, source: "host_configuration")
|
|
14
|
+
entries = collection(trusted_proxies).map { |entry| normalize(entry) }
|
|
15
|
+
entries.sort_by! { |entry| CanonicalJson.generate(entry) }
|
|
16
|
+
|
|
17
|
+
payload = {
|
|
18
|
+
"schema" => SCHEMA,
|
|
19
|
+
"source" => source.to_s,
|
|
20
|
+
"entries" => entries
|
|
21
|
+
}
|
|
22
|
+
Digest.digest_canonical(payload)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def digest_for_rails_application(application)
|
|
26
|
+
configured = application.config.action_dispatch.trusted_proxies
|
|
27
|
+
if configured.nil?
|
|
28
|
+
require "action_dispatch/middleware/remote_ip"
|
|
29
|
+
configured = ActionDispatch::RemoteIp::TRUSTED_PROXIES
|
|
30
|
+
source = "rails_default_trusted_proxies"
|
|
31
|
+
else
|
|
32
|
+
source = "rails_application_config_action_dispatch_trusted_proxies"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
digest_for(configured, source: source)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def collection(value)
|
|
41
|
+
return value.to_a if value.is_a?(Array) || value.is_a?(Set)
|
|
42
|
+
return [value] if proxy_entry?(value)
|
|
43
|
+
return value.to_a if value.respond_to?(:to_a) && !value.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
raise ConfigurationError,
|
|
46
|
+
"Trusted proxy configuration must be a proxy rule or a collection of proxy rules; " \
|
|
47
|
+
"got #{value.class}. Pass the same IP ranges or regular expressions Rails uses."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def proxy_entry?(value)
|
|
51
|
+
value.is_a?(IPAddr) || value.is_a?(Regexp) || value.is_a?(Range) ||
|
|
52
|
+
value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Hash)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def normalize(value)
|
|
56
|
+
case value
|
|
57
|
+
when IPAddr
|
|
58
|
+
range = value.to_range
|
|
59
|
+
{ "type" => "ip_address_range", "first" => range.begin.to_s, "last" => range.end.to_s }
|
|
60
|
+
when Regexp
|
|
61
|
+
{ "type" => "regular_expression", "source" => value.source, "options" => value.options }
|
|
62
|
+
when Range
|
|
63
|
+
{
|
|
64
|
+
"type" => "range",
|
|
65
|
+
"first" => value.begin.to_s,
|
|
66
|
+
"last" => value.end.to_s,
|
|
67
|
+
"excludes_last" => value.exclude_end?
|
|
68
|
+
}
|
|
69
|
+
when String, Symbol
|
|
70
|
+
{ "type" => "literal", "value" => value.to_s }
|
|
71
|
+
when Hash
|
|
72
|
+
normalize_hash(value)
|
|
73
|
+
else
|
|
74
|
+
raise ConfigurationError,
|
|
75
|
+
"Trusted proxy rule #{value.inspect} (#{value.class}) cannot be serialized " \
|
|
76
|
+
"deterministically. Use explicit IPAddr ranges, regular expressions, ranges, " \
|
|
77
|
+
"strings, or a canonical Hash describing the effective rule."
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def normalize_hash(value)
|
|
82
|
+
normalized = value.deep_stringify_keys
|
|
83
|
+
CanonicalJson.generate(normalized)
|
|
84
|
+
{ "type" => "configuration", "value" => normalized }
|
|
85
|
+
rescue CanonicalJson::SerializationError => error
|
|
86
|
+
raise ConfigurationError,
|
|
87
|
+
"Trusted proxy configuration contains a value that cannot be canonicalized: " \
|
|
88
|
+
"#{error.message}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|