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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 861fe6533a77e8819c03400430f32ddb420a7cfa29e042b3c86734f6d7fd57bc
4
- data.tar.gz: 48fe377db280cad64c5e9331926a24319a44c78c96eeb9aa7e2c6aa038a77024
3
+ metadata.gz: 869153710ece6717285edf2de7483f4c9f5fe5b493837681b80d05b77b014971
4
+ data.tar.gz: 2813fd707f0f7a80858802046b7ba2a2f23611f94a1515c66e5c7a5db1771dd6
5
5
  SHA512:
6
- metadata.gz: 3850a5774dbc1de772fd8a2b4762dddc9da3164e9cff32b5f02b946f7608dba551394bcb3b2aa90590f038199f33da655957c74896ff74419e840a4769b7008e
7
- data.tar.gz: 006b1ed6fd955a876dabfa6875fa075eb8abfec1aa59467b3411e36e3ce32752735277b4013ba908db280cb98a4c152bb91e14563be55c41d6ff53778c3f944a
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
- log.info "Connected to #{adapter}://#{conn_user}@#{conn_host}:#{conn_port}/#{conn_db}"
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(
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Data
5
- VERSION = '1.10.6'
5
+ VERSION = '1.10.8'
6
6
  end
7
7
  end
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.6
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