Module: Karst::Identity

Defined in:
lib/karst/identity.rb,
lib/karst/identity/devise_support.rb,
lib/karst/identity/warden_adapter.rb

Overview

Framework-neutral identity seam for controlled probes. It deliberately does not discover or enumerate principals; callers own that policy.

For a conventional single-model Devise application, Karst can also infer everything below automatically from Devise's own routing metadata and Warden's public runtime API -- see DeviseSupport and WardenAdapter. Explicit configuration (config.principals/config.principal_sources, config.assume_identity/config.clear_identity, config.assume_browser_identity/config.clear_browser_identity) always overrides inference; inference never partially combines with explicit configuration for the same seam. rubocop:disable Metrics/ModuleLength

Defined Under Namespace

Modules: DeviseSupport Classes: ConfigurationError, Error, Unavailable, WardenAdapter

Constant Summary collapse

PrincipalDescriptor =

authentication_* is presentation-only evidence. Machine serializers deliberately ignore it; it exists so local human interfaces can be useful without broadening JSON/MCP disclosure.

Value.define(:model_name, :id, :display_label, :authentication_key,
:authentication_identifier)
SetupState =

Compact, inspectable report of why Karst's zero-config Devise/Warden path is or isn't active. status is one of:

:ready_automatic -- principal source, probe identity, and browser
                   identity are all inferred; no configuration
                   required.
:ready_mixed      -- at least one of principal source / probe
                   identity / browser identity is explicitly
                   configured and the rest are safely inferred
                   (e.g. an explicit config.principals selecting
                   one of several Devise models).
:ready_explicit   -- principal source, probe identity, and browser
                   identity are all explicitly configured.
:ambiguous        -- more than one Devise model was detected and no
                   explicit config.principals/principal_sources
                   selects one.
:unavailable      -- Karst could not identify enough of an
                   authentication integration to run the primary
                   workflow without explicit configuration.

message is nil whenever the caller's own local hint text already says everything Karst can usefully add (the two ready states, and :unavailable with no principal source at all -- every hint call site already has its own "nothing is configured" wording for that). It is populated only when Karst has something more specific to say: which Devise models are ambiguous, or that a principal source exists but probe/browser identity still couldn't be wired up automatically.

Value.define(:status, :message)

Class Method Summary collapse

Class Method Details

.assume_browser(request, principal) ⇒ Object

Returns the Devise/Warden scope this browser identity was actually assumed under (nil for explicit hooks, or for a non-Devise bare Warden proxy) -- see Karst::Web::BrowserIdentity, which retains it for the lifetime of the browser session so #clear_browser below never has to guess which of several selected sources produced the principal being cleared.

Raises:



167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/karst/identity.rb', line 167

def assume_browser(request, principal)
  raise Unavailable, "browser identity hooks are not configured" unless browser_supported?

  if explicit_browser_hooks?
    Karst.config.assume_browser_identity.call(request, principal)
    nil
  else
    warden_adapter = inferred_adapter(principal)
    warden_adapter.assume(request, principal)
    warden_adapter.scope
  end
end

.browser_supported?Boolean

Returns:

  • (Boolean)


157
158
159
# File 'lib/karst/identity.rb', line 157

def browser_supported?
  explicit_browser_hooks? || automatic_browser_identity_available?
end

.clear(session) ⇒ Object



116
117
118
# File 'lib/karst/identity.rb', line 116

def clear(session)
  adapter.clear(session)
end

.clear_browser(request, scope: nil) ⇒ Object

scope, when given, is the exact scope #assume_browser returned for the identity actually being cleared (see Karst::Web::BrowserIdentity) -- used in preference to #scope_for_effective_source, which cannot always determine one on its own with no principal in hand and several selected sources. Karst still refuses to guess when neither is available.

Raises:



186
187
188
189
190
191
192
193
194
# File 'lib/karst/identity.rb', line 186

def clear_browser(request, scope: nil)
  raise Unavailable, "browser identity hooks are not configured" unless browser_supported?

  if explicit_browser_hooks?
    Karst.config.clear_browser_identity.call(request)
  else
    inferred_adapter(nil, scope: scope).clear(request)
  end
end

.describe(principal) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/karst/identity.rb', line 120

def describe(principal)
  model_name = model_name_for(principal)
  id = id_for(principal)
  label_hook = Karst.config.principal_label
  if label_hook && !label_hook.respond_to?(:call)
    raise ConfigurationError, "config.principal_label must be callable"
  end

  label, key, identifier = label_attributes(principal, model_name, id, label_hook)
  PrincipalDescriptor.new(model_name: model_name, id: id, display_label: label,
                          authentication_key: key, authentication_identifier: identifier)
end

.principal_sourcesObject

The effective, normalized principal population(s): a Hash of Symbol => Karst::Access::PrincipalSource, covering an explicit config.principal_sources, a bare config.principals (wrapped as one implicit :default source), and -- when neither is configured -- one inferred Devise model (see Karst::Configuration#principal_sources). Every multi-source-aware caller (Identity.resolve, Access::PrincipalSelection, the panel) reads this instead of config.principals directly.

Raises:



98
99
100
101
102
103
# File 'lib/karst/identity.rb', line 98

def principal_sources
  sources = Karst.config.principal_sources
  raise Unavailable, "no principal source is configured" unless sources

  sources
end

.principalsObject

Raises:



80
81
82
83
84
85
86
87
88
# File 'lib/karst/identity.rb', line 80

def principals
  source = Karst.config.principals
  return called_principal_source(source) if source

  inferred = DeviseSupport.unambiguous_mapping
  return inferred.model.all if inferred

  raise Unavailable, "no principal source is configured"
end

.resolve(model_name:, id:) ⇒ Object

Resolves only principals exposed by a configured source. In particular, this never constantizes a submitted model name or performs an unrestricted model lookup.

Tries each configured Karst::Access::PrincipalSource in order and returns the first match; a model name that does not belong to any configured source resolves nothing, without ever touching that source's records. For an Active Record relation/class source, a matching model name resolves through a scoped primary-key query against that exact relation instead of enumerating it -- a source may cover hundreds of thousands of rows, and this must stay a single bounded query regardless of table size. The relation's own WHERE clauses (tenant scoping, soft deletes, and so on) still apply, so a principal outside a configured relation is never resolved. A generic Enumerable source (no scoped-query capability) keeps the original enumerate-and-compare behavior, bounded to that one source.



149
150
151
152
153
154
155
# File 'lib/karst/identity.rb', line 149

def resolve(model_name:, id:)
  principal_sources.each_value do |source|
    resolved = resolve_within_source(source, model_name: model_name, id: id)
    return resolved if resolved
  end
  nil
end

.setup_stateObject

See SetupState above. Cheap and side-effect free: touches only already-established configuration/metadata plus, at most, calling a configured principals/principal_sources callable the same way the panel already does on every render to type-check its result (see Access::PrincipalSampler.representative_capable?) -- never to enumerate or query it.



202
203
204
205
206
207
208
# File 'lib/karst/identity.rb', line 202

def setup_state
  return SetupState.new(status: :ambiguous, message: ambiguous_message) if ambiguous_principal_source?
  return SetupState.new(status: :unavailable, message: nil) unless principal_source_ready?
  return SetupState.new(status: :unavailable, message: unavailable_message) unless identity_channels_ready?

  SetupState.new(status: ready_status, message: nil)
end

.with(session, principal) ⇒ Object



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

def with(session, principal)
  active_adapter = adapter(principal)
  # The hook may establish identity and then raise, so cleanup becomes
  # mandatory before invoking it rather than only after it returns.
  assumed = true
  active_adapter.assume(session, principal)
  yield
ensure
  active_adapter.clear(session) if active_adapter && assumed
end