Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,15 @@ 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:
- 'spec/**/*'
- 'lib/legion/data/migrations/*'
ThreadSafety/ClassInstanceVariable:
Enabled: false
Legion/Llm/TaxonomyEnum:
Enabled: false
ThreadSafety/ClassAndModuleAttributes:
Enabled: false
Metrics/AbcSize:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
20 changes: 20 additions & 0 deletions lib/legion/data/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
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 = {
Expand Down Expand Up @@ -129,7 +133,7 @@
end

def close
@mutex.synchronize do

Check notice on line 136 in lib/legion/data/connection.rb

View workflow job for this annotation

GitHub Actions / lint / Framework Conventions

mutex-nested-sync

Mutex detected. Verify no method called inside this block re-acquires the same mutex (deadlock risk).
@closed = true
@file.close unless @file.closed?
end
Expand All @@ -138,7 +142,7 @@
private

def write(level, message)
@mutex.synchronize do

Check notice on line 145 in lib/legion/data/connection.rb

View workflow job for this annotation

GitHub Actions / lint / Framework Conventions

mutex-nested-sync

Mutex detected. Verify no method called inside this block re-acquires the same mutex (deadlock risk).
return if @closed || @file.closed?

@file.puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')}] #{level} #{message}"
Expand Down Expand Up @@ -171,6 +175,9 @@
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?

Expand Down Expand Up @@ -410,10 +417,23 @@

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 = {}
Expand Down
2 changes: 1 addition & 1 deletion lib/legion/data/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Legion
module Data
VERSION = '1.10.6'
VERSION = '1.10.7'
end
end
3 changes: 2 additions & 1 deletion spec/legion/data/connection_fallback_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions spec/legion/data/connection_reconnect_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 75 additions & 1 deletion spec/legion/data/connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -72,6 +72,80 @@
end
end

describe 'unresolved URI placeholder detection' do
before do
Legion::Settings[:data][:adapter] = 'postgres'
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

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'
}

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

describe Legion::Data::Connection::QueryFileLogger do
around do |example|
Dir.mktmpdir('legion-data-query-log') do |dir|
Expand Down
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading