Class: Clickwrap::RequestEvidenceExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/clickwrap/request_evidence_extractor.rb

Overview

Resolves the optional request evidence for one capture: the IP address the server observed, the browser user-agent the client supplied, and whatever a resolver was authorized to estimate from that address.

Four properties of this class are load-bearing.

It runs SYNCHRONOUSLY, before the evidence and domain transaction opens. Not in a job, not after commit, not in a rescue that fills the gap in later. Evidence that arrives after the action it was supposed to accompany is a different claim from evidence that accompanied it, and a receipt cannot tell the two apart once they are in the same columns. Resolving before the transaction opens also keeps a provider lookup from holding locks on evidence rows while it waits on somebody else's network.

It WRITES NOTHING. It returns a value object holding the attribute hash for one clickwrap_request_evidence row, and the caller writes that row inside the transaction that carries the protected action. Required evidence and the action commit together or not at all.

It reads ONLY from the HTTP request, through the host's configured readers. No parameter, no hidden form field, and no client-supplied header can select a policy, a resolver, a field, a precision, or a retention rule. A browser may answer a policy; it may never author one.

It RECORDS WHAT IT COULD NOT GET. Missing, refused, failed, and answered are four different states, and every one of them ends up in a column with a reason attached. Nothing here silently substitutes a blank, a zero, or the word "Unknown" for an answer nobody gave.

Nothing is collected unless the COMPILED policy says so by name. Application defaults are merged into that policy at boot, not here at capture time, so a configuration change necessarily produces a different policy revision and a policy can explicitly narrow an application default.

Defined Under Namespace

Classes: Resolved

Constant Summary collapse

NO_HTTP_REQUEST =

Reason strings are permanent evidence vocabulary. They are added to, never renamed or repurposed: a receipt written today is read by code that may be years newer, and a reason that changed meaning underneath it would make old evidence say something it never said.

"no_http_request"
CHANNEL_CARRIES_NO_HTTP_REQUEST =
"capture_channel_carries_no_http_request"
NO_IP_ADDRESS_ON_HTTP_REQUEST =
"no_ip_address_on_http_request"
FORWARDED_CHAIN_REFUSED =
"ip_address_reader_returned_a_forwarded_chain"
NO_BROWSER_USER_AGENT_ON_HTTP_REQUEST =
"no_browser_user_agent_on_http_request"
RESOLVER_RETURNED_NO_RESULT =
"resolver_returned_no_result"
RESOLVER_CANNOT_SUPPLY_AUTHORIZED_FIELDS =
"resolver_cannot_supply_authorized_fields"
PROVIDER_SUPPLIED_NO_AUTHORIZED_FIELD =
"provider_supplied_no_authorized_field"
RAILS_REQUEST_REMOTE_IP_READER_NAME =

What produced the stored address. Rails' request.remote_ip is the conventional reader and the one Clickwrap ships with; anything else is the host's own, and the receipt says so rather than implying Rails' spoof checks and trusted-proxy handling were involved when they were not.

"rails_request_remote_ip"
HOST_CONFIGURED_READER_NAME =
"host_configured_reader"
CHANNELS_WITHOUT_AN_HTTP_REQUEST =

Channels that structurally carry no HTTP request. A background job has no browser and never had one; that is a fact about the capture, not a failure to collect something, and the reason string says which it was.

%w[background_job imported_provider system].freeze
MAXIMUM_UNAVAILABLE_REASON_LENGTH =

Reasons are stored in a string column. A pathological error class name must not turn a recorded unavailability into a failed INSERT that rolls back the protected action.

200
TEXT_COLUMNS_BY_AUTHORIZED_FIELD =

Exactly which columns each authorized field unlocks. This table IS the minimization guarantee, which is why it is a table rather than a run of conditionals: a reviewer can see at a glance that authorizing country unlocks a country code and name and nothing else, and adding a column here is a visible decision to store more. Coordinates are absent on purpose — they are a coupled pair and are handled separately below.

{
  "country" => { ip_geolocation_country_code: :country_code,
                 ip_geolocation_country_name: :country_name },
  "region" => { ip_geolocation_region_name: :region_name,
                ip_geolocation_region_code: :region_code },
  "city" => { ip_geolocation_city_name: :city_name },
  "postal_code" => { ip_geolocation_postal_code: :postal_code },
  "timezone" => { ip_geolocation_timezone: :timezone },
  "continent" => { ip_geolocation_continent_code: :continent_code },
  "metro_code" => { ip_geolocation_metro_code: :metro_code }
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(policy:, http_request: nil, capture_channel: nil) ⇒ RequestEvidenceExtractor

policy: accepts a compiled Clickwrap::Policy or the RequestEvidencePolicy it carries, so a test can hand this class an allowlist directly. http_request: is nil for captures that genuinely have no request, and that absence is recorded rather than papered over.



131
132
133
134
135
# File 'lib/clickwrap/request_evidence_extractor.rb', line 131

def initialize(policy:, http_request: nil, capture_channel: nil)
  @policy = policy.respond_to?(:request_evidence) ? policy.request_evidence : policy
  @http_request = http_request
  @capture_channel = capture_channel&.to_s
end

Class Method Details

.default_ip_address_reader_source_locationObject

The source location of a freshly built Configuration's default IP-address reader. See #ip_address_reader_name for why this is a comparison against a fresh object rather than against a constant.



115
116
117
118
119
120
121
122
123
124
# File 'lib/clickwrap/request_evidence_extractor.rb', line 115

def default_ip_address_reader_source_location
  return @default_ip_address_reader_source_location if defined?(@default_ip_address_reader_source_location)

  @default_ip_address_reader_source_location =
    begin
      Configuration.new.read_ip_address_from_http_request_with.source_location
    rescue StandardError
      nil
    end
end

Instance Method Details

#extractObject



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/clickwrap/request_evidence_extractor.rb', line 137

def extract
  return Resolved.none(authorized_fields: authorized_fields) unless records_anything?

  attributes = { authorized_fields: authorized_fields }
               .merge(ip_address_attributes)
               .merge(browser_user_agent_attributes)
               .merge(ip_geolocation_attributes)

  Resolved.new(attributes: attributes.freeze, authorized_fields: authorized_fields,
               records_anything: true)
end