legion-data 1.10.6 → 1.10.8
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/.rubocop.yml +2 -7
- data/CHANGELOG.md +8 -0
- data/lib/legion/data/auth_failure_handler.rb +80 -0
- data/lib/legion/data/connection.rb +53 -1
- data/lib/legion/data/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 869153710ece6717285edf2de7483f4c9f5fe5b493837681b80d05b77b014971
|
|
4
|
+
data.tar.gz: 2813fd707f0f7a80858802046b7ba2a2f23611f94a1515c66e5c7a5db1771dd6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41aff7bbce675a95d8a2ec6683eb0350decde2cf66854971b0c43fed1d4a09bdc50563e922bfe29d5a01660c522d579be7f2719495150d136ba262f433ba4ac4
|
|
7
|
+
data.tar.gz: c766be4f0a13cf3ac3dc91e305d275742d5c49206ec90819631d05739137fb1c9eadbc550feeef1d6e635961ec8bf439b9b5fc19dfc334d1798859573d87902a
|
data/.rubocop.yml
CHANGED
|
@@ -23,13 +23,6 @@ Naming/VariableNumber:
|
|
|
23
23
|
- lib/legion/data/connection.rb
|
|
24
24
|
Legion/Framework/EagerSequelModel:
|
|
25
25
|
Enabled: false
|
|
26
|
-
# TaxonomyEnum validates LLM lane taxonomy (:tier/:type/:circuit_state). In
|
|
27
|
-
# these paths `type:` is a Sequel column type (e.g. foreign_key type: :uuid) or
|
|
28
|
-
# the extract API (type: :auto/:text) — not LLM taxonomy — so the cop misfires.
|
|
29
|
-
Legion/Llm/TaxonomyEnum:
|
|
30
|
-
Exclude:
|
|
31
|
-
- 'lib/legion/data/migrations/**/*'
|
|
32
|
-
- 'spec/**/*'
|
|
33
26
|
Metrics/BlockLength:
|
|
34
27
|
Max: 100
|
|
35
28
|
Exclude:
|
|
@@ -37,6 +30,8 @@ Metrics/BlockLength:
|
|
|
37
30
|
- 'lib/legion/data/migrations/*'
|
|
38
31
|
ThreadSafety/ClassInstanceVariable:
|
|
39
32
|
Enabled: false
|
|
33
|
+
Legion/Llm/TaxonomyEnum:
|
|
34
|
+
Enabled: false
|
|
40
35
|
ThreadSafety/ClassAndModuleAttributes:
|
|
41
36
|
Enabled: false
|
|
42
37
|
Metrics/AbcSize:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Legion::Data Changelog
|
|
2
2
|
|
|
3
|
+
## [1.10.8] - 2026-07-23
|
|
4
|
+
### Fixed
|
|
5
|
+
- Detect auth failures (`role does not exist`, `password authentication failed`) on Sequel pool connections and trigger immediate Vault lease reissue via LeaseManager instead of retrying dead credentials forever. 30-second cooldown prevents reissue storms during bulk credential rotation.
|
|
6
|
+
|
|
7
|
+
## [1.10.7] - 2026-07-15
|
|
8
|
+
### Fixed
|
|
9
|
+
- Fail loud with actionable error when unresolved lease:// credentials reach connection
|
|
10
|
+
|
|
3
11
|
## [1.10.6] - 2026-07-03
|
|
4
12
|
|
|
5
13
|
### Changed
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module AuthFailureHandler
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
10
|
+
AUTH_FAILURE_PATTERNS = [
|
|
11
|
+
/role .* does not exist/i,
|
|
12
|
+
/password authentication failed/i,
|
|
13
|
+
/authentication failed/i,
|
|
14
|
+
/no pg_hba\.conf entry/i,
|
|
15
|
+
/permission denied for table/i,
|
|
16
|
+
/permission denied for relation/i
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
REISSUE_COOLDOWN = 30
|
|
20
|
+
|
|
21
|
+
@last_reissue_at = nil
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
|
|
24
|
+
module SequelHook
|
|
25
|
+
def connect(server)
|
|
26
|
+
super
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
Legion::Data::AuthFailureHandler.handle(e)
|
|
29
|
+
raise
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def raise_error(exception, opts = Sequel::OPTS)
|
|
33
|
+
Legion::Data::AuthFailureHandler.handle(exception)
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
def install(sequel_db)
|
|
40
|
+
sequel_db.singleton_class.prepend(SequelHook)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def handle(error)
|
|
44
|
+
return unless auth_failure?(error)
|
|
45
|
+
return if on_cooldown?
|
|
46
|
+
|
|
47
|
+
request_reissue(error)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def auth_failure?(error)
|
|
51
|
+
message = error.message.to_s
|
|
52
|
+
AUTH_FAILURE_PATTERNS.any? { |pattern| message.match?(pattern) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def on_cooldown?
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
return false unless @last_reissue_at
|
|
58
|
+
|
|
59
|
+
(Time.now - @last_reissue_at) < REISSUE_COOLDOWN
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def request_reissue(error, adapter: :postgresql)
|
|
64
|
+
@mutex.synchronize { @last_reissue_at = Time.now }
|
|
65
|
+
log.error("Legion::Data auth failure detected: #{error.message} — requesting lease reissue")
|
|
66
|
+
|
|
67
|
+
return unless defined?(Legion::Crypt::LeaseManager)
|
|
68
|
+
|
|
69
|
+
Legion::Crypt::LeaseManager.instance.reissue_lease(adapter)
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
handle_exception(e, level: :error, handled: true, operation: :auth_failure_reissue)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reset!
|
|
75
|
+
@mutex.synchronize { @last_reissue_at = nil }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/data/auth_failure_handler'
|
|
4
5
|
|
|
5
6
|
require 'fileutils'
|
|
6
7
|
require 'sequel'
|
|
@@ -10,6 +11,10 @@ module Legion
|
|
|
10
11
|
module Connection
|
|
11
12
|
ADAPTERS = %i[sqlite mysql2 postgres].freeze
|
|
12
13
|
|
|
14
|
+
UNRESOLVED_URI_PATTERN = %r{\A(?:vault|env|lease)://}
|
|
15
|
+
|
|
16
|
+
class UnresolvedCredentialError < StandardError; end
|
|
17
|
+
|
|
13
18
|
GENERIC_KEYS = %i[max_connections pool_timeout preconnect single_threaded test name].freeze
|
|
14
19
|
|
|
15
20
|
ADAPTER_KEYS = {
|
|
@@ -171,6 +176,9 @@ module Legion
|
|
|
171
176
|
attempted_adapter = adapter
|
|
172
177
|
begin
|
|
173
178
|
::Sequel.connect(connection_opts_for(adapter: attempted_adapter, opts: opts))
|
|
179
|
+
rescue UnresolvedCredentialError => e
|
|
180
|
+
handle_exception(e, level: :fatal, handled: false, operation: :data_connect)
|
|
181
|
+
raise
|
|
174
182
|
rescue StandardError => e
|
|
175
183
|
raise unless dev_fallback?
|
|
176
184
|
|
|
@@ -292,6 +300,7 @@ module Legion
|
|
|
292
300
|
@sequel.opts[:password] = new_pass
|
|
293
301
|
|
|
294
302
|
@sequel.disconnect
|
|
303
|
+
expire_all_pooled_connections
|
|
295
304
|
|
|
296
305
|
@sequel.test_connection
|
|
297
306
|
log.info("reconnect_with_fresh_creds: rotated credentials (#{old_user} → #{new_user})")
|
|
@@ -302,6 +311,16 @@ module Legion
|
|
|
302
311
|
false
|
|
303
312
|
end
|
|
304
313
|
|
|
314
|
+
def expire_all_pooled_connections
|
|
315
|
+
pool = @sequel.pool
|
|
316
|
+
return unless pool.instance_variable_defined?(:@connection_expiration_timestamps)
|
|
317
|
+
|
|
318
|
+
timestamps = pool.instance_variable_get(:@connection_expiration_timestamps)
|
|
319
|
+
timestamps.each_key { |conn| timestamps[conn] = [0, 0].freeze }
|
|
320
|
+
rescue StandardError => e
|
|
321
|
+
handle_exception(e, level: :warn, handled: true, operation: :expire_all_pooled_connections)
|
|
322
|
+
end
|
|
323
|
+
|
|
305
324
|
def connect_with_replicas
|
|
306
325
|
return unless adapter == :postgres
|
|
307
326
|
|
|
@@ -390,10 +409,22 @@ module Legion
|
|
|
390
409
|
conn_host = actual[:host] || '127.0.0.1'
|
|
391
410
|
conn_port = actual[:port]
|
|
392
411
|
conn_db = actual[:database] || actual[:db]
|
|
393
|
-
|
|
412
|
+
lease_id = current_lease_id
|
|
413
|
+
msg = "Connected to #{adapter}://#{conn_user}@#{conn_host}:#{conn_port}/#{conn_db}"
|
|
414
|
+
msg += " lease_id=#{lease_id}" if lease_id
|
|
415
|
+
log.info msg
|
|
394
416
|
end
|
|
395
417
|
end
|
|
396
418
|
|
|
419
|
+
def current_lease_id
|
|
420
|
+
return unless defined?(Legion::Crypt::LeaseManager)
|
|
421
|
+
|
|
422
|
+
Legion::Crypt::LeaseManager.instance.active_leases.dig(:postgresql, :lease_id)
|
|
423
|
+
rescue StandardError => e
|
|
424
|
+
handle_exception(e, level: :warn, handled: true, operation: :current_lease_id)
|
|
425
|
+
nil
|
|
426
|
+
end
|
|
427
|
+
|
|
397
428
|
def dev_fallback?
|
|
398
429
|
data_settings = Legion::Settings[:data]
|
|
399
430
|
data_settings[:dev_mode] == true && data_settings[:dev_fallback] != false
|
|
@@ -410,10 +441,23 @@ module Legion
|
|
|
410
441
|
|
|
411
442
|
def connection_opts_for(adapter:, opts:)
|
|
412
443
|
connection_opts = opts.merge(adapter: adapter, **creds_builder)
|
|
444
|
+
validate_no_unresolved_uris!(connection_opts)
|
|
413
445
|
connection_opts[:preconnect] = false if adapter != :sqlite && dev_fallback?
|
|
414
446
|
connection_opts
|
|
415
447
|
end
|
|
416
448
|
|
|
449
|
+
def validate_no_unresolved_uris!(connection_opts)
|
|
450
|
+
%i[user username password].each do |key|
|
|
451
|
+
value = connection_opts[key]
|
|
452
|
+
next unless value.is_a?(String) && value.match?(UNRESOLVED_URI_PATTERN)
|
|
453
|
+
|
|
454
|
+
raise UnresolvedCredentialError,
|
|
455
|
+
"Legion::Data cannot connect: settings[:data][:creds][:#{key}] is still " \
|
|
456
|
+
"an unresolved URI placeholder (#{value}). The settings resolver failed to " \
|
|
457
|
+
'resolve this lease — check that Legion::Crypt and Vault are available at boot.'
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
417
461
|
def sequel_opts
|
|
418
462
|
data = Legion::Settings[:data]
|
|
419
463
|
opts = {}
|
|
@@ -588,10 +632,18 @@ module Legion
|
|
|
588
632
|
@sequel.extension(:connection_expiration)
|
|
589
633
|
@sequel.pool.connection_expiration_timeout = data[:connection_expiration_timeout]
|
|
590
634
|
end
|
|
635
|
+
|
|
636
|
+
install_auth_failure_hook
|
|
591
637
|
rescue StandardError => e
|
|
592
638
|
handle_exception(e, level: :warn, handled: true, operation: :configure_extensions, adapter: adapter)
|
|
593
639
|
end
|
|
594
640
|
|
|
641
|
+
def install_auth_failure_hook
|
|
642
|
+
Legion::Data::AuthFailureHandler.install(@sequel)
|
|
643
|
+
rescue StandardError => e
|
|
644
|
+
handle_exception(e, level: :warn, handled: true, operation: :install_auth_failure_hook)
|
|
645
|
+
end
|
|
646
|
+
|
|
595
647
|
def build_data_logger
|
|
596
648
|
tagged = if defined?(Legion::Logging::TaggedLogger) && respond_to?(:tagged_logger_settings, true)
|
|
597
649
|
Legion::Logging::TaggedLogger.new(
|
data/lib/legion/data/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-data
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.10.
|
|
4
|
+
version: 1.10.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -124,6 +124,7 @@ files:
|
|
|
124
124
|
- lib/legion/data/archiver.rb
|
|
125
125
|
- lib/legion/data/audit_log_hash_chain.rb
|
|
126
126
|
- lib/legion/data/audit_record.rb
|
|
127
|
+
- lib/legion/data/auth_failure_handler.rb
|
|
127
128
|
- lib/legion/data/connection.rb
|
|
128
129
|
- lib/legion/data/encryption/cipher.rb
|
|
129
130
|
- lib/legion/data/encryption/key_provider.rb
|