legion-crypt 1.4.19 → 1.4.22

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: e3a746c03498b392437f0d7db81caca5f6daa84aeda465bbf0db71bb898d25f1
4
- data.tar.gz: d27dfcdfa9032fa6bc8df4174f370c7896151c746bf8b810421d1c28d5d8e51b
3
+ metadata.gz: ec95cba72f1350ed61da82d296c14ddf6729cce61785ac908fbcf98256935e48
4
+ data.tar.gz: 15770beb8714e14a4ff14c42473a9f55ef0de549683e299b6b4d044da513249e
5
5
  SHA512:
6
- metadata.gz: 9e3a72a7a2fc6b78439f582b33c3c667fb40f1ef86c8a0201fb53e38d2d122e3689fa3496e11df36e5085a551753d58864a8033bc2cb028662922d87ce7afc9a
7
- data.tar.gz: ab485a6da390007f060067bcca2863a5585fd6845f2dfc023658d48c6195364af7f99bf86f2492830d7991ae6a600a43fbd819d715412ca3f9c210fa95a830e3
6
+ metadata.gz: aea19ef52f31d1d7b5e8ddd8392749fd40e7305eef410958ae9012128dd8c79698df37baf557ef25c0b34ebf8ba6821ef42900346a28d73dee5241c44024039a
7
+ data.tar.gz: ba9ab25d478aaeed112a41362ca4b587ea0acacf1745f5518f4867a936372567dc9f7bb906b7855a5596973d020da1629f7349608d81e7ec907e1965a4777686
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [1.4.22] - 2026-03-27
4
+
5
+ ### Changed
6
+ - Replace split `log.error(e.message); log.error(e.backtrace)` patterns with single `Legion::Logging.log_exception` calls in `vault.rb`, `cluster_secret.rb`, and `settings.rb` for structured exception events
7
+ - Guard all `log_exception` call sites in `vault.rb`, `settings.rb`, and `cluster_secret.rb` with `Legion::Logging` presence checks (`defined?` in `vault.rb`/`cluster_secret.rb`, `Legion.const_defined?('Logging')` in `settings.rb`) plus `Legion::Logging.respond_to?(:log_exception)`; fall back to `Legion::Logging.fatal`/`error` or `warn` to preserve structured logging in environments where `log_exception` is unavailable
8
+ - `from_transport` and `cs` rescue blocks in `cluster_secret.rb` now use the same 4-branch guard (log_exception / Logging.error / Logging.warn / Kernel.warn) and explicitly return `nil` to preserve expected return types
9
+ - Fallback `.error`/`.warn`/`Kernel.warn` branches in `from_transport` and `cs` include the first 10 backtrace lines for debuggability parity with the prior `e.backtrace[0..10]` logging; `Vault#connect_vault` warn fallback omits backtrace to keep health-check failure messages concise
10
+ - `cs` rescue adds final `Kernel.warn` fallback so exceptions are never silently swallowed when `Legion::Logging` is absent
11
+
12
+ ### Added
13
+ - Specs for `connect_vault` rescue logging: asserts `false` return and covers log_exception / Logging.error / warn fallback branches when `Vault.sys.health_status` raises
14
+ - Specs for `from_transport` and `cs` rescue paths: asserts `nil` return and covers all logging fallback branches (including `Kernel.warn`) plus `Legion::Logging` absent case
15
+ - Duplicate invocation eliminated in rescue-path specs: single call stored in `result`, both no-raise and return value asserted on that one call
16
+
17
+ ## [1.4.20] - 2026-03-27
18
+
19
+ ### Fixed
20
+ - `Vault#read`: unwrap KV v2 response envelope — `logical.read` returns `{data: {keys}, metadata: {}}` for KV v2 mounts; the nested `:data` key is now auto-detected and unwrapped
21
+
22
+ ### Added
23
+ - Debug logging throughout Vault auth, read, and cluster connection paths (`vault.rb`, `vault_cluster.rb`, `kerberos_auth.rb`, `lease_manager.rb`)
24
+ - `Vault#log_read_context`: logs path and namespace context for each Vault read
25
+ - `Vault#unwrap_kv_v2`: detects and unwraps KV v2 envelope pattern
26
+ - `VaultCluster`: debug logging for cluster connection, client build, and Kerberos auth flow
27
+ - `KerberosAuth`: debug logging for SPN, token exchange, policies, and renewal metadata
28
+ - `LeaseManager`: debug logging for lease fetch, renewal, and revocation
29
+
3
30
  ## [1.4.19] - 2026-03-26
4
31
 
5
32
  ### Fixed
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Encryption, secrets management, JWT token management, and HashiCorp Vault integration for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides AES-256-CBC message encryption, RSA key pair generation, cluster secret management, JWT issue/verify operations, Vault token lifecycle management, and multi-cluster Vault connectivity.
4
4
 
5
- **Version**: 1.4.15
5
+ **Version**: 1.4.22
6
6
 
7
7
  ## Installation
8
8
 
@@ -64,8 +64,14 @@ module Legion
64
64
  Legion::Logging.error 'Cluster secret is still unknown!'
65
65
  nil
66
66
  rescue StandardError => e
67
- Legion::Logging.error e.message
68
- Legion::Logging.error e.backtrace[0..10]
67
+ if defined?(Legion::Logging) && Legion::Logging.respond_to?(:log_exception)
68
+ Legion::Logging.log_exception(e, lex: 'crypt', component_type: :helper)
69
+ elsif defined?(Legion::Logging) && Legion::Logging.respond_to?(:error)
70
+ Legion::Logging.error "from_transport failed: #{e.class}=#{e.message}\n#{Array(e.backtrace).first(10).join("\n")}"
71
+ else
72
+ warn "from_transport failed: #{e.class}=#{e.message}\n#{Array(e.backtrace).first(10).join("\n")}"
73
+ end
74
+ nil
69
75
  end
70
76
 
71
77
  def force_cluster_secret
@@ -114,8 +120,19 @@ module Legion
114
120
  def cs
115
121
  @cs ||= Digest::SHA256.digest(find_cluster_secret)
116
122
  rescue StandardError => e
117
- Legion::Logging.error e.message
118
- Legion::Logging.error e.backtrace[0..10]
123
+ if defined?(Legion::Logging) && Legion::Logging.respond_to?(:log_exception)
124
+ Legion::Logging.log_exception(e, lex: 'crypt', component_type: :helper)
125
+ elsif defined?(Legion::Logging) && Legion::Logging.respond_to?(:error)
126
+ backtrace = Array(e.backtrace).first(10).join("\n")
127
+ Legion::Logging.error "Legion::Crypt::ClusterSecret#cs failed: #{e.class}: #{e.message}\n#{backtrace}"
128
+ elsif defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn)
129
+ backtrace = Array(e.backtrace).first(10).join("\n")
130
+ Legion::Logging.warn "Legion::Crypt::ClusterSecret#cs failed: #{e.class}: #{e.message}\n#{backtrace}"
131
+ else
132
+ backtrace = Array(e.backtrace).first(10).join("\n")
133
+ ::Kernel.warn "Legion::Crypt::ClusterSecret#cs failed: #{e.class}: #{e.message}\n#{backtrace}"
134
+ end
135
+ nil
119
136
  end
120
137
 
121
138
  def validate_hex(value, length = secret_length)
@@ -17,10 +17,19 @@ module Legion
17
17
  def self.login(vault_client:, service_principal:, auth_path: DEFAULT_AUTH_PATH)
18
18
  raise GemMissingError, 'lex-kerberos gem is required for Kerberos auth' unless spnego_available?
19
19
 
20
+ log_debug("login: SPN=#{service_principal}, auth_path=#{auth_path}")
21
+ addr = vault_client.respond_to?(:address) ? vault_client.address : 'n/a'
22
+ ns = vault_client.respond_to?(:namespace) ? vault_client.namespace.inspect : 'n/a'
23
+ log_debug("login: vault_client.address=#{addr}, namespace=#{ns}")
24
+
20
25
  @kerberos_principal = nil
21
26
  token = obtain_token(service_principal)
27
+ log_debug("login: SPNEGO token obtained (#{token.length} chars)")
28
+
22
29
  result = exchange_token(vault_client, token, auth_path)
23
30
  @kerberos_principal = result[:metadata]&.dig('username') || result[:metadata]&.dig(:username)
31
+ log_debug("login: authenticated as #{@kerberos_principal.inspect}, policies=#{result[:policies].inspect}")
32
+ log_debug("login: renewable=#{result[:renewable]}, ttl=#{result[:lease_duration]}s")
24
33
  result
25
34
  end
26
35
 
@@ -41,6 +50,11 @@ module Legion
41
50
  @kerberos_principal = nil
42
51
  end
43
52
 
53
+ def self.log_debug(message)
54
+ Legion::Logging.debug("KerberosAuth: #{message}") if defined?(Legion::Logging)
55
+ end
56
+ private_class_method :log_debug
57
+
44
58
  class << self
45
59
  private
46
60
 
@@ -58,6 +72,7 @@ module Legion
58
72
 
59
73
  # The Vault Kerberos plugin reads the SPNEGO token from the HTTP
60
74
  # Authorization header, not the JSON body.
75
+ log_debug("exchange_token: PUT /v1/#{auth_path} (namespace=#{vault_client.respond_to?(:namespace) ? vault_client.namespace.inspect : 'n/a'})")
61
76
  json = vault_client.put(
62
77
  "/v1/#{auth_path}",
63
78
  '{}',
@@ -75,6 +90,7 @@ module Legion
75
90
  metadata: auth.metadata
76
91
  }
77
92
  rescue ::Vault::HTTPClientError => e
93
+ log_debug("exchange_token: HTTP error: #{e.message}")
78
94
  raise AuthError, "Vault Kerberos auth failed: #{e.message}"
79
95
  end
80
96
  end
@@ -27,7 +27,10 @@ module Legion
27
27
 
28
28
  begin
29
29
  response = logical.read(path)
30
- next unless response
30
+ unless response
31
+ log_warn("LeaseManager: no data at '#{name}' (#{path}) — path may not exist or role not configured")
32
+ next
33
+ end
31
34
 
32
35
  @lease_cache[name] = response.data || {}
33
36
  @active_leases[name] = {
@@ -44,6 +47,10 @@ module Legion
44
47
  end
45
48
  end
46
49
 
50
+ def fetched_count
51
+ @active_leases.size
52
+ end
53
+
47
54
  def fetch(name, key)
48
55
  data = @lease_cache[name.to_sym] || @lease_cache[name.to_s]
49
56
  return nil unless data
@@ -67,9 +67,10 @@ end
67
67
  begin
68
68
  Legion::Settings.merge_settings('crypt', Legion::Crypt::Settings.default) if Legion.const_defined?('Settings')
69
69
  rescue StandardError => e
70
- if Legion.const_defined?('Logging') && Legion::Logging.method_defined?(:fatal)
71
- Legion::Logging.fatal(e.message)
72
- Legion::Logging.fatal(e.backtrace)
70
+ if Legion.const_defined?('Logging') && Legion::Logging.respond_to?(:log_exception)
71
+ Legion::Logging.log_exception(e, lex: 'crypt', component_type: :helper, level: :fatal)
72
+ elsif Legion.const_defined?('Logging') && Legion::Logging.respond_to?(:fatal)
73
+ Legion::Logging.fatal("crypt settings merge error: #{e.class}: #{e.message}\n#{Array(e.backtrace).join("\n")}")
73
74
  else
74
75
  puts e.message
75
76
  puts e.backtrace
@@ -37,30 +37,47 @@ module Legion
37
37
  Legion::Logging.info "Vault connected at #{::Vault.address}" if defined?(Legion::Logging)
38
38
  end
39
39
  rescue StandardError => e
40
- Legion::Logging.error e.message
40
+ if defined?(Legion::Logging) && Legion::Logging.respond_to?(:log_exception)
41
+ Legion::Logging.log_exception(e, lex: 'crypt', component_type: :helper)
42
+ elsif defined?(Legion::Logging) && Legion::Logging.respond_to?(:error)
43
+ Legion::Logging.error "Vault connection failed: #{e.class}=#{e.message}\n#{Array(e.backtrace).first(10).join("\n")}"
44
+ else
45
+ warn "Vault connection failed: #{e.class}=#{e.message}"
46
+ end
41
47
  Legion::Settings[:crypt][:vault][:connected] = false
42
48
  false
43
49
  end
44
50
 
45
51
  def read(path, type = 'legion')
46
52
  full_path = type.nil? || type.empty? ? "#{type}/#{path}" : path
47
- Legion::Logging.debug "Vault read: #{full_path}" if defined?(Legion::Logging)
53
+ log_read_context(full_path)
48
54
  lease = logical_client.read(full_path)
49
- add_session(path: lease.lease_id) if lease.respond_to? :lease_id
50
- lease.data
55
+ if lease.nil?
56
+ log_vault_debug("Vault read: #{full_path} returned nil")
57
+ return nil
58
+ end
59
+ add_session(path: lease.lease_id) if lease.respond_to?(:lease_id) && lease.lease_id && !lease.lease_id.empty?
60
+
61
+ data = lease.data
62
+ log_vault_debug("Vault read: #{full_path} returned keys=#{data&.keys&.inspect}")
63
+ unwrap_kv_v2(data, full_path)
51
64
  rescue StandardError => e
52
- Legion::Logging.warn "Vault read failed at #{full_path}: #{e.message}" if defined?(Legion::Logging)
65
+ Legion::Logging.warn "Vault read failed at #{full_path}: #{e.class}=#{e.message}" if defined?(Legion::Logging)
53
66
  raise
54
67
  end
55
68
 
56
69
  def get(path)
57
- Legion::Logging.debug "Vault kv get: #{path}" if defined?(Legion::Logging)
70
+ Legion::Logging.debug "Vault kv get: path=#{path}" if defined?(Legion::Logging)
58
71
  result = kv_client.read(path)
59
- return nil if result.nil?
72
+ if result.nil?
73
+ Legion::Logging.debug "Vault kv get: #{path} returned nil" if defined?(Legion::Logging)
74
+ return nil
75
+ end
60
76
 
77
+ Legion::Logging.debug "Vault kv get: #{path} returned keys=#{result.data&.keys&.inspect}" if defined?(Legion::Logging)
61
78
  result.data
62
79
  rescue StandardError => e
63
- Legion::Logging.warn "Vault kv get failed at #{path}: #{e.message}" if defined?(Legion::Logging)
80
+ Legion::Logging.warn "Vault kv get failed at #{path}: #{e.class}=#{e.message}" if defined?(Legion::Logging)
64
81
  raise
65
82
  end
66
83
 
@@ -159,6 +176,29 @@ module Legion
159
176
  ::Vault.logical
160
177
  end
161
178
  end
179
+
180
+ def log_read_context(full_path)
181
+ return unless defined?(Legion::Logging)
182
+
183
+ namespace = if respond_to?(:connected_clusters) && connected_clusters.any?
184
+ client = vault_client
185
+ client.respond_to?(:namespace) ? client.namespace : 'n/a'
186
+ else
187
+ 'n/a (global client)'
188
+ end
189
+ Legion::Logging.debug "Vault read: path=#{full_path}, namespace=#{namespace}"
190
+ end
191
+
192
+ def unwrap_kv_v2(data, full_path)
193
+ return data unless data.is_a?(Hash) && data.key?(:data) && data[:data].is_a?(Hash) && data.key?(:metadata)
194
+
195
+ log_vault_debug("Vault read: #{full_path} detected KV v2 envelope, unwrapping :data key")
196
+ data[:data]
197
+ end
198
+
199
+ def log_vault_debug(message)
200
+ Legion::Logging.debug(message) if defined?(Legion::Logging)
201
+ end
162
202
  end
163
203
  end
164
204
  end
@@ -40,8 +40,10 @@ module Legion
40
40
  end
41
41
 
42
42
  def connect_all_clusters
43
+ log_vault_debug("connect_all_clusters: #{clusters.size} cluster(s) configured")
43
44
  results = {}
44
45
  clusters.each do |name, config|
46
+ log_vault_debug("connect_all_clusters: #{name} (auth_method=#{config[:auth_method].inspect})")
45
47
  case config[:auth_method]&.to_s
46
48
  when 'kerberos'
47
49
  results[name] = connect_kerberos_cluster(name, config)
@@ -61,7 +63,9 @@ module Legion
61
63
  log_vault_error(name, e)
62
64
  end
63
65
 
64
- mark_vault_connected if results.any? { |_, v| v }
66
+ connected = results.select { |_, v| v }
67
+ log_vault_debug("connect_all_clusters: #{connected.size}/#{results.size} connected")
68
+ mark_vault_connected if connected.any?
65
69
  results
66
70
  end
67
71
 
@@ -82,8 +86,10 @@ module Legion
82
86
  def build_vault_client(config)
83
87
  return nil unless config.is_a?(Hash)
84
88
 
89
+ addr = "#{config[:protocol]}://#{config[:address]}:#{config[:port]}"
90
+ log_vault_debug("build_vault_client: address=#{addr}")
85
91
  client = ::Vault::Client.new(
86
- address: "#{config[:protocol]}://#{config[:address]}:#{config[:port]}",
92
+ address: addr,
87
93
  token: config[:token]
88
94
  )
89
95
  namespace =
@@ -94,6 +100,7 @@ module Legion
94
100
  crypt_settings.respond_to?(:dig) ? crypt_settings.dig(:vault, :vault_namespace) : nil
95
101
  end
96
102
  client.namespace = namespace if namespace
103
+ log_vault_debug("build_vault_client: namespace=#{namespace.inspect}")
97
104
  client
98
105
  end
99
106
 
@@ -108,6 +115,9 @@ module Legion
108
115
  def connect_kerberos_cluster(name, config)
109
116
  krb_config = config[:kerberos] || {}
110
117
  spn = krb_config[:service_principal]
118
+ auth_path = krb_config[:auth_path] || Legion::Crypt::KerberosAuth::DEFAULT_AUTH_PATH
119
+
120
+ log_vault_debug("connect_kerberos_cluster[#{name}]: SPN=#{spn}, auth_path=#{auth_path}, namespace=#{config[:namespace].inspect}")
111
121
 
112
122
  unless spn
113
123
  log_vault_warn(name, 'Kerberos auth missing service_principal, skipping')
@@ -116,10 +126,13 @@ module Legion
116
126
  end
117
127
 
118
128
  require 'legion/crypt/kerberos_auth'
129
+ client = vault_client(name)
130
+ log_vault_debug("connect_kerberos_cluster[#{name}]: client.namespace=#{client.respond_to?(:namespace) ? client.namespace.inspect : 'n/a'}")
131
+
119
132
  result = Legion::Crypt::KerberosAuth.login(
120
- vault_client: vault_client(name),
133
+ vault_client: client,
121
134
  service_principal: spn,
122
- auth_path: krb_config[:auth_path] || Legion::Crypt::KerberosAuth::DEFAULT_AUTH_PATH
135
+ auth_path: auth_path
123
136
  )
124
137
 
125
138
  config[:token] = result[:token]
@@ -127,6 +140,7 @@ module Legion
127
140
  config[:renewable] = result[:renewable]
128
141
  config[:connected] = true
129
142
  vault_client(name).token = result[:token]
143
+ log_vault_debug("connect_kerberos_cluster[#{name}]: policies=#{result[:policies].inspect}")
130
144
  log_cluster_connected(name, config)
131
145
  true
132
146
  rescue Legion::Crypt::KerberosAuth::GemMissingError => e
@@ -150,6 +164,10 @@ module Legion
150
164
  warn("Vault cluster #{name}: #{message}")
151
165
  end
152
166
  end
167
+
168
+ def log_vault_debug(message)
169
+ Legion::Logging.debug(message) if defined?(Legion::Logging)
170
+ end
153
171
  end
154
172
  end
155
173
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.4.19'
5
+ VERSION = '1.4.22'
6
6
  end
7
7
  end
data/lib/legion/crypt.rb CHANGED
@@ -123,7 +123,13 @@ module Legion
123
123
  lease_manager = Legion::Crypt::LeaseManager.instance
124
124
  lease_manager.start(leases, vault_client: client)
125
125
  lease_manager.start_renewal_thread
126
- Legion::Logging.info "LeaseManager: #{leases.size} lease(s) initialized"
126
+ fetched = lease_manager.fetched_count
127
+ defined = leases.size
128
+ if fetched == defined
129
+ Legion::Logging.info "LeaseManager: #{fetched} lease(s) initialized"
130
+ else
131
+ Legion::Logging.warn "LeaseManager: #{fetched}/#{defined} lease(s) initialized (#{defined - fetched} failed)"
132
+ end
127
133
  rescue StandardError => e
128
134
  Legion::Logging.warn "LeaseManager startup failed: #{e.message}"
129
135
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-crypt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.19
4
+ version: 1.4.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity