Class: Karst::Access::ResourceEvidence

Inherits:
Object
  • Object
show all
Defined in:
lib/karst/access/resource_evidence.rb

Overview

Given one exact resource (the specific record a route addresses by id) and one specific principal (typically the successful outcome from an Access::Sweep), reports simple, directly observed foreign-key relationships between those two records. This is evidence, not an authorization claim: it never states or implies why an outcome occurred, only which foreign-key columns, if any, point from one given record to the other's id.

Deliberately narrow, matching the v1 scope this class was built for:

  • only foreign-key-shaped columns (ending in "_id") on the two given records are ever inspected -- never an arbitrary attribute, so no other column value (name, email, token, ...) is ever read or shown;
  • only a direct column-value comparison between the two given records, never a join, a has_many traversal, or any multi-hop graph walk;
  • resource resolution from a route path is attempted only through Rails' own route recognition plus its controller-to-model naming convention, and only trusted when every step succeeds unambiguously (a recognized route with an :id segment, a controller name that classifies to a real loaded Active Record class, and a record that actually exists for that id). Anything softer -- an unrecognized route, a controller with no conventional model, a missing record -- is reported as a limitation rather than guessed at. rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: Error

Constant Summary collapse

ResourceDescriptor =

The resource side never gets Identity::PrincipalDescriptor's configurable display_label hook -- there is no equivalent concept for "the current route's resource" -- so it gets its own minimal, equally attribute-free descriptor.

Value.define(:model_name, :id)
Relationship =

from_model/from_id is whichever of the resource/principal actually holds the foreign-key column; to_model/to_id is the other side.

Value.define(:column, :from_model, :from_id, :to_model, :to_id)
Result =
Value.define(:principal, :resource, :relationships, :observed_status, :observed_redirect,
                      :limitation) do
  # Plain-text rendering deliberately kept free of causal wording
  # ("owns", "is authorized", "grants") -- see class comment above.
  def to_text
    lines = [principal.display_label]
    lines << observed_line if observed_status || observed_redirect
    lines << "" << "Related state:"
    lines.concat(related_state_lines)
    lines.join("\n")
  end

  private

  def observed_line
    observed_redirect ? "Observed #{observed_status}#{observed_redirect}" : "Observed #{observed_status}"
  end

  def related_state_lines
    return ["  Unavailable: #{limitation}"] if limitation
    return [no_relationship_line] if relationships.empty?

    grouped_relationship_lines
  end

  def no_relationship_line
    "  No observed foreign-key relationship to #{resource.model_name} ##{resource.id}."
  end

  def grouped_relationship_lines
    relationships.group_by { |rel| [rel.from_model, rel.from_id] }.flat_map do |(model, id), grouped|
      ["#{model} ##{id}"] + grouped.map { |rel| "  #{rel.column}#{rel.to_model} ##{rel.to_id}" }
    end
  end
end

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(resource:, principal:) ⇒ ResourceEvidence

Returns a new instance of ResourceEvidence.



166
167
168
169
# File 'lib/karst/access/resource_evidence.rb', line 166

def initialize(resource:, principal:)
  @resource = resource
  @principal = principal
end

Class Method Details

.for_outcome(outcome:, path:, http_method: "GET", application: nil) ⇒ Object

Resolves the resource, resolves the principal (from a Sweep::Outcome's PrincipalDescriptor), and reports relationships in one call. Either resolution step may fail safely -- see #resolve_resource and #resolve_principal -- in which case the Result carries a limitation instead of relationships.



87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/karst/access/resource_evidence.rb', line 87

def for_outcome(outcome:, path:, http_method: "GET", application: nil)
  resource, resource_limitation = resolve_resource(path: path, http_method: http_method,
                                                   application: application)
  principal, principal_limitation = resolve_principal(outcome.principal)
  limitation = [resource_limitation, principal_limitation].compact.join("; ")

  return unresolved_result(outcome, limitation) if resource.nil? || principal.nil?

  new(resource: resource, principal: principal).call(
    observed_status: outcome.status, observed_redirect: outcome.redirect
  )
end

.resolve_principal(descriptor) ⇒ Object

Resolves the actual record behind a Karst::Identity::PrincipalDescriptor only through the configured principal source. A valid model/id outside that source is deliberately unresolved.



121
122
123
124
125
126
127
128
# File 'lib/karst/access/resource_evidence.rb', line 121

def resolve_principal(descriptor)
  record = Identity.resolve(model_name: descriptor.model_name, id: descriptor.id)
  return [nil, "principal is not available from the configured principal source"] unless record

  [record, nil]
rescue Identity::Error
  [nil, "the configured principal source is unavailable"]
end

.resolve_resource(path:, http_method: "GET", application: nil) ⇒ Object

Attempts to resolve the exact record a route addresses, trusting only Rails' own route recognition and controller naming convention, and only when every step is unambiguous. Returns [record, nil] on success or [nil, reason] when any step is not reliable -- never a guessed record.



105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/karst/access/resource_evidence.rb', line 105

def resolve_resource(path:, http_method: "GET", application: nil)
  app = application || rails_application
  return [nil, "no Rails application is available to recognize the route"] unless app

  params = recognize(app, path, http_method)
  return [nil, "the route could not be recognized"] unless params

  id = params[:id]
  return [nil, "the recognized route has no :id segment addressing one specific resource"] unless id

  find_by_controller(params[:controller], id)
end

Instance Method Details

#call(observed_status: nil, observed_redirect: nil) ⇒ Object



171
172
173
174
175
176
177
178
179
180
# File 'lib/karst/access/resource_evidence.rb', line 171

def call(observed_status: nil, observed_redirect: nil)
  Result.new(
    principal: Identity.describe(@principal),
    resource: ResourceDescriptor.new(model_name: model_name(@resource), id: primary_key_value(@resource)),
    relationships: relationships.freeze,
    observed_status: observed_status,
    observed_redirect: observed_redirect,
    limitation: nil
  )
end