Class: Clickwrap::StatementState

Inherits:
ApplicationRecord show all
Defined in:
lib/clickwrap/models/statement_state.rb

Overview

The current-state projection: the answer to "does this person currently have X?" without walking the whole event history on every request.

This table is a cache of a computation over retained event payloads. Nothing here is evidence — the evidence is in clickwrap_events. Before retention disposes of a root payload it can be rebuilt from those events; afterward, deleting this projection would try to recreate personal identity facts that the reviewed disposition intentionally removed. CurrentState.rebuild_for! therefore refuses that destructive operation when it can see such a root. That separation lets this row be mutable, indexed, and fast without those properties leaking into the evidence record.

The unique index guarantees one projection row per identity. Portable StatementIdentityLock rows serialize writers; the unique index alone would not stop two immutable capture events or decide which one is current.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.identity_digest_for(attributes) ⇒ Object

The digest the unique index is taken over. Canonicalized first, so the value depends on the five parts and not on the order a caller happened to build the hash in.



68
69
70
# File 'lib/clickwrap/models/statement_state.rb', line 68

def self.identity_digest_for(attributes)
  Digest.digest_canonical(attributes.transform_keys(&:to_s))
end

.identity_for(policy_key:, statement_key:, actor_reference:, tenant_key: nil, subject_key: nil, represented_party_reference: nil) ⇒ Object

The identity a unique index can enforce. NULLs do not collide in a unique index on most adapters, so "no tenant" and "no subject" are the empty string rather than NULL — otherwise a policy with no subject would happily accumulate duplicate live grants.



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/clickwrap/models/statement_state.rb', line 51

def self.identity_for(policy_key:, statement_key:, actor_reference:, tenant_key: nil,
                      subject_key: nil, represented_party_reference: nil)
  attributes = {
    policy_key: policy_key.to_s,
    statement_key: statement_key.to_s,
    actor_reference: actor_reference.to_s,
    tenant_key: tenant_key.to_s,
    subject_key: subject_key.to_s,
    represented_party_reference: represented_party_reference.to_s
  }

  attributes.merge(identity_digest: identity_digest_for(attributes))
end

.subject_key_for(subject) ⇒ Object



72
# File 'lib/clickwrap/models/statement_state.rb', line 72

def self.subject_key_for(subject) = Reference.subject(subject)

.tenant_key_for(tenant) ⇒ Object



73
# File 'lib/clickwrap/models/statement_state.rb', line 73

def self.tenant_key_for(tenant) = Reference.tenant(tenant)

Instance Method Details

#current_eventObject



75
# File 'lib/clickwrap/models/statement_state.rb', line 75

def current_event = Event.find_by(id: current_event_id)

#expired?(at = Clickwrap.now) ⇒ Boolean

Returns:

  • (Boolean)


77
# File 'lib/clickwrap/models/statement_state.rb', line 77

def expired?(at = Clickwrap.now) = expires_at.present? && expires_at <= at

#failure_reason(at = Clickwrap.now) ⇒ Object

Why it does not, as one of the stable error symbols applications branch on. Never an English string: an authorization decision should not depend on parsing a message.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/clickwrap/models/statement_state.rb', line 92

def failure_reason(at = Clickwrap.now)
  return nil if satisfies?(at)

  case state
  when "withdrawn" then :consent_withdrawn
  when "declined" then :declined
  when "superseded", "corrected" then :superseded
  when "revoked" then :revoked
  when "consumed" then :authorization_consumed
  when "exempted" then :exemption_not_accepted
  when "expired" then expiry_error
  else expired?(at) ? expiry_error : :no_evidence
  end
end

#satisfies?(at = Clickwrap.now) ⇒ Boolean

Whether this projection currently satisfies a requirement. Expiry is evaluated live rather than trusted from the state column, because a declaration expires on a clock, not on a background job having run.

Returns:

  • (Boolean)


82
83
84
85
86
87
# File 'lib/clickwrap/models/statement_state.rb', line 82

def satisfies?(at = Clickwrap.now)
  return false unless state == "active"
  return false if expired?(at)

  true
end

#to_sObject



107
# File 'lib/clickwrap/models/statement_state.rb', line 107

def to_s = "#{kind} #{statement_key} for #{actor_reference} (#{state})"