Class: Clickwrap::PresentationManifest
- Inherits:
-
Object
- Object
- Clickwrap::PresentationManifest
- Defined in:
- lib/clickwrap/presentation_manifest.rb
Overview
The canonical description of one offer: which policy revision, which document versions and digests, the exact sentences and link labels, the choice structure, the call to action, who it was issued to, and when it stops being valid.
This exists because "the database says version 3" is a weak claim. It says what the row contains now, not what the server offered then. A manifest is generated by the server at render, signed, and verified again at submit against the current immutable rows — so a deploy between GET and POST can never cause the server to record a version the person was not offered.
What it proves is bounded, and the receipt says so: the server generated and accepted this particular offer. It does not prove the person read the document, understood it, saw particular pixels, or received an interface that any given jurisdiction would consider adequate.
Constant Summary collapse
- SIGNING_PURPOSE =
"clickwrap/presentation"
Instance Attribute Summary collapse
-
#attributes ⇒ Object
readonly
Returns the value of attribute attributes.
-
#digest ⇒ Object
readonly
Computed in the constructor, before the object freezes.
Class Method Summary collapse
- .build(policy:, revision_digest:, statements:, submit_button_text:, locale:, actor_reference: nil, actor_type: nil, tenant_key: nil, subject_key: nil, subject_fingerprint: nil, registration_flow_id: nil, prospective_actor_type: nil, represented_party_reference: nil, represented_party_type: nil, authority_rule: nil, represented_party_creation_flow_id: nil, represented_party_will_be_created_by_protected_action: false, authority_at_presentation: nil, combined_control: nil, capture_channel: "web_browser", issued_at: nil, expires_at: nil, nonce: nil) ⇒ Object
-
.from_token(token) ⇒ Object
Reads a token back into a manifest.
- .reset_verifier! ⇒ Object
- .verifier ⇒ Object
Instance Method Summary collapse
- #[](key) ⇒ Object
- #actor_reference ⇒ Object
- #authority_at_presentation ⇒ Object
- #authority_rule ⇒ Object
- #capture_channel ⇒ Object
-
#combined_answered_as ⇒ Object
The statement key the single control is submitted under.
-
#combined_control ⇒ Object
The one control this offer rendered, and the statements it answered.
- #combined_sentence ⇒ Object
- #combined_statement_keys ⇒ Object
- #expired?(at = Clickwrap.now) ⇒ Boolean
- #expires_at ⇒ Object
-
#initialize(attributes) ⇒ PresentationManifest
constructor
A new instance of PresentationManifest.
- #issued_at ⇒ Object
- #locale ⇒ Object
- #nonce ⇒ Object
- #policy_key ⇒ Object
- #prospective_actor_type ⇒ Object
- #registration_flow_id ⇒ Object
- #represented_party_creation_flow_id ⇒ Object
- #represented_party_reference ⇒ Object
- #represented_party_type ⇒ Object
- #represented_party_will_be_created_by_protected_action? ⇒ Boolean
- #revision_digest ⇒ Object
- #statement(key) ⇒ Object
- #statements ⇒ Object
- #subject_fingerprint ⇒ Object
- #subject_key ⇒ Object
- #submit_button_text ⇒ Object
- #tenant_key ⇒ Object
- #to_h ⇒ Object
- #to_s ⇒ Object
- #to_token ⇒ Object
Constructor Details
#initialize(attributes) ⇒ PresentationManifest
Returns a new instance of PresentationManifest.
24 25 26 27 28 29 30 31 32 33 |
# File 'lib/clickwrap/presentation_manifest.rb', line 24 def initialize(attributes) @attributes = deep_stringify(attributes).freeze # Computed here rather than memoized on first use: the object is frozen # below, and a manifest that could still change after construction is not # a manifest anything should be bound to. @digest = Digest.digest_canonical( @attributes, algorithm: Clickwrap.config.digest_canonical_receipts_with.to_s ).freeze freeze end |
Instance Attribute Details
#attributes ⇒ Object (readonly)
Returns the value of attribute attributes.
22 23 24 |
# File 'lib/clickwrap/presentation_manifest.rb', line 22 def attributes @attributes end |
#digest ⇒ Object (readonly)
Computed in the constructor, before the object freezes. Lazy memoization would raise on a frozen manifest, and recomputing on every call would canonicalize the whole body each time it is read — which happens on every render and every capture.
181 182 183 |
# File 'lib/clickwrap/presentation_manifest.rb', line 181 def digest @digest end |
Class Method Details
.build(policy:, revision_digest:, statements:, submit_button_text:, locale:, actor_reference: nil, actor_type: nil, tenant_key: nil, subject_key: nil, subject_fingerprint: nil, registration_flow_id: nil, prospective_actor_type: nil, represented_party_reference: nil, represented_party_type: nil, authority_rule: nil, represented_party_creation_flow_id: nil, represented_party_will_be_created_by_protected_action: false, authority_at_presentation: nil, combined_control: nil, capture_channel: "web_browser", issued_at: nil, expires_at: nil, nonce: nil) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/clickwrap/presentation_manifest.rb', line 36 def build(policy:, revision_digest:, statements:, submit_button_text:, locale:, actor_reference: nil, actor_type: nil, tenant_key: nil, subject_key: nil, subject_fingerprint: nil, registration_flow_id: nil, prospective_actor_type: nil, represented_party_reference: nil, represented_party_type: nil, authority_rule: nil, represented_party_creation_flow_id: nil, represented_party_will_be_created_by_protected_action: false, authority_at_presentation: nil, combined_control: nil, capture_channel: "web_browser", issued_at: nil, expires_at: nil, nonce: nil) issued_at ||= Clickwrap.now expires_at ||= issued_at + Clickwrap.config.presentation_valid_for attributes = { "schema" => Clickwrap::CANONICAL_SCHEMA_VERSION, "policy" => { "key" => policy.key, "revision" => revision_digest }, "statements" => statements, "submit_button_text" => , "locale" => locale.to_s, "actor" => { "reference" => actor_reference, "type" => actor_type }.compact.presence, "registration_flow_id" => registration_flow_id, "prospective_actor_type" => prospective_actor_type, "represented_party" => { "reference" => represented_party_reference, "type" => represented_party_type, "authority_rule" => , "creation_flow_id" => represented_party_creation_flow_id, "will_be_created_by_protected_action" => represented_party_will_be_created_by_protected_action == true, "authority_at_presentation" => }.compact.presence, "tenant_key" => tenant_key, "subject" => { "reference" => subject_key, "fingerprint" => subject_fingerprint }.compact.presence, "capture_channel" => capture_channel.to_s, "issued_at" => Receipt.format_time(issued_at), "expires_at" => Receipt.format_time(expires_at), "nonce" => nonce || SecureRandom.uuid, "gem_version" => Clickwrap::VERSION, "application_version" => Clickwrap.config.resolved_application_version, "template_version" => Clickwrap.config.resolved_template_version } # Only present when the offer really was one control, so a manifest # from an itemized presentation is byte-identical to the ones this gem # has always written. attributes["combined_control"] = combined_control if combined_control new(attributes) end |
.from_token(token) ⇒ Object
Reads a token back into a manifest. Every failure here is the same kind of failure — the browser handed us something we did not sign, or signed too long ago — so they all raise PresentationInvalid rather than leaking which specific check tripped.
89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/clickwrap/presentation_manifest.rb', line 89 def from_token(token) raise PresentationInvalid, "No presentation token was submitted" if token.blank? payload = verifier.verified(token, purpose: SIGNING_PURPOSE) raise PresentationInvalid, "The presentation token could not be verified" if payload.nil? new(payload) rescue ActiveSupport::MessageVerifier::InvalidSignature raise PresentationInvalid, "The presentation token could not be verified" end |
.reset_verifier! ⇒ Object
105 106 107 |
# File 'lib/clickwrap/presentation_manifest.rb', line 105 def reset_verifier! @verifier = nil end |
.verifier ⇒ Object
101 102 103 |
# File 'lib/clickwrap/presentation_manifest.rb', line 101 def verifier @verifier ||= build_verifier end |
Instance Method Details
#[](key) ⇒ Object
128 |
# File 'lib/clickwrap/presentation_manifest.rb', line 128 def [](key) = attributes[key.to_s] |
#actor_reference ⇒ Object
136 |
# File 'lib/clickwrap/presentation_manifest.rb', line 136 def actor_reference = attributes.dig("actor", "reference") |
#authority_at_presentation ⇒ Object
148 |
# File 'lib/clickwrap/presentation_manifest.rb', line 148 def = attributes.dig("represented_party", "authority_at_presentation") |
#authority_rule ⇒ Object
141 |
# File 'lib/clickwrap/presentation_manifest.rb', line 141 def = attributes.dig("represented_party", "authority_rule") |
#capture_channel ⇒ Object
152 |
# File 'lib/clickwrap/presentation_manifest.rb', line 152 def capture_channel = attributes["capture_channel"] |
#combined_answered_as ⇒ Object
The statement key the single control is submitted under. Every covered key takes its answer from this one, server-side.
173 |
# File 'lib/clickwrap/presentation_manifest.rb', line 173 def combined_answered_as = combined_control&.fetch("answered_as", nil) |
#combined_control ⇒ Object
The one control this offer rendered, and the statements it answered.
This is where the substitution defense lives for a composed presentation. The per-statement assertions below say what each act was; this says what the person actually READ — one sentence, composed from those acts, with the document links inside it — and which keys the single answer covers. Nil whenever the offer was itemized.
167 |
# File 'lib/clickwrap/presentation_manifest.rb', line 167 def combined_control = attributes["combined_control"] |
#combined_sentence ⇒ Object
168 |
# File 'lib/clickwrap/presentation_manifest.rb', line 168 def combined_sentence = combined_control&.fetch("sentence", nil) |
#combined_statement_keys ⇒ Object
169 |
# File 'lib/clickwrap/presentation_manifest.rb', line 169 def combined_statement_keys = Array(combined_control&.fetch("covers", nil)) |
#expired?(at = Clickwrap.now) ⇒ Boolean
175 |
# File 'lib/clickwrap/presentation_manifest.rb', line 175 def expired?(at = Clickwrap.now) = expires_at.nil? || expires_at <= at |
#expires_at ⇒ Object
154 |
# File 'lib/clickwrap/presentation_manifest.rb', line 154 def expires_at = parse_time(attributes["expires_at"]) |
#issued_at ⇒ Object
153 |
# File 'lib/clickwrap/presentation_manifest.rb', line 153 def issued_at = parse_time(attributes["issued_at"]) |
#locale ⇒ Object
134 |
# File 'lib/clickwrap/presentation_manifest.rb', line 134 def locale = attributes["locale"] |
#nonce ⇒ Object
135 |
# File 'lib/clickwrap/presentation_manifest.rb', line 135 def nonce = attributes["nonce"] |
#policy_key ⇒ Object
130 |
# File 'lib/clickwrap/presentation_manifest.rb', line 130 def policy_key = attributes.dig("policy", "key") |
#prospective_actor_type ⇒ Object
138 |
# File 'lib/clickwrap/presentation_manifest.rb', line 138 def prospective_actor_type = attributes["prospective_actor_type"] |
#registration_flow_id ⇒ Object
137 |
# File 'lib/clickwrap/presentation_manifest.rb', line 137 def registration_flow_id = attributes["registration_flow_id"] |
#represented_party_creation_flow_id ⇒ Object
142 |
# File 'lib/clickwrap/presentation_manifest.rb', line 142 def represented_party_creation_flow_id = attributes.dig("represented_party", "creation_flow_id") |
#represented_party_reference ⇒ Object
139 |
# File 'lib/clickwrap/presentation_manifest.rb', line 139 def represented_party_reference = attributes.dig("represented_party", "reference") |
#represented_party_type ⇒ Object
140 |
# File 'lib/clickwrap/presentation_manifest.rb', line 140 def represented_party_type = attributes.dig("represented_party", "type") |
#represented_party_will_be_created_by_protected_action? ⇒ Boolean
144 145 146 |
# File 'lib/clickwrap/presentation_manifest.rb', line 144 def represented_party_will_be_created_by_protected_action? attributes.dig("represented_party", "will_be_created_by_protected_action") == true end |
#revision_digest ⇒ Object
131 |
# File 'lib/clickwrap/presentation_manifest.rb', line 131 def revision_digest = attributes.dig("policy", "revision") |
#statement(key) ⇒ Object
156 157 158 |
# File 'lib/clickwrap/presentation_manifest.rb', line 156 def statement(key) statements.find { |statement| statement["key"] == key.to_s } end |
#statements ⇒ Object
132 |
# File 'lib/clickwrap/presentation_manifest.rb', line 132 def statements = attributes["statements"] || [] |
#subject_fingerprint ⇒ Object
151 |
# File 'lib/clickwrap/presentation_manifest.rb', line 151 def subject_fingerprint = attributes.dig("subject", "fingerprint") |
#subject_key ⇒ Object
150 |
# File 'lib/clickwrap/presentation_manifest.rb', line 150 def subject_key = attributes.dig("subject", "reference") |
#submit_button_text ⇒ Object
133 |
# File 'lib/clickwrap/presentation_manifest.rb', line 133 def = attributes["submit_button_text"] |
#tenant_key ⇒ Object
149 |
# File 'lib/clickwrap/presentation_manifest.rb', line 149 def tenant_key = attributes["tenant_key"] |
#to_h ⇒ Object
187 |
# File 'lib/clickwrap/presentation_manifest.rb', line 187 def to_h = attributes |
#to_s ⇒ Object
189 |
# File 'lib/clickwrap/presentation_manifest.rb', line 189 def to_s = "presentation of #{policy_key} (#{nonce})" |
#to_token ⇒ Object
183 184 185 |
# File 'lib/clickwrap/presentation_manifest.rb', line 183 def to_token self.class.verifier.generate(attributes, purpose: SIGNING_PURPOSE, expires_at: expires_at) end |