legionio 1.9.28 → 1.9.29
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/CHANGELOG.md +19 -0
- data/lib/legion/api/identity_audit.rb +2 -2
- data/lib/legion/api/llm.rb +8 -1
- data/lib/legion/cli/setup_command.rb +38 -1
- data/lib/legion/extensions/actors/subscription.rb +14 -1
- data/lib/legion/extensions/transport.rb +9 -0
- data/lib/legion/identity/broker.rb +4 -10
- data/lib/legion/identity/lease_renewer.rb +5 -4
- data/lib/legion/identity/resolver.rb +115 -79
- data/lib/legion/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 952eb59489faed844f57a13d5fb71a10d4f6e737b3af38f96cc9ab7beec5a9da
|
|
4
|
+
data.tar.gz: e8af435fd6f5caa5324f678c45a38d7c8bde94a801a80c8380032f43d9721fbf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1cd3acf9f4e8af3da0db483123afbeb5b9c8b69430dbaf538474523a3d5fb02aa8e5fd9ac262244a61ed1f9a0ed7add934bdc59ecdd239a3cd8621ddbdec02e4
|
|
7
|
+
data.tar.gz: dc7a8cc98adcd63d75419cc29d88f025539185fc2c9254d9c9a1b8abdeaeadc1ff7e3e1481de1352e398a63826c74e41975f8c13708e462a6dcebb35fd97ef4b
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.9.29] - 2026-05-11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `Subscription#activate` now checks `channel.open?` before calling `subscribe_with`; if closed, it re-prepares once and retries, preventing silent activation failures when a channel is closed between prepare and activate.
|
|
9
|
+
- `Transport` mixin now guards `auto_create_dlx_exchange` and `auto_create_dlx_queue` with a `remote_invocable?` check — non-remote extensions no longer attempt to create dead-letter exchanges and queues they never use.
|
|
10
|
+
- DLX exchange type corrected from `fanout` to `topic` for consistency with the rest of the exchange topology.
|
|
11
|
+
- Identity resolver DB persistence now uses Sequel models (`Identity::Provider`, `Identity::Principal`, `Identity::Identity`, `Identity::AuditLog`) instead of raw `Legion::Data.db` dataset calls that didn't exist on the module.
|
|
12
|
+
- Identity audit API endpoint now references correct column names (`detail_payload`, `node_ref`, `session_ref`) matching the schema.
|
|
13
|
+
- Fixed `LeaseRenewer#log_renewal_failure` to fall back to `$stderr` when `Legion::Logging` is not yet loaded, matching the original contract.
|
|
14
|
+
- Fixed `Legion::Service#log_privacy_mode_status`, `#shutdown_component`, and TLS-fallback logging specs to assert against `emit_tagged` (the actual dispatch path used by `Legion::Logging::Helper`) rather than `Legion::Logging.warn/info` directly.
|
|
15
|
+
- Fixed `Cluster::Leader` boot integration spec to stub `Legion::Settings[:logging]` so `log` helper initialization does not raise on unexpectedly-received arguments.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Added `remote_invocable_extension?` helper to the `Transport` module; returns `lex_class.remote_invocable?` when available, `true` otherwise.
|
|
19
|
+
- Refactored `Identity::Resolver#persist_to_db` into extracted helpers (`upsert_providers`, `upsert_principal`, `upsert_identities`, `upsert_single_identity`) to reduce method complexity and improve readability.
|
|
20
|
+
- Replaced hand-rolled `log_warn`/`log_debug` methods in `Identity::Resolver`, `Identity::Broker`, and `Identity::LeaseRenewer` with `include Legion::Logging::Helper` and standard `log.debug`/`log.warn` calls.
|
|
21
|
+
- Added debug logging throughout `Identity::Resolver` for registration, resolution, auth racing, binding, and DB persistence.
|
|
22
|
+
- LLM inference API now passes `instance` and `tier` routing hints from request body through to `Legion::LLM::Inference::Request`.
|
|
23
|
+
|
|
5
24
|
## [1.9.28] - 2026-05-08
|
|
6
25
|
|
|
7
26
|
### Fixed
|
|
@@ -35,8 +35,8 @@ module Legion
|
|
|
35
35
|
records = dataset.order(Sequel.desc(:created_at)).limit(100).all
|
|
36
36
|
json_collection(records.map do |r|
|
|
37
37
|
{ id: r.id, event_type: r.event_type, provider_name: r.provider_name,
|
|
38
|
-
trust_level: r.trust_level,
|
|
39
|
-
|
|
38
|
+
trust_level: r.trust_level, detail_payload: r.detail_payload,
|
|
39
|
+
node_ref: r.node_ref, session_ref: r.session_ref, created_at: r.created_at }
|
|
40
40
|
end)
|
|
41
41
|
end
|
|
42
42
|
end
|
data/lib/legion/api/llm.rb
CHANGED
|
@@ -268,16 +268,23 @@ module Legion
|
|
|
268
268
|
end
|
|
269
269
|
|
|
270
270
|
caller_metadata = body[:metadata].is_a?(Hash) ? body[:metadata] : {}
|
|
271
|
+
instance = body[:instance]
|
|
272
|
+
tier = body[:tier]
|
|
271
273
|
request_args = {
|
|
272
274
|
messages: messages,
|
|
273
275
|
system: body[:system],
|
|
274
|
-
routing: { provider: provider, model: model },
|
|
276
|
+
routing: { provider: provider, model: model, instance: instance }.compact,
|
|
275
277
|
caller: caller_ctx,
|
|
276
278
|
conversation_id: body[:conversation_id],
|
|
277
279
|
metadata: caller_metadata.merge(requested_tools: requested_tools),
|
|
278
280
|
stream: streaming,
|
|
279
281
|
cache: { strategy: :default, cacheable: true }
|
|
280
282
|
}
|
|
283
|
+
if tier
|
|
284
|
+
halt 400, Legion::JSON.dump({ error: 'invalid tier' }) unless tier.is_a?(String)
|
|
285
|
+
halt 400, Legion::JSON.dump({ error: 'invalid tier' }) unless %w[local fleet auto].include?(tier)
|
|
286
|
+
request_args[:extra] = { tier: tier.to_sym }
|
|
287
|
+
end
|
|
281
288
|
request_args[:tools] = tool_classes if tools_present
|
|
282
289
|
|
|
283
290
|
req = Legion::LLM::Inference::Request.build(**request_args)
|
|
@@ -55,10 +55,27 @@ module Legion
|
|
|
55
55
|
description: 'LLM routing and provider integration (no cognitive stack)',
|
|
56
56
|
gems: %w[
|
|
57
57
|
legion-llm lex-llm lex-llm-anthropic lex-llm-azure-foundry
|
|
58
|
-
lex-llm-bedrock lex-llm-gemini lex-llm-
|
|
58
|
+
lex-llm-bedrock lex-llm-gemini lex-llm-mlx
|
|
59
59
|
lex-llm-ollama lex-llm-openai lex-llm-vertex lex-llm-vllm
|
|
60
60
|
]
|
|
61
61
|
},
|
|
62
|
+
gaia: {
|
|
63
|
+
description: 'Cognitive coordination engine + agentic extensions (GAIA stack)',
|
|
64
|
+
gems: %w[
|
|
65
|
+
legion-gaia
|
|
66
|
+
lex-agentic-affect lex-agentic-attention lex-agentic-defense
|
|
67
|
+
lex-agentic-executive lex-agentic-homeostasis lex-agentic-inference
|
|
68
|
+
lex-agentic-integration lex-agentic-language lex-agentic-learning
|
|
69
|
+
lex-agentic-memory lex-agentic-self lex-agentic-social
|
|
70
|
+
lex-synapse lex-mind-growth lex-tick
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
identity: {
|
|
74
|
+
description: 'Identity and access management (RBAC + identity providers)',
|
|
75
|
+
gems: %w[
|
|
76
|
+
legion-rbac lex-identity-system lex-identity-kerberos
|
|
77
|
+
]
|
|
78
|
+
},
|
|
62
79
|
channels: {
|
|
63
80
|
description: 'Channel adapters for chat platforms',
|
|
64
81
|
gems: %w[lex-slack lex-microsoft_teams]
|
|
@@ -153,6 +170,18 @@ module Legion
|
|
|
153
170
|
install_pack(:llm)
|
|
154
171
|
end
|
|
155
172
|
|
|
173
|
+
desc 'gaia', 'Install cognitive coordination engine and agentic extensions (GAIA stack)'
|
|
174
|
+
option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed without installing'
|
|
175
|
+
def gaia
|
|
176
|
+
install_pack(:gaia)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
desc 'identity', 'Install identity and access management (RBAC + identity providers)'
|
|
180
|
+
option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed without installing'
|
|
181
|
+
def identity
|
|
182
|
+
install_pack(:identity)
|
|
183
|
+
end
|
|
184
|
+
|
|
156
185
|
desc 'channels', 'Install channel adapters (Slack, Teams)'
|
|
157
186
|
option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed without installing'
|
|
158
187
|
def channels
|
|
@@ -494,6 +523,14 @@ module Legion
|
|
|
494
523
|
puts ' Next steps:'
|
|
495
524
|
puts ' legion chat # interactive AI conversation'
|
|
496
525
|
puts ' legion llm status # check provider connectivity'
|
|
526
|
+
when :gaia
|
|
527
|
+
puts ' Next steps:'
|
|
528
|
+
puts ' legion start # start daemon with cognitive stack'
|
|
529
|
+
puts ' legion start --lite # single-process, no external services'
|
|
530
|
+
when :identity
|
|
531
|
+
puts ' Next steps:'
|
|
532
|
+
puts ' Configure RBAC in settings: {"rbac": {"enabled": true}}'
|
|
533
|
+
puts ' legion start # start daemon with identity services'
|
|
497
534
|
when :channels
|
|
498
535
|
puts ' Next steps:'
|
|
499
536
|
puts ' Configure channels in settings: {"gaia": {"channels": {"slack": {"enabled": true}}}}'
|
|
@@ -110,7 +110,20 @@ module Legion
|
|
|
110
110
|
log.warn "[Subscription] skipping activate for #{lex_name}/#{runner_name}: no consumer (prepare failed?)"
|
|
111
111
|
return
|
|
112
112
|
end
|
|
113
|
-
|
|
113
|
+
|
|
114
|
+
if @queue.channel.open?
|
|
115
|
+
@queue.subscribe_with(@consumer)
|
|
116
|
+
else
|
|
117
|
+
log.warn "[Subscription] channel closed before activate for #{lex_name}/#{runner_name}, re-preparing"
|
|
118
|
+
prepare
|
|
119
|
+
if @consumer && @queue.channel.open?
|
|
120
|
+
@queue.subscribe_with(@consumer)
|
|
121
|
+
else
|
|
122
|
+
log.error "[Subscription] re-prepare failed for #{lex_name}/#{runner_name}, skipping activate"
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
114
127
|
log.info "[Subscription] activated: #{lex_name}/#{runner_name} (consumer registered)"
|
|
115
128
|
end
|
|
116
129
|
|
|
@@ -71,6 +71,8 @@ module Legion
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def auto_create_dlx_exchange
|
|
74
|
+
return unless remote_invocable_extension?
|
|
75
|
+
|
|
74
76
|
dlx = if transport_class::Exchanges.const_defined?('Dlx', false)
|
|
75
77
|
transport_class::Exchanges::Dlx
|
|
76
78
|
else
|
|
@@ -89,6 +91,7 @@ module Legion
|
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
def auto_create_dlx_queue
|
|
94
|
+
return unless remote_invocable_extension?
|
|
92
95
|
return if transport_class::Queues.const_defined?('Dlx', false)
|
|
93
96
|
|
|
94
97
|
special_name = default_exchange.new.exchange_name
|
|
@@ -207,6 +210,12 @@ module Legion
|
|
|
207
210
|
def additional_e_to_q
|
|
208
211
|
[]
|
|
209
212
|
end
|
|
213
|
+
|
|
214
|
+
def remote_invocable_extension?
|
|
215
|
+
return lex_class.remote_invocable? if lex_class.respond_to?(:remote_invocable?)
|
|
216
|
+
|
|
217
|
+
true
|
|
218
|
+
end
|
|
210
219
|
end
|
|
211
220
|
end
|
|
212
221
|
end
|
|
@@ -10,6 +10,8 @@ module Legion
|
|
|
10
10
|
AUDIT_DROP_LOG_INTERVAL = 100
|
|
11
11
|
|
|
12
12
|
class << self
|
|
13
|
+
include Legion::Logging::Helper
|
|
14
|
+
|
|
13
15
|
def token_for(provider_name, qualifier: nil, for_context: nil, purpose: nil, context: nil)
|
|
14
16
|
name = provider_name.to_sym
|
|
15
17
|
resolved = resolve_qualifier(name, qualifier: qualifier, for_context: for_context)
|
|
@@ -235,7 +237,7 @@ module Legion
|
|
|
235
237
|
|
|
236
238
|
if audit_queue.size >= AUDIT_QUEUE_MAX
|
|
237
239
|
drops = (@audit_drops ||= Concurrent::AtomicFixnum.new(0)).increment
|
|
238
|
-
|
|
240
|
+
log.warn("Audit queue full, dropping event (total drops: #{drops})") if (drops % AUDIT_DROP_LOG_INTERVAL).zero?
|
|
239
241
|
else
|
|
240
242
|
audit_queue.push(event)
|
|
241
243
|
end
|
|
@@ -289,7 +291,7 @@ module Legion
|
|
|
289
291
|
nil
|
|
290
292
|
end
|
|
291
293
|
rescue StandardError => e
|
|
292
|
-
|
|
294
|
+
log.warn("Broker.db_groups failed: #{e.message}")
|
|
293
295
|
[]
|
|
294
296
|
end
|
|
295
297
|
|
|
@@ -298,14 +300,6 @@ module Legion
|
|
|
298
300
|
Legion::Data.respond_to?(:connected?) &&
|
|
299
301
|
Legion::Data.connected?
|
|
300
302
|
end
|
|
301
|
-
|
|
302
|
-
def log_warn(message)
|
|
303
|
-
if defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn)
|
|
304
|
-
Legion::Logging.warn("[Identity::Broker] #{message}")
|
|
305
|
-
else
|
|
306
|
-
$stderr.puts "[Identity::Broker] #{message}" # rubocop:disable Style/StderrPuts
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
303
|
end
|
|
310
304
|
|
|
311
305
|
# Initialize atomics at module definition time
|
|
@@ -5,6 +5,8 @@ require 'concurrent'
|
|
|
5
5
|
module Legion
|
|
6
6
|
module Identity
|
|
7
7
|
class LeaseRenewer
|
|
8
|
+
include Legion::Logging::Helper
|
|
9
|
+
|
|
8
10
|
attr_reader :provider_name, :provider
|
|
9
11
|
|
|
10
12
|
BACKOFF_SLEEP = 5
|
|
@@ -70,11 +72,10 @@ module Legion
|
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def log_renewal_failure(error)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Legion::Logging.warn(message)
|
|
75
|
+
if defined?(Legion::Logging)
|
|
76
|
+
log.warn("renewal failed: #{error.message}")
|
|
76
77
|
else
|
|
77
|
-
$stderr.puts message # rubocop:disable Style/StderrPuts
|
|
78
|
+
$stderr.puts "[LeaseRenewer][#{@provider_name}] renewal failed: #{error.message}" # rubocop:disable Style/StderrPuts
|
|
78
79
|
end
|
|
79
80
|
end
|
|
80
81
|
end
|
|
@@ -13,25 +13,34 @@ module Legion
|
|
|
13
13
|
TIMEOUT_SECONDS = 5
|
|
14
14
|
|
|
15
15
|
class << self
|
|
16
|
+
include Legion::Logging::Helper
|
|
17
|
+
|
|
16
18
|
def register(provider)
|
|
17
19
|
return if @providers.any? { |p| p.provider_name == provider.provider_name }
|
|
18
20
|
|
|
21
|
+
log.debug("register: #{provider.provider_name} type=#{provider.provider_type} trust=#{provider.trust_level}")
|
|
19
22
|
@providers << provider
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def resolve!(timeout: TIMEOUT_SECONDS)
|
|
26
|
+
log.debug("resolve!: starting with #{@providers.size} providers, timeout=#{timeout}s")
|
|
23
27
|
drain_pending_registrations
|
|
24
28
|
|
|
25
29
|
auth_providers, profile_providers, fallback_providers = partition_providers
|
|
30
|
+
log.debug("resolve!: partitioned auth=#{auth_providers.map(&:provider_name)} " \
|
|
31
|
+
"profile=#{profile_providers.map(&:provider_name)} " \
|
|
32
|
+
"fallback=#{fallback_providers.map(&:provider_name)}")
|
|
26
33
|
|
|
27
34
|
winning_provider, winning_result, provider_results = resolve_auth(auth_providers, timeout: timeout)
|
|
28
35
|
|
|
29
36
|
if winning_provider.nil?
|
|
37
|
+
log.debug('resolve!: no auth winner, trying fallback providers')
|
|
30
38
|
winning_provider, winning_result, fallback_results = resolve_auth(fallback_providers, timeout: timeout)
|
|
31
39
|
provider_results.merge!(fallback_results) if fallback_results
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
unless winning_provider
|
|
43
|
+
log.debug('resolve!: no provider resolved, identity unresolved')
|
|
35
44
|
@resolved.make_false
|
|
36
45
|
@composite.set(nil)
|
|
37
46
|
return nil
|
|
@@ -40,8 +49,10 @@ module Legion
|
|
|
40
49
|
canonical = winning_result[:canonical_name]
|
|
41
50
|
trust_level = winning_provider.trust_level
|
|
42
51
|
source = winning_provider.provider_name
|
|
52
|
+
log.debug("resolve!: winner=#{source} canonical=#{canonical} trust=#{trust_level}")
|
|
43
53
|
|
|
44
54
|
profile_data = resolve_profiles(profile_providers, canonical, timeout: timeout)
|
|
55
|
+
log.debug("resolve!: profiles resolved groups=#{profile_data[:groups].size} profile_keys=#{profile_data[:profile].keys}")
|
|
45
56
|
|
|
46
57
|
composite = assemble_composite(
|
|
47
58
|
provider_results, profile_data,
|
|
@@ -51,6 +62,7 @@ module Legion
|
|
|
51
62
|
)
|
|
52
63
|
|
|
53
64
|
bind_and_persist(winning_provider, composite, trust_level)
|
|
65
|
+
log.debug("resolve!: complete canonical=#{composite[:canonical_name]} providers=#{composite[:providers].keys}")
|
|
54
66
|
composite
|
|
55
67
|
end
|
|
56
68
|
|
|
@@ -58,6 +70,8 @@ module Legion
|
|
|
58
70
|
current = @composite.get
|
|
59
71
|
return unless current
|
|
60
72
|
|
|
73
|
+
log.debug("upgrade!: provider=#{provider.provider_name} trust=#{provider.trust_level} current_canonical=#{current[:canonical_name]}")
|
|
74
|
+
|
|
61
75
|
new_trust = provider.trust_level
|
|
62
76
|
new_canonical = result[:canonical_name] || current[:canonical_name]
|
|
63
77
|
canonical_changed = new_canonical != current[:canonical_name]
|
|
@@ -109,6 +123,7 @@ module Legion
|
|
|
109
123
|
|
|
110
124
|
persist_identity_json(new_canonical, updated[:kind]) unless new_trust == :unverified
|
|
111
125
|
|
|
126
|
+
log.debug("upgrade!: complete canonical=#{new_canonical} trust=#{effective_trust} canonical_changed=#{canonical_changed}")
|
|
112
127
|
updated
|
|
113
128
|
end
|
|
114
129
|
|
|
@@ -145,6 +160,7 @@ module Legion
|
|
|
145
160
|
pending = Legion::Identity.pending_registrations
|
|
146
161
|
return if pending.nil? || pending.empty?
|
|
147
162
|
|
|
163
|
+
log.debug("drain_pending_registrations: draining #{pending.size} pending providers")
|
|
148
164
|
drained = []
|
|
149
165
|
drained << pending.shift until pending.empty?
|
|
150
166
|
drained.each { |p| register(p) }
|
|
@@ -172,6 +188,7 @@ module Legion
|
|
|
172
188
|
def resolve_auth(auth_providers, timeout:)
|
|
173
189
|
return [nil, nil, {}] if auth_providers.empty?
|
|
174
190
|
|
|
191
|
+
log.debug("resolve_auth: racing #{auth_providers.map(&:provider_name)} timeout=#{timeout}s")
|
|
175
192
|
futures = auth_providers.map do |provider|
|
|
176
193
|
Concurrent::Promises.future { provider.resolve }
|
|
177
194
|
end
|
|
@@ -179,10 +196,13 @@ module Legion
|
|
|
179
196
|
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout
|
|
180
197
|
provider_results = {}
|
|
181
198
|
auth_providers.zip(futures).each do |provider, future|
|
|
199
|
+
result = nil
|
|
182
200
|
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
183
201
|
future.wait(remaining.positive? ? remaining : 0)
|
|
184
202
|
result = future.value(0) if future.resolved?
|
|
185
203
|
status = auth_future_status(future, result)
|
|
204
|
+
log.debug("resolve_auth: #{provider.provider_name} status=#{status}" \
|
|
205
|
+
"#{" canonical=#{result[:canonical_name]}" if status == :resolved}")
|
|
186
206
|
|
|
187
207
|
provider_results[provider.provider_name] = {
|
|
188
208
|
status: status,
|
|
@@ -195,6 +215,7 @@ module Legion
|
|
|
195
215
|
|
|
196
216
|
resolved_entries = provider_results.select { |_, v| v[:status] == :resolved }
|
|
197
217
|
if resolved_entries.empty?
|
|
218
|
+
log.debug('resolve_auth: no providers resolved')
|
|
198
219
|
[nil, nil, provider_results]
|
|
199
220
|
else
|
|
200
221
|
winner_name = resolved_entries.min_by do |_, v|
|
|
@@ -202,6 +223,7 @@ module Legion
|
|
|
202
223
|
[-p.priority, p.trust_weight]
|
|
203
224
|
end&.first
|
|
204
225
|
|
|
226
|
+
log.debug("resolve_auth: winner=#{winner_name}")
|
|
205
227
|
winner_info = provider_results[winner_name]
|
|
206
228
|
[winner_info[:provider], winner_info[:result], provider_results]
|
|
207
229
|
end
|
|
@@ -302,11 +324,13 @@ module Legion
|
|
|
302
324
|
end
|
|
303
325
|
|
|
304
326
|
def bind_and_persist(winning_provider, composite, trust_level)
|
|
327
|
+
log.debug("bind_and_persist: binding provider=#{winning_provider.provider_name} trust=#{trust_level}")
|
|
305
328
|
Legion::Identity::Process.bind!(winning_provider, composite) if defined?(Legion::Identity::Process)
|
|
306
329
|
|
|
307
330
|
if defined?(Legion::Settings) && Legion::Settings.respond_to?(:loader) && Legion::Settings.loader.respond_to?(:settings)
|
|
308
331
|
Legion::Settings.loader.settings[:client] ||= {}
|
|
309
332
|
Legion::Settings.loader.settings[:client][:name] = Legion::Identity::Process.queue_prefix
|
|
333
|
+
log.debug("bind_and_persist: client name set to #{Legion::Identity::Process.queue_prefix}")
|
|
310
334
|
end
|
|
311
335
|
|
|
312
336
|
persist_to_db(composite)
|
|
@@ -314,72 +338,26 @@ module Legion
|
|
|
314
338
|
|
|
315
339
|
@composite.set(composite)
|
|
316
340
|
@resolved.make_true
|
|
341
|
+
log.debug('bind_and_persist: resolved=true')
|
|
317
342
|
end
|
|
318
343
|
|
|
319
|
-
def persist_to_db(composite)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
db = Legion::Data.db
|
|
324
|
-
|
|
325
|
-
composite[:providers]&.each_key do |name|
|
|
326
|
-
db[:identity_providers].insert_conflict(
|
|
327
|
-
target: :name,
|
|
328
|
-
update: { updated_at: now }
|
|
329
|
-
).insert(
|
|
330
|
-
uuid: SecureRandom.uuid,
|
|
331
|
-
name: name.to_s,
|
|
332
|
-
provider_type: 'authenticate',
|
|
333
|
-
facing: 'both',
|
|
334
|
-
source: 'resolver',
|
|
335
|
-
enabled: true,
|
|
336
|
-
created_at: now,
|
|
337
|
-
updated_at: now
|
|
338
|
-
)
|
|
344
|
+
def persist_to_db(composite)
|
|
345
|
+
unless defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
|
|
346
|
+
log.debug('persist_to_db: skipped — Legion::Data not connected')
|
|
347
|
+
return
|
|
339
348
|
end
|
|
340
349
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
uuid: SecureRandom.uuid,
|
|
346
|
-
canonical_name: composite[:canonical_name],
|
|
347
|
-
kind: composite[:kind].to_s,
|
|
348
|
-
active: true,
|
|
349
|
-
last_seen_at: now,
|
|
350
|
-
created_at: now,
|
|
351
|
-
updated_at: now
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
principal_row = db[:identity_principals].where(
|
|
355
|
-
canonical_name: composite[:canonical_name], kind: composite[:kind].to_s
|
|
356
|
-
).first
|
|
357
|
-
principal_id = principal_row[:id] if principal_row
|
|
358
|
-
|
|
359
|
-
composite[:aliases]&.each do |provider_name, identities|
|
|
360
|
-
provider_row = db[:identity_providers].where(name: provider_name.to_s).first
|
|
361
|
-
next unless provider_row
|
|
350
|
+
log.debug("persist_to_db: persisting canonical=#{composite[:canonical_name]} providers=#{composite[:providers]&.keys}")
|
|
351
|
+
now = Time.now.utc
|
|
352
|
+
provider_model = Legion::Data::Model::Identity::Provider
|
|
353
|
+
audit_model = Legion::Data::Model::Identity::AuditLog
|
|
362
354
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
update: { last_authenticated_at: now, updated_at: now }
|
|
367
|
-
).insert(
|
|
368
|
-
uuid: SecureRandom.uuid,
|
|
369
|
-
principal_id: principal_id,
|
|
370
|
-
provider_id: provider_row[:id],
|
|
371
|
-
provider_identity_key: ident,
|
|
372
|
-
active: true,
|
|
373
|
-
last_authenticated_at: now,
|
|
374
|
-
created_at: now,
|
|
375
|
-
updated_at: now
|
|
376
|
-
)
|
|
377
|
-
end
|
|
378
|
-
end
|
|
355
|
+
upsert_providers(composite, provider_model, now)
|
|
356
|
+
principal = upsert_principal(composite, now)
|
|
357
|
+
upsert_identities(composite, provider_model, principal, now)
|
|
379
358
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
principal_id: principal_id,
|
|
359
|
+
audit_model.create(
|
|
360
|
+
principal_id: principal.id,
|
|
383
361
|
event_type: 'identity.resolved',
|
|
384
362
|
provider_name: composite[:source].to_s,
|
|
385
363
|
trust_level: composite[:trust]&.to_s,
|
|
@@ -392,11 +370,79 @@ module Legion
|
|
|
392
370
|
}
|
|
393
371
|
),
|
|
394
372
|
node_ref: composite[:node_id],
|
|
395
|
-
session_ref: @session_id
|
|
396
|
-
created_at: now
|
|
373
|
+
session_ref: @session_id
|
|
397
374
|
)
|
|
398
375
|
rescue StandardError => e
|
|
399
|
-
|
|
376
|
+
log.warn("DB persistence failed: #{e.message}")
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def upsert_providers(composite, provider_model, now)
|
|
380
|
+
composite[:providers]&.each_key do |name|
|
|
381
|
+
existing = provider_model.where(name: name.to_s).first
|
|
382
|
+
if existing
|
|
383
|
+
existing.update(updated_at: now)
|
|
384
|
+
else
|
|
385
|
+
provider_model.create(
|
|
386
|
+
name: name.to_s,
|
|
387
|
+
provider_type: 'authenticate',
|
|
388
|
+
facing: 'both',
|
|
389
|
+
source: 'resolver',
|
|
390
|
+
enabled: true
|
|
391
|
+
)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def upsert_principal(composite, now)
|
|
397
|
+
principal_model = Legion::Data::Model::Identity::Principal
|
|
398
|
+
principal = principal_model.where(
|
|
399
|
+
canonical_name: composite[:canonical_name],
|
|
400
|
+
kind: composite[:kind].to_s
|
|
401
|
+
).first
|
|
402
|
+
|
|
403
|
+
if principal
|
|
404
|
+
principal.update(last_seen_at: now, updated_at: now)
|
|
405
|
+
principal
|
|
406
|
+
else
|
|
407
|
+
principal_model.create(
|
|
408
|
+
canonical_name: composite[:canonical_name],
|
|
409
|
+
kind: composite[:kind].to_s,
|
|
410
|
+
active: true,
|
|
411
|
+
last_seen_at: now
|
|
412
|
+
)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def upsert_identities(composite, provider_model, principal, now)
|
|
417
|
+
identity_model = Legion::Data::Model::Identity::Identity
|
|
418
|
+
composite[:aliases]&.each do |provider_name, identities|
|
|
419
|
+
provider_row = provider_model.where(name: provider_name.to_s).first
|
|
420
|
+
next unless provider_row
|
|
421
|
+
|
|
422
|
+
Array(identities).each do |ident|
|
|
423
|
+
upsert_single_identity(identity_model, principal, provider_row, ident, now)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def upsert_single_identity(identity_model, principal, provider_row, ident, now)
|
|
429
|
+
existing = identity_model.where(
|
|
430
|
+
principal_id: principal.id,
|
|
431
|
+
provider_id: provider_row.id,
|
|
432
|
+
provider_identity_key: ident
|
|
433
|
+
).first
|
|
434
|
+
|
|
435
|
+
if existing
|
|
436
|
+
existing.update(last_authenticated_at: now, updated_at: now)
|
|
437
|
+
else
|
|
438
|
+
identity_model.create(
|
|
439
|
+
principal_id: principal.id,
|
|
440
|
+
provider_id: provider_row.id,
|
|
441
|
+
provider_identity_key: ident,
|
|
442
|
+
active: true,
|
|
443
|
+
last_authenticated_at: now
|
|
444
|
+
)
|
|
445
|
+
end
|
|
400
446
|
end
|
|
401
447
|
|
|
402
448
|
def persist_identity_json(canonical_name, kind)
|
|
@@ -412,7 +458,7 @@ module Legion
|
|
|
412
458
|
end
|
|
413
459
|
File.write(path, json)
|
|
414
460
|
rescue StandardError => e
|
|
415
|
-
|
|
461
|
+
log.warn("identity.json write failed: #{e.message}")
|
|
416
462
|
end
|
|
417
463
|
|
|
418
464
|
def handle_canonical_change(old_canonical, new_canonical, _composite)
|
|
@@ -424,25 +470,15 @@ module Legion
|
|
|
424
470
|
|
|
425
471
|
return unless defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
|
|
426
472
|
|
|
427
|
-
|
|
428
|
-
Legion::Data.
|
|
429
|
-
|
|
430
|
-
principal_id: old_row&.dig(:id),
|
|
473
|
+
old_principal = Legion::Data::Model::Identity::Principal.where(canonical_name: old_canonical).first
|
|
474
|
+
Legion::Data::Model::Identity::AuditLog.create(
|
|
475
|
+
principal_id: old_principal&.id,
|
|
431
476
|
event_type: 'identity.canonical_changed',
|
|
432
477
|
provider_name: 'resolver',
|
|
433
|
-
detail_payload: Legion::JSON.dump({ old: old_canonical, new: new_canonical })
|
|
434
|
-
created_at: Time.now
|
|
478
|
+
detail_payload: Legion::JSON.dump({ old: old_canonical, new: new_canonical })
|
|
435
479
|
)
|
|
436
480
|
rescue StandardError => e
|
|
437
|
-
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
def log_warn(message)
|
|
441
|
-
if defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn)
|
|
442
|
-
Legion::Logging.warn("[Identity::Resolver] #{message}")
|
|
443
|
-
else
|
|
444
|
-
$stderr.puts "[Identity::Resolver] #{message}" # rubocop:disable Style/StderrPuts
|
|
445
|
-
end
|
|
481
|
+
log.warn("canonical change handling failed: #{e.message}")
|
|
446
482
|
end
|
|
447
483
|
end
|
|
448
484
|
|
data/lib/legion/version.rb
CHANGED