karst 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +6 -1
- data/CHANGELOG.md +82 -2
- data/CONTRIBUTING.md +9 -1
- data/README.md +118 -2
- data/docs/advanced-configuration.md +106 -0
- data/docs/rails8-authentication.md +24 -4
- data/docs/request-reproduction.md +242 -0
- data/lib/generators/karst/install/templates/karst_initializer.rb +6 -0
- data/lib/karst/access/approved_populations.rb +15 -6
- data/lib/karst/access/errors.rb +16 -0
- data/lib/karst/access/identity_probe.rb +151 -0
- data/lib/karst/access/local_path.rb +42 -0
- data/lib/karst/access/observed_endpoint.rb +46 -0
- data/lib/karst/access/outcome_groups.rb +93 -0
- data/lib/karst/access/population_discovery.rb +25 -1
- data/lib/karst/access/principal_source.rb +24 -3
- data/lib/karst/access/probe.rb +397 -0
- data/lib/karst/access/search.rb +14 -6
- data/lib/karst/access/sweep.rb +42 -112
- data/lib/karst/cli/identity_serialization.rb +82 -0
- data/lib/karst/cli/outcome_summary.rb +131 -0
- data/lib/karst/cli/principal_reference.rb +51 -0
- data/lib/karst/cli/reproduction.rb +267 -0
- data/lib/karst/cli/verification.rb +219 -55
- data/lib/karst/configuration.rb +52 -6
- data/lib/karst/identity/evidence.rb +110 -0
- data/lib/karst/identity/observation.rb +167 -0
- data/lib/karst/identity/warden_adapter.rb +26 -1
- data/lib/karst/identity.rb +157 -22
- data/lib/karst/mcp/compatibility.rb +24 -0
- data/lib/karst/mcp/reproduce_request_tool.rb +137 -0
- data/lib/karst/mcp/reproduction.rb +58 -0
- data/lib/karst/mcp/server.rb +27 -8
- data/lib/karst/mcp/verify_access_tool.rb +36 -2
- data/lib/karst/reproduction/curl.rb +73 -0
- data/lib/karst/reproduction/exercise.rb +246 -0
- data/lib/karst/reproduction/observation.rb +87 -0
- data/lib/karst/reproduction/sanitizer.rb +210 -0
- data/lib/karst/version.rb +1 -1
- data/lib/karst/web/middleware.rb +105 -16
- data/lib/karst/web/panel.rb +302 -47
- data/lib/karst.rb +2 -0
- data/lib/rails/commands/karst/mcp/mcp_command.rb +30 -3
- data/lib/rails/commands/karst/reproduce/reproduce_command.rb +67 -0
- data/lib/rails/commands/karst/verify/verify_command.rb +14 -2
- metadata +33 -6
data/lib/karst/configuration.rb
CHANGED
|
@@ -23,17 +23,45 @@ module Karst
|
|
|
23
23
|
# Karst), already true by default wherever it should be. What remains
|
|
24
24
|
# below is for exceptional applications: custom (non-Devise)
|
|
25
25
|
# authentication, identity spread across several models, and a handful of
|
|
26
|
-
# deliberately unprominent bounds.
|
|
26
|
+
# deliberately unprominent bounds. Mutating MCP reproduction is a separate,
|
|
27
|
+
# default-off capability because same-connection rollback cannot contain
|
|
28
|
+
# arbitrary application side effects. See docs/advanced-configuration.md.
|
|
27
29
|
class Configuration
|
|
28
30
|
# None of these are normal configuration. `enabled` is an off switch for
|
|
29
31
|
# the exceptional case where Karst's default (on in development/test) is
|
|
30
32
|
# wrong for this environment. The rest are escape hatches for custom
|
|
31
33
|
# (non-Devise) authentication -- principals, assume_identity,
|
|
32
34
|
# clear_identity, principal_label, and the browser Test-as pair -- and
|
|
33
|
-
# are documented as such.
|
|
34
|
-
|
|
35
|
+
# are documented as such. `mcp_mutating_requests` is the narrowly scoped
|
|
36
|
+
# opt-in that lets an MCP client issue POST/PUT/PATCH/DELETE reproduction;
|
|
37
|
+
# it is false by default and does not affect human reproduction.
|
|
38
|
+
attr_accessor :enabled, :mcp_mutating_requests, :principals, :assume_identity, :clear_identity, :principal_label,
|
|
35
39
|
:assume_browser_identity, :clear_browser_identity
|
|
36
40
|
|
|
41
|
+
# How Karst observes which principal the *application itself* resolved
|
|
42
|
+
# while running a probe request -- the only thing that can turn "Karst
|
|
43
|
+
# was asked to run as User#123" into evidence that the request actually
|
|
44
|
+
# did. A callable taking one Karst::Identity::ObservationContext
|
|
45
|
+
# (#controller, #request, #env) and returning the application's runtime
|
|
46
|
+
# principal, or nil when the application resolved none:
|
|
47
|
+
#
|
|
48
|
+
# config.observe_identity = ->(context) { context.controller&.current_user }
|
|
49
|
+
#
|
|
50
|
+
# `controller` is the exact ActionController instance that processed the
|
|
51
|
+
# request, present even when a before_action halted it, so an identity
|
|
52
|
+
# the application keeps in an instance variable, a Current attribute, or
|
|
53
|
+
# anything else Karst cannot know about is still observable. Returning
|
|
54
|
+
# nil/false means "the application ran this request anonymously"; raising
|
|
55
|
+
# (or returning something with no id) makes the observation unobservable,
|
|
56
|
+
# never confirmed.
|
|
57
|
+
#
|
|
58
|
+
# A Devise/Warden application needs none of this: Karst reads the
|
|
59
|
+
# application's own Warden proxy from the probe request instead (see
|
|
60
|
+
# Karst::Identity::Observer). With neither seam available, Karst reports
|
|
61
|
+
# identity as unobservable rather than assuming the requested principal
|
|
62
|
+
# is the one that ran.
|
|
63
|
+
attr_accessor :observe_identity
|
|
64
|
+
|
|
37
65
|
# principal_populations is an escape hatch (committed, reviewable
|
|
38
66
|
# populations for CI); the four bounds after it are advanced tuning an
|
|
39
67
|
# ordinary developer should never need to see.
|
|
@@ -88,8 +116,10 @@ module Karst
|
|
|
88
116
|
access_scenario: "artifact scenarios were removed; Karst analyzes routes, not record sweeps"
|
|
89
117
|
}.freeze
|
|
90
118
|
|
|
119
|
+
# rubocop:disable Metrics/MethodLength
|
|
91
120
|
def initialize
|
|
92
121
|
@enabled = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env.development? || Rails.env.test? : false
|
|
122
|
+
@mcp_mutating_requests = false
|
|
93
123
|
@access_sweep_limit = 25
|
|
94
124
|
@principal_candidate_pool_size = 1_000
|
|
95
125
|
@population_retry_limit = 3
|
|
@@ -97,8 +127,10 @@ module Karst
|
|
|
97
127
|
@principal_populations = {}
|
|
98
128
|
@configured_principal_sources = nil
|
|
99
129
|
%i[@principals @assume_identity @clear_identity @principal_label
|
|
100
|
-
@assume_browser_identity @clear_browser_identity
|
|
130
|
+
@assume_browser_identity @clear_browser_identity
|
|
131
|
+
@observe_identity].each { |hook| instance_variable_set(hook, nil) }
|
|
101
132
|
end
|
|
133
|
+
# rubocop:enable Metrics/MethodLength
|
|
102
134
|
|
|
103
135
|
# An application-authored hint about meaningful candidate populations for
|
|
104
136
|
# whatever config.principals returns -- a Hash of name => zero-argument
|
|
@@ -149,8 +181,22 @@ module Karst
|
|
|
149
181
|
# what a "approved population" is. Resolved on every call rather than
|
|
150
182
|
# memoized: an approval revoked, or a scope deleted, must stop being
|
|
151
183
|
# executed on the next analysis without a server restart.
|
|
152
|
-
|
|
153
|
-
|
|
184
|
+
#
|
|
185
|
+
# That resolution is real work -- reading the approval (and selection)
|
|
186
|
+
# file, parsing model source, evaluating principal source callables -- so
|
|
187
|
+
# one high-level operation resolves it once and reuses the result (see
|
|
188
|
+
# Karst::Identity::Snapshot) rather than calling this repeatedly. Within
|
|
189
|
+
# one result, each source's records callable is evaluated at most once
|
|
190
|
+
# (see Access::PrincipalSource#evaluated_once).
|
|
191
|
+
#
|
|
192
|
+
# `record:`/`discovery:`, when given, are threaded straight to
|
|
193
|
+
# Access::ApprovedPopulations.merge -- a caller's already-loaded
|
|
194
|
+
# approval record and population discovery instance, reused instead of
|
|
195
|
+
# loaded and parsed again (see Karst::Identity::Snapshot).
|
|
196
|
+
def principal_sources(record: nil, discovery: nil)
|
|
197
|
+
sources = configured_or_inferred_sources
|
|
198
|
+
Access::ApprovedPopulations.merge(sources&.transform_values(&:evaluated_once), record: record,
|
|
199
|
+
discovery: discovery)
|
|
154
200
|
end
|
|
155
201
|
|
|
156
202
|
def access_sweep_limit=(value)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../value"
|
|
4
|
+
require_relative "observation"
|
|
5
|
+
|
|
6
|
+
module Karst
|
|
7
|
+
module Identity
|
|
8
|
+
# The three identities of one probe request, kept deliberately apart:
|
|
9
|
+
#
|
|
10
|
+
# requested -- what the caller asked Karst to execute as (intent).
|
|
11
|
+
# nil means an anonymous probe was requested.
|
|
12
|
+
# establishment -- what Karst's own identity seam managed to set up
|
|
13
|
+
# (setup), never evidence on its own.
|
|
14
|
+
# observed -- what the Rails application itself resolved as its
|
|
15
|
+
# authenticated principal while running the request
|
|
16
|
+
# (evidence).
|
|
17
|
+
#
|
|
18
|
+
# `confirmation` is the only field a machine consumer should read to
|
|
19
|
+
# decide whether a principal claim is true, and it fails closed:
|
|
20
|
+
#
|
|
21
|
+
# :confirmed requested a principal, observed that same one
|
|
22
|
+
# :confirmed_anonymous requested anonymous, observed no principal
|
|
23
|
+
# :mismatch requested a principal, observed a different one
|
|
24
|
+
# :absent requested a principal, observed none
|
|
25
|
+
# :contaminated requested anonymous, observed a principal
|
|
26
|
+
# :unobservable Karst could not determine what identity the
|
|
27
|
+
# application used
|
|
28
|
+
#
|
|
29
|
+
# Only :confirmed and :confirmed_anonymous are evidence about identity.
|
|
30
|
+
Evidence = Value.define(:requested, :observed, :confirmation, :observed_at, :observation_source,
|
|
31
|
+
:observation_error, :establishment, :establishment_error, :cleanup_error,
|
|
32
|
+
:changed_during_request) do
|
|
33
|
+
def confirmed?
|
|
34
|
+
%i[confirmed confirmed_anonymous].include?(confirmation)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def anonymous_probe?
|
|
38
|
+
requested.nil?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Derives one Evidence from the observations a probe actually collected.
|
|
43
|
+
module EvidenceBuilder
|
|
44
|
+
class << self
|
|
45
|
+
# `halt` is the observation taken at the moment an access callback
|
|
46
|
+
# halted the request -- the state the application had established
|
|
47
|
+
# when that access decision was made -- and `completion` the one
|
|
48
|
+
# taken after the request returned. Both may be nil (never attempted).
|
|
49
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
50
|
+
def build(requested:, halt: nil, completion: nil, establishment: nil,
|
|
51
|
+
establishment_error: nil, cleanup_error: nil)
|
|
52
|
+
decisive = decisive_observation(halt, completion)
|
|
53
|
+
Evidence.new(
|
|
54
|
+
requested: requested,
|
|
55
|
+
observed: decisive&.observable? ? decisive.principal : nil,
|
|
56
|
+
confirmation: confirmation(requested, decisive),
|
|
57
|
+
observed_at: decisive&.observable? ? phase_of(decisive, halt) : nil,
|
|
58
|
+
observation_source: decisive&.source,
|
|
59
|
+
observation_error: decisive&.error,
|
|
60
|
+
establishment: establishment,
|
|
61
|
+
establishment_error: establishment_error,
|
|
62
|
+
cleanup_error: cleanup_error,
|
|
63
|
+
changed_during_request: changed?(halt, completion)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# The halt moment is the one that explains an access decision, so it
|
|
71
|
+
# wins whenever it produced a real observation; a failed observation
|
|
72
|
+
# there still falls back to the completed request rather than
|
|
73
|
+
# discarding evidence Karst does hold.
|
|
74
|
+
def decisive_observation(halt, completion)
|
|
75
|
+
return halt if halt&.observable?
|
|
76
|
+
return completion if completion&.observable?
|
|
77
|
+
|
|
78
|
+
halt || completion
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def phase_of(decisive, halt)
|
|
82
|
+
decisive.equal?(halt) ? :halted_callback : :request_completion
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def confirmation(requested, decisive)
|
|
86
|
+
return :unobservable if decisive.nil? || !decisive.observable?
|
|
87
|
+
return anonymous_confirmation(decisive.principal) if requested.nil?
|
|
88
|
+
return :absent if decisive.principal.nil?
|
|
89
|
+
|
|
90
|
+
same?(requested, decisive.principal) ? :confirmed : :mismatch
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def anonymous_confirmation(observed)
|
|
94
|
+
observed.nil? ? :confirmed_anonymous : :contaminated
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def same?(requested, observed)
|
|
98
|
+
requested.model_name.to_s == observed.model_name.to_s &&
|
|
99
|
+
requested.id.to_s == observed.id.to_s
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def changed?(halt, completion)
|
|
103
|
+
return false unless halt&.observable? && completion&.observable?
|
|
104
|
+
|
|
105
|
+
halt.principal != completion.principal
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../value"
|
|
4
|
+
require_relative "devise_support"
|
|
5
|
+
|
|
6
|
+
module Karst
|
|
7
|
+
module Identity
|
|
8
|
+
# The one place a principal object's stable model name is derived, shared
|
|
9
|
+
# by requested-identity description (Identity.describe) and runtime
|
|
10
|
+
# observation below, so the two can never disagree about what "User"
|
|
11
|
+
# means for the same object.
|
|
12
|
+
module Naming
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def model_name_for(klass)
|
|
16
|
+
klass.respond_to?(:model_name) ? klass.model_name.name.to_s : klass.name.to_s
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# What the *application* resolved as its own authenticated principal
|
|
21
|
+
# during a probe request -- never what Karst asked for. Deliberately
|
|
22
|
+
# narrower than PrincipalDescriptor: an observation carries no display
|
|
23
|
+
# label or authentication identifier, because it is evidence about a
|
|
24
|
+
# request rather than a record Karst selected and can describe.
|
|
25
|
+
ObservedPrincipal = Value.define(:model_name, :id)
|
|
26
|
+
|
|
27
|
+
# Handed to config.observe_identity. `controller` is the exact
|
|
28
|
+
# ActionController instance that processed the probe request (Rails sets
|
|
29
|
+
# env["action_controller.instance"] in ActionController::Metal#set_request!
|
|
30
|
+
# before any callback runs, so it is present even for a request halted in
|
|
31
|
+
# a before_action); `request` is an ActionDispatch::Request over the same
|
|
32
|
+
# env, and `env` the raw Rack env.
|
|
33
|
+
ObservationContext = Value.define(:controller, :request, :env)
|
|
34
|
+
|
|
35
|
+
# One attempt to observe the application's runtime principal.
|
|
36
|
+
# `principal` is an ObservedPrincipal, or nil for "the application
|
|
37
|
+
# resolved no principal" -- which is only meaningful when `error` is nil.
|
|
38
|
+
# `source` names which seam produced it (:configured, :warden), so a
|
|
39
|
+
# consumer can tell an application-authored observation from an inferred
|
|
40
|
+
# one. A non-nil `error` means Karst could not determine what identity the
|
|
41
|
+
# application used at all, and nothing may be concluded from `principal`.
|
|
42
|
+
Observation = Value.define(:principal, :source, :error) do
|
|
43
|
+
def observable?
|
|
44
|
+
error.nil?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Observes the principal the Rails application actually used, from the
|
|
49
|
+
# runtime state of the probe request itself.
|
|
50
|
+
#
|
|
51
|
+
# Two seams, in order of authority:
|
|
52
|
+
#
|
|
53
|
+
# 1. config.observe_identity -- an application-authored callable given
|
|
54
|
+
# the controller instance that processed the request. This is the
|
|
55
|
+
# only seam that can observe an identity the application keeps
|
|
56
|
+
# somewhere Karst cannot know about (a @current_user set by a
|
|
57
|
+
# before_action, a Current attribute, a custom session lookup).
|
|
58
|
+
#
|
|
59
|
+
# 2. The Warden proxy on the probe request's own env, read through
|
|
60
|
+
# Warden's public Proxy#user. For a Devise application this is the
|
|
61
|
+
# same object Devise's own current_<scope> helper returns, and
|
|
62
|
+
# reading it neither runs authentication strategies nor mutates the
|
|
63
|
+
# request (Proxy#user only deserializes an existing session, unlike
|
|
64
|
+
# Proxy#authenticate).
|
|
65
|
+
#
|
|
66
|
+
# With neither available, Karst reports the observation as unobservable
|
|
67
|
+
# rather than assuming the requested identity was the one that ran.
|
|
68
|
+
module Observer
|
|
69
|
+
UNCONFIGURED = "no runtime identity observation seam is available; " \
|
|
70
|
+
"configure config.observe_identity (or use Karst's Devise/Warden integration)"
|
|
71
|
+
|
|
72
|
+
class << self
|
|
73
|
+
def observe(env, requested: nil)
|
|
74
|
+
return unobservable("the probe request environment was not captured") unless env.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
hook = Karst.config.observe_identity
|
|
77
|
+
return observe_with_hook(hook, env) if hook
|
|
78
|
+
return observe_with_warden(env, requested) if warden_proxy(env)
|
|
79
|
+
|
|
80
|
+
unobservable(UNCONFIGURED)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Public so Identity.describe and observation agree on model naming.
|
|
84
|
+
def model_name_for(klass)
|
|
85
|
+
Naming.model_name_for(klass)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def observe_with_hook(hook, env)
|
|
91
|
+
raise ConfigurationError, "config.observe_identity must be callable" unless hook.respond_to?(:call)
|
|
92
|
+
|
|
93
|
+
observed(hook.call(context_for(env)), :configured)
|
|
94
|
+
rescue ConfigurationError
|
|
95
|
+
raise
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
unobservable("config.observe_identity raised #{e.class}: #{e.message}")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def context_for(env)
|
|
101
|
+
ObservationContext.new(controller: env["action_controller.instance"],
|
|
102
|
+
request: action_dispatch_request(env), env: env)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def action_dispatch_request(env)
|
|
106
|
+
return nil unless defined?(ActionDispatch::Request)
|
|
107
|
+
|
|
108
|
+
ActionDispatch::Request.new(env)
|
|
109
|
+
rescue StandardError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Every scope the application might have authenticated under, most
|
|
114
|
+
# likely first: the requested principal's own Devise scope, then every
|
|
115
|
+
# other registered one (so an anonymous probe contaminated under some
|
|
116
|
+
# *other* scope is still seen), then Warden's own default scope for a
|
|
117
|
+
# plain non-Devise Warden setup.
|
|
118
|
+
def observe_with_warden(env, requested)
|
|
119
|
+
proxy = warden_proxy(env)
|
|
120
|
+
scopes(requested).each do |scope|
|
|
121
|
+
user = scope ? proxy.user(scope: scope) : proxy.user
|
|
122
|
+
return observed(user, :warden) if user
|
|
123
|
+
end
|
|
124
|
+
Observation.new(principal: nil, source: :warden, error: nil)
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
unobservable("reading the Warden proxy raised #{e.class}: #{e.message}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def scopes(requested)
|
|
130
|
+
registered = DeviseSupport.mappings.map(&:scope)
|
|
131
|
+
return [nil] if registered.empty?
|
|
132
|
+
|
|
133
|
+
preferred = requested && DeviseSupport.mapping_for(requested.class)&.scope
|
|
134
|
+
([preferred] + registered).compact.uniq
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# false and nil both mean "the application resolved no principal";
|
|
138
|
+
# anything that cannot state a stable identity is unobservable rather
|
|
139
|
+
# than silently reported as some principal.
|
|
140
|
+
def observed(value, source)
|
|
141
|
+
return Observation.new(principal: nil, source: source, error: nil) if value.nil? || value == false
|
|
142
|
+
|
|
143
|
+
id = identifier_of(value)
|
|
144
|
+
return unobservable("the observed principal (#{value.class}) has no usable id", source: source) if id.nil?
|
|
145
|
+
|
|
146
|
+
Observation.new(principal: ObservedPrincipal.new(model_name: model_name_for(value.class), id: id),
|
|
147
|
+
source: source, error: nil)
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
unobservable("describing the observed principal raised #{e.class}: #{e.message}", source: source)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def identifier_of(value)
|
|
153
|
+
value.respond_to?(:id) ? value.id : nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def warden_proxy(env)
|
|
157
|
+
proxy = env["warden"]
|
|
158
|
+
proxy if proxy.respond_to?(:user)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def unobservable(message, source: nil)
|
|
162
|
+
Observation.new(principal: nil, source: source, error: message)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -49,6 +49,22 @@ module Karst
|
|
|
49
49
|
defined?(Warden::Manager) && Warden::Manager.respond_to?(:on_request)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Drops any principal this thread queued but never consumed -- the
|
|
53
|
+
# exact state an anonymous probe must not inherit (see
|
|
54
|
+
# Access::IdentityProbe). A queued principal survives only until the
|
|
55
|
+
# next Warden request on this thread, so a probe whose request never
|
|
56
|
+
# reached Warden (construction failure, an exception before dispatch)
|
|
57
|
+
# would otherwise leave one behind for whichever probe ran next.
|
|
58
|
+
def discard_pending!
|
|
59
|
+
Karst::ExecutionContext.delete(PENDING_PRINCIPAL_KEY)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The queued sign-in runs inside the probed request, so it is marked
|
|
63
|
+
# as identity setup (Identity.establishing): nothing it or the
|
|
64
|
+
# application's own after_set_user hooks do -- a Devise :trackable
|
|
65
|
+
# UPDATE, say -- is evidence about the probed route. If it raises,
|
|
66
|
+
# the route never runs, and EstablishmentError says so rather than
|
|
67
|
+
# letting the failure pass for the route's own exception.
|
|
52
68
|
def install_hook!
|
|
53
69
|
return if @installed
|
|
54
70
|
|
|
@@ -57,9 +73,18 @@ module Karst
|
|
|
57
73
|
pending = Karst::ExecutionContext.delete(PENDING_PRINCIPAL_KEY)
|
|
58
74
|
next unless pending
|
|
59
75
|
|
|
60
|
-
|
|
76
|
+
apply_pending(proxy, *pending)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def apply_pending(proxy, principal, scope)
|
|
83
|
+
Karst::Identity.establishing do
|
|
61
84
|
scope ? proxy.set_user(principal, scope: scope) : proxy.set_user(principal)
|
|
62
85
|
end
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
raise Karst::Identity::EstablishmentError, "#{e.class}: #{e.message}"
|
|
63
88
|
end
|
|
64
89
|
end
|
|
65
90
|
|