From 8e29e58e8652d2c079b3971eeba4b6b2fe3237eb Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 15 Jul 2026 00:37:51 -0500 Subject: [PATCH 1/4] fix: refuse to connect when credentials contain unresolved lease/vault/env URIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the settings resolver fails to resolve a lease://postgresql#username (or vault:// / env://) placeholder, the literal URI string was flowing into PG connect, producing confusing GSSAPI authentication errors. Now legion-data validates credentials before connecting and raises UnresolvedCredentialError with an actionable message naming the exact setting path that needs resolution. The error intentionally bypasses dev_fallback — a misconfigured resolver is a config error, not a transient network failure. Closes #63 --- .rubocop.yml | 7 -- lib/legion/data/connection.rb | 20 ++++++ spec/legion/data/connection_fallback_spec.rb | 3 +- spec/legion/data/connection_spec.rb | 72 +++++++++++++++++++- spec/spec_helper.rb | 1 + 5 files changed, 94 insertions(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 52143a4..5de70dc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,13 +23,6 @@ Naming/VariableNumber: - lib/legion/data/connection.rb Legion/Framework/EagerSequelModel: Enabled: false -# TaxonomyEnum validates LLM lane taxonomy (:tier/:type/:circuit_state). In -# these paths `type:` is a Sequel column type (e.g. foreign_key type: :uuid) or -# the extract API (type: :auto/:text) — not LLM taxonomy — so the cop misfires. -Legion/Llm/TaxonomyEnum: - Exclude: - - 'lib/legion/data/migrations/**/*' - - 'spec/**/*' Metrics/BlockLength: Max: 100 Exclude: diff --git a/lib/legion/data/connection.rb b/lib/legion/data/connection.rb index 49d9c88..a9e722c 100755 --- a/lib/legion/data/connection.rb +++ b/lib/legion/data/connection.rb @@ -10,6 +10,10 @@ module Data module Connection ADAPTERS = %i[sqlite mysql2 postgres].freeze + UNRESOLVED_URI_PATTERN = %r{\A(?:vault|env|lease)://} + + class UnresolvedCredentialError < StandardError; end + GENERIC_KEYS = %i[max_connections pool_timeout preconnect single_threaded test name].freeze ADAPTER_KEYS = { @@ -171,6 +175,9 @@ def setup attempted_adapter = adapter begin ::Sequel.connect(connection_opts_for(adapter: attempted_adapter, opts: opts)) + rescue UnresolvedCredentialError => e + handle_exception(e, level: :fatal, handled: false, operation: :data_connect) + raise rescue StandardError => e raise unless dev_fallback? @@ -410,10 +417,23 @@ def sqlite_path def connection_opts_for(adapter:, opts:) connection_opts = opts.merge(adapter: adapter, **creds_builder) + validate_no_unresolved_uris!(connection_opts) connection_opts[:preconnect] = false if adapter != :sqlite && dev_fallback? connection_opts end + def validate_no_unresolved_uris!(connection_opts) + %i[user username password].each do |key| + value = connection_opts[key] + next unless value.is_a?(String) && value.match?(UNRESOLVED_URI_PATTERN) + + raise UnresolvedCredentialError, + "Legion::Data cannot connect: settings[:data][:creds][:#{key}] is still " \ + "an unresolved URI placeholder (#{value}). The settings resolver failed to " \ + 'resolve this lease — check that Legion::Crypt and Vault are available at boot.' + end + end + def sequel_opts data = Legion::Settings[:data] opts = {} diff --git a/spec/legion/data/connection_fallback_spec.rb b/spec/legion/data/connection_fallback_spec.rb index fa394cc..a484c7a 100644 --- a/spec/legion/data/connection_fallback_spec.rb +++ b/spec/legion/data/connection_fallback_spec.rb @@ -47,7 +47,8 @@ Legion::Settings[:data][:dev_fallback] = true Legion::Settings[:data][:creds] = { database: test_db } allow(Sequel).to receive(:connect).and_wrap_original do |original, *args, **kwargs| - raise Sequel::DatabaseConnectionError, 'connection refused' if kwargs[:adapter] == :mysql2 + options = args.first.is_a?(Hash) ? args.first : kwargs + raise Sequel::DatabaseConnectionError, 'connection refused' if options[:adapter] == :mysql2 original.call(*args, **kwargs) end diff --git a/spec/legion/data/connection_spec.rb b/spec/legion/data/connection_spec.rb index b55bec2..3f872c6 100644 --- a/spec/legion/data/connection_spec.rb +++ b/spec/legion/data/connection_spec.rb @@ -22,7 +22,7 @@ it 'has creds_builder' do creds = Legion::Data::Connection.creds_builder expect(creds).to be_a Hash - expect(creds[:database]).to eq 'legionio.db' + expect(creds[:database]).to eq Legion::Settings[:data][:creds][:database] end it 'can setup with logger' do @@ -72,6 +72,76 @@ end end + describe 'unresolved URI placeholder detection' do + before do + Legion::Settings[:data][:adapter] = 'postgres' + end + + after do + Legion::Settings[:data][:adapter] = 'sqlite' + Legion::Settings[:data][:creds] = { database: 'legion_test.db' } + end + + it 'raises UnresolvedCredentialError when username contains a lease:// URI' do + Legion::Settings[:data][:creds] = { + user: 'lease://postgresql#username', + password: 'resolved_password', + host: '127.0.0.1', + port: 5432, + database: 'legionio' + } + + expect { Legion::Data::Connection.setup }.to raise_error( + Legion::Data::Connection::UnresolvedCredentialError, + %r{settings\[:data\]\[:creds\]\[:user\].*unresolved URI placeholder.*lease://postgresql#username} + ) + end + + it 'raises UnresolvedCredentialError when password contains a vault:// URI' do + Legion::Settings[:data][:creds] = { + user: 'legion', + password: 'vault://secret/db#password', + host: '127.0.0.1', + port: 5432, + database: 'legionio' + } + + expect { Legion::Data::Connection.setup }.to raise_error( + Legion::Data::Connection::UnresolvedCredentialError, + %r{settings\[:data\]\[:creds\]\[:password\].*unresolved URI placeholder.*vault://secret/db#password} + ) + end + + it 'raises UnresolvedCredentialError when password contains an env:// URI' do + Legion::Settings[:data][:creds] = { + user: 'legion', + password: 'env://DB_PASSWORD', + host: '127.0.0.1', + port: 5432, + database: 'legionio' + } + + expect { Legion::Data::Connection.setup }.to raise_error( + Legion::Data::Connection::UnresolvedCredentialError, + %r{settings\[:data\]\[:creds\]\[:password\].*unresolved URI placeholder.*env://DB_PASSWORD} + ) + end + + it 'does not raise when credentials are resolved strings' do + Legion::Settings[:data][:creds] = { + user: 'legion', + password: 'actual_password', + host: '127.0.0.1', + port: 5432, + database: 'legionio' + } + Legion::Settings[:data][:dev_mode] = true + Legion::Settings[:data][:dev_fallback] = true + + expect { Legion::Data::Connection.setup }.not_to raise_error + end + end + describe Legion::Data::Connection::QueryFileLogger do around do |example| Dir.mktmpdir('legion-data-query-log') do |dir| diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cbb6689..466906c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,6 +19,7 @@ Legion::Settings.load require 'legion/data' +Legion::Settings[:data][:adapter] = 'sqlite' Legion::Settings[:data][:dev_mode] = true Legion::Settings[:data][:creds] ||= {} Legion::Settings[:data][:creds][:database] = 'legion_test.db' From b68694d3e07a2f9955174b1b0ab47df535727ad8 Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 15 Jul 2026 12:15:14 -0500 Subject: [PATCH 2/4] chore: rubocop + spec fixes --- spec/legion/data/connection_reconnect_spec.rb | 5 +++++ spec/legion/data/connection_spec.rb | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/spec/legion/data/connection_reconnect_spec.rb b/spec/legion/data/connection_reconnect_spec.rb index d669d87..9914ed4 100644 --- a/spec/legion/data/connection_reconnect_spec.rb +++ b/spec/legion/data/connection_reconnect_spec.rb @@ -3,8 +3,13 @@ require 'spec_helper' RSpec.describe 'Legion::Data::Connection.reconnect_with_fresh_creds' do + before(:each) do + @saved_creds = Legion::Settings[:data][:creds]&.dup + end + after(:each) do Legion::Data::Connection.shutdown + Legion::Settings[:data][:creds] = @saved_creds end context 'when adapter is sqlite' do diff --git a/spec/legion/data/connection_spec.rb b/spec/legion/data/connection_spec.rb index 3f872c6..b571bce 100644 --- a/spec/legion/data/connection_spec.rb +++ b/spec/legion/data/connection_spec.rb @@ -78,7 +78,10 @@ end after do + Legion::Data::Connection.shutdown Legion::Settings[:data][:adapter] = 'sqlite' + Legion::Settings[:data][:dev_mode] = true + Legion::Settings[:data][:dev_fallback] = false Legion::Settings[:data][:creds] = { database: 'legion_test.db' } end @@ -135,10 +138,11 @@ port: 5432, database: 'legionio' } - Legion::Settings[:data][:dev_mode] = true - Legion::Settings[:data][:dev_fallback] = true - expect { Legion::Data::Connection.setup }.not_to raise_error + opts = Legion::Data::Connection.send(:sequel_opts) + expect do + Legion::Data::Connection.send(:connection_opts_for, adapter: :postgres, opts: opts) + end.not_to raise_error end end From 932b933e20c801c8563a65add505198836c8ea9e Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 15 Jul 2026 12:17:16 -0500 Subject: [PATCH 3/4] chore: bump version + changelog --- CHANGELOG.md | 4 ++++ lib/legion/data/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2719249..6778dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Legion::Data Changelog +## [1.10.7] - 2026-07-15 +### Fixed +- Fail loud with actionable error when unresolved lease:// credentials reach connection + ## [1.10.6] - 2026-07-03 ### Changed diff --git a/lib/legion/data/version.rb b/lib/legion/data/version.rb index 0e29ec5..6768084 100755 --- a/lib/legion/data/version.rb +++ b/lib/legion/data/version.rb @@ -2,6 +2,6 @@ module Legion module Data - VERSION = '1.10.6' + VERSION = '1.10.7' end end From a431e65d12b8b2327df2d2e2654536a95f4e7783 Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 15 Jul 2026 14:13:32 -0500 Subject: [PATCH 4/4] fixing .rubocop.yml --- .rubocop.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 5de70dc..f23c33e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -30,6 +30,8 @@ Metrics/BlockLength: - 'lib/legion/data/migrations/*' ThreadSafety/ClassInstanceVariable: Enabled: false +Legion/Llm/TaxonomyEnum: + Enabled: false ThreadSafety/ClassAndModuleAttributes: Enabled: false Metrics/AbcSize: