legion-crypt 1.4.28 → 1.5.0

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.
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/logging/helper'
3
4
  require 'uri'
4
5
  require 'vault'
5
6
 
6
7
  module Legion
7
8
  module Crypt
8
9
  module Vault
10
+ include Legion::Logging::Helper
11
+
9
12
  attr_accessor :sessions
10
13
 
11
14
  def settings
@@ -16,16 +19,17 @@ module Legion
16
19
  @sessions = []
17
20
  vault_settings = Legion::Settings[:crypt][:vault]
18
21
  ::Vault.address = resolve_vault_address(vault_settings)
22
+ namespace = vault_settings[:vault_namespace]
23
+ log.info "Vault connection requested address=#{::Vault.address} namespace=#{namespace || 'none'}"
19
24
 
20
25
  Legion::Settings[:crypt][:vault][:token] = ENV['VAULT_DEV_ROOT_TOKEN_ID'] if ENV.key? 'VAULT_DEV_ROOT_TOKEN_ID'
21
26
  return nil if Legion::Settings[:crypt][:vault][:token].nil?
22
27
 
23
28
  ::Vault.token = Legion::Settings[:crypt][:vault][:token]
24
- namespace = vault_settings[:vault_namespace]
25
29
  ::Vault.namespace = namespace if namespace
26
30
  if vault_healthy?
27
31
  Legion::Settings[:crypt][:vault][:connected] = true
28
- Legion::Logging.info "Vault connected at #{::Vault.address} (namespace=#{namespace || 'none'})" if defined?(Legion::Logging)
32
+ log.info "Vault connected at #{::Vault.address} (namespace=#{namespace || 'none'})"
29
33
  end
30
34
  rescue StandardError => e
31
35
  log_vault_connection_error(e)
@@ -43,10 +47,10 @@ module Legion
43
47
  raise
44
48
  end
45
49
 
46
- def read(path, type = 'legion')
47
- full_path = type.nil? || type.empty? ? "#{type}/#{path}" : path
48
- log_read_context(full_path)
49
- lease = logical_client.read(full_path)
50
+ def read(path, type = 'legion', cluster_name: nil)
51
+ full_path = type.nil? || type.empty? ? path : "#{type}/#{path}"
52
+ log_read_context(full_path, cluster_name: cluster_name)
53
+ lease = logical_client(cluster_name: cluster_name).read(full_path)
50
54
  if lease.nil?
51
55
  log_vault_debug("Vault read: #{full_path} returned nil")
52
56
  return nil
@@ -57,43 +61,45 @@ module Legion
57
61
  log_vault_debug("Vault read: #{full_path} returned keys=#{data&.keys&.inspect}")
58
62
  unwrap_kv_v2(data, full_path)
59
63
  rescue StandardError => e
60
- Legion::Logging.warn "Vault read failed at #{full_path}: #{e.class}=#{e.message}" if defined?(Legion::Logging)
64
+ handle_exception(e, level: :error, operation: 'crypt.vault.read', path: full_path)
61
65
  raise
62
66
  end
63
67
 
64
- def get(path)
65
- Legion::Logging.debug "Vault kv get: path=#{path}" if defined?(Legion::Logging)
66
- result = kv_client.read(path)
68
+ def get(path, cluster_name: nil)
69
+ log.debug "Vault kv get: path=#{path}"
70
+ result = kv_client(cluster_name: cluster_name).read(path)
67
71
  if result.nil?
68
- Legion::Logging.debug "Vault kv get: #{path} returned nil" if defined?(Legion::Logging)
72
+ log.debug "Vault kv get: #{path} returned nil"
69
73
  return nil
70
74
  end
71
75
 
72
- Legion::Logging.debug "Vault kv get: #{path} returned keys=#{result.data&.keys&.inspect}" if defined?(Legion::Logging)
76
+ log.debug "Vault kv get: #{path} returned keys=#{result.data&.keys&.inspect}"
73
77
  result.data
74
78
  rescue StandardError => e
75
- Legion::Logging.warn "Vault kv get failed at #{path}: #{e.class}=#{e.message}" if defined?(Legion::Logging)
79
+ handle_exception(e, level: :error, operation: 'crypt.vault.get', path: path)
76
80
  raise
77
81
  end
78
82
 
79
- def write(path, **hash)
80
- Legion::Logging.debug "Vault kv write: #{path}" if defined?(Legion::Logging)
81
- kv_client.write(path, **hash)
83
+ def write(path, cluster_name: nil, **hash)
84
+ log.info "Vault kv write requested path=#{path}"
85
+ kv_client(cluster_name: cluster_name).write(path, **hash)
86
+ log.info "Vault kv write complete path=#{path}"
82
87
  rescue StandardError => e
83
- Legion::Logging.warn "Vault kv write failed at #{path}: #{e.message}" if defined?(Legion::Logging)
88
+ handle_exception(e, level: :error, operation: 'crypt.vault.write', path: path)
84
89
  raise
85
90
  end
86
91
 
87
- def delete(path)
88
- logical_client.delete(path)
92
+ def delete(path, cluster_name: nil)
93
+ logical_client(cluster_name: cluster_name).delete(path)
94
+ log.info "Vault delete complete path=#{path}"
89
95
  { success: true, path: path }
90
96
  rescue StandardError => e
91
- Legion::Logging.warn "Vault delete failed for #{path}: #{e.message}" if defined?(Legion::Logging)
97
+ handle_exception(e, level: :error, operation: 'crypt.vault.delete', path: path)
92
98
  { success: false, path: path, error: e.message }
93
99
  end
94
100
 
95
- def exist?(path)
96
- !kv_client.read_metadata(path).nil?
101
+ def exist?(path, cluster_name: nil)
102
+ !kv_client(cluster_name: cluster_name).read_metadata(path).nil?
97
103
  end
98
104
 
99
105
  def add_session(path:)
@@ -104,7 +110,7 @@ module Legion
104
110
  def close_sessions
105
111
  return if @sessions.nil?
106
112
 
107
- Legion::Logging.info 'Closing all Legion::Crypt vault sessions'
113
+ log.info 'Closing all Legion::Crypt vault sessions'
108
114
 
109
115
  @sessions.each do |session|
110
116
  close_session(session: session)
@@ -115,7 +121,7 @@ module Legion
115
121
  return unless Legion::Settings[:crypt][:vault][:connected]
116
122
  return if @renewer.nil?
117
123
 
118
- Legion::Logging.debug 'Shutting down Legion::Crypt::Vault::Renewer'
124
+ log.info 'Shutting down Legion::Crypt::Vault::Renewer'
119
125
  @renewer.cancel
120
126
  end
121
127
 
@@ -128,7 +134,7 @@ module Legion
128
134
  end
129
135
 
130
136
  def renew_sessions(**_opts)
131
- Legion::Logging.debug 'Vault renewal cycle start' if defined?(Legion::Logging)
137
+ log.debug 'Vault renewal cycle start'
132
138
  result = if respond_to?(:connected_clusters) && connected_clusters.any?
133
139
  renew_cluster_tokens
134
140
  else
@@ -136,15 +142,18 @@ module Legion
136
142
  renew_session(session: session)
137
143
  end
138
144
  end
139
- Legion::Logging.debug 'Vault renewal cycle complete' if defined?(Legion::Logging)
145
+ log.debug 'Vault renewal cycle complete'
140
146
  result
147
+ rescue StandardError => e
148
+ handle_exception(e, level: :error, operation: 'crypt.vault.renew_sessions')
149
+ raise
141
150
  end
142
151
 
143
152
  def renew_cluster_tokens
144
153
  connected_clusters.each_key do |name|
145
154
  client = vault_client(name)
146
155
  client.auth_token.renew_self
147
- Legion::Logging.info "Vault token renewed for cluster #{name}" if defined?(Legion::Logging)
156
+ log.info "Vault token renewed for cluster #{name}"
148
157
  rescue StandardError => e
149
158
  log_vault_error(name, e)
150
159
  end
@@ -156,32 +165,41 @@ module Legion
156
165
 
157
166
  private
158
167
 
159
- def kv_client
168
+ def kv_client(cluster_name: nil)
160
169
  if respond_to?(:connected_clusters) && connected_clusters.any?
161
- vault_client.kv(settings[:vault][:kv_path])
170
+ connected_vault_client(cluster_name).kv(settings[:kv_path])
162
171
  else
163
- ::Vault.kv(settings[:vault][:kv_path])
172
+ raise ArgumentError, "Vault cluster not connected: #{cluster_name}" if cluster_name
173
+
174
+ ::Vault.kv(settings[:kv_path])
164
175
  end
165
176
  end
166
177
 
167
- def logical_client
178
+ def logical_client(cluster_name: nil)
168
179
  if respond_to?(:connected_clusters) && connected_clusters.any?
169
- vault_client.logical
180
+ connected_vault_client(cluster_name).logical
170
181
  else
182
+ raise ArgumentError, "Vault cluster not connected: #{cluster_name}" if cluster_name
183
+
171
184
  ::Vault.logical
172
185
  end
173
186
  end
174
187
 
175
- def log_read_context(full_path)
176
- return unless defined?(Legion::Logging)
177
-
188
+ def log_read_context(full_path, cluster_name: nil)
178
189
  namespace = if respond_to?(:connected_clusters) && connected_clusters.any?
179
- client = vault_client
190
+ client = connected_vault_client(cluster_name)
180
191
  client.respond_to?(:namespace) ? client.namespace : 'n/a'
181
192
  else
182
193
  'n/a (global client)'
183
194
  end
184
- Legion::Logging.debug "Vault read: path=#{full_path}, namespace=#{namespace}"
195
+ log.debug "Vault read: path=#{full_path}, namespace=#{namespace}"
196
+ end
197
+
198
+ def connected_vault_client(cluster_name = nil)
199
+ selected_cluster = selected_connected_cluster_name(cluster_name)
200
+ raise ArgumentError, "Vault cluster not connected: #{cluster_name}" if selected_cluster.nil? && cluster_name
201
+
202
+ selected_cluster ? vault_client(selected_cluster) : nil
185
203
  end
186
204
 
187
205
  def unwrap_kv_v2(data, full_path)
@@ -207,17 +225,11 @@ module Legion
207
225
  end
208
226
 
209
227
  def log_vault_connection_error(error)
210
- if defined?(Legion::Logging) && Legion::Logging.respond_to?(:log_exception)
211
- Legion::Logging.log_exception(error, lex: 'crypt', component_type: :helper)
212
- elsif defined?(Legion::Logging) && Legion::Logging.respond_to?(:error)
213
- Legion::Logging.error "Vault connection failed: #{error.class}=#{error.message}\n#{Array(error.backtrace).first(10).join("\n")}"
214
- else
215
- warn "Vault connection failed: #{error.class}=#{error.message}"
216
- end
228
+ handle_exception(error, level: :error, operation: 'crypt.vault.connect_vault', address: ::Vault.address)
217
229
  end
218
230
 
219
231
  def log_vault_debug(message)
220
- Legion::Logging.debug(message) if defined?(Legion::Logging)
232
+ log.debug(message)
221
233
  end
222
234
  end
223
235
  end
@@ -3,6 +3,7 @@
3
3
  # Ruby 4.0 freezes OpenSSL::SSL::SSLContext::DEFAULT_PARAMS by default.
4
4
  # The vault gem (0.18.x) mutates this hash in Vault.setup! — replace it
5
5
  # with a mutable dup so the require succeeds on Ruby 4.0+.
6
+ require 'legion/logging/helper'
6
7
  require 'openssl'
7
8
  if OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.frozen?
8
9
  unfrozen = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.dup
@@ -15,6 +16,8 @@ require 'vault'
15
16
  module Legion
16
17
  module Crypt
17
18
  module VaultCluster
19
+ include Legion::Logging::Helper
20
+
18
21
  def vault_client(name = nil)
19
22
  name = resolve_cluster_name(name)
20
23
  @vault_clients ||= {}
@@ -40,6 +43,7 @@ module Legion
40
43
  end
41
44
 
42
45
  def connect_all_clusters
46
+ log.info "Vault cluster connect requested configured_clusters=#{clusters.size}"
43
47
  log_vault_debug("connect_all_clusters: #{clusters.size} cluster(s) configured")
44
48
  results = {}
45
49
  clusters.each do |name, config|
@@ -60,12 +64,13 @@ module Legion
60
64
  rescue StandardError => e
61
65
  config[:connected] = false
62
66
  results[name] = false
63
- log_vault_error(name, e)
67
+ log_vault_error(name, e, operation: 'crypt.vault_cluster.connect_all_clusters')
64
68
  end
65
69
 
66
70
  connected = results.select { |_, v| v }
71
+ log.info "Vault cluster connect complete connected=#{connected.size} attempted=#{results.size}"
67
72
  log_vault_debug("connect_all_clusters: #{connected.size}/#{results.size} connected")
68
- mark_vault_connected if connected.any?
73
+ sync_vault_connected(connected.any?)
69
74
  results
70
75
  end
71
76
 
@@ -79,10 +84,27 @@ module Legion
79
84
  raise
80
85
  end
81
86
 
82
- def mark_vault_connected
87
+ def sync_vault_connected(connected)
83
88
  return unless defined?(Legion::Settings)
84
89
 
85
- Legion::Settings[:crypt][:vault][:connected] = true
90
+ Legion::Settings[:crypt][:vault][:connected] = connected
91
+ end
92
+
93
+ def selected_connected_cluster_name(name = nil)
94
+ active_clusters = connected_clusters
95
+ return nil if active_clusters.empty?
96
+
97
+ if name
98
+ cluster_name = name.to_sym
99
+ raise ArgumentError, "Vault cluster not connected: #{cluster_name}" unless active_clusters.key?(cluster_name)
100
+
101
+ return cluster_name
102
+ end
103
+
104
+ default_name = vault_settings[:default]&.to_sym
105
+ return default_name if default_name && active_clusters.key?(default_name)
106
+
107
+ active_clusters.keys.first
86
108
  end
87
109
 
88
110
  def resolve_cluster_name(name)
@@ -95,6 +117,7 @@ module Legion
95
117
  return nil unless config.is_a?(Hash)
96
118
 
97
119
  addr = "#{config[:protocol]}://#{config[:address]}:#{config[:port]}"
120
+ log.info "Building Vault client address=#{addr} namespace=#{config[:namespace].inspect}"
98
121
  log_vault_debug("build_vault_client: address=#{addr}")
99
122
  client = ::Vault::Client.new(
100
123
  address: addr,
@@ -112,12 +135,8 @@ module Legion
112
135
  client
113
136
  end
114
137
 
115
- def log_vault_error(name, error)
116
- if defined?(Legion::Logging)
117
- Legion::Logging.error("Vault cluster #{name}: #{error.message}")
118
- else
119
- warn("Vault cluster #{name}: #{error.message}")
120
- end
138
+ def log_vault_error(name, error, operation: 'crypt.vault_cluster.error')
139
+ handle_exception(error, level: :error, operation: operation, cluster_name: name)
121
140
  end
122
141
 
123
142
  def connect_kerberos_cluster(name, config)
@@ -136,6 +155,7 @@ module Legion
136
155
  require 'legion/crypt/kerberos_auth'
137
156
  client = vault_client(name)
138
157
  log_vault_debug("connect_kerberos_cluster[#{name}]: client.namespace=#{client.respond_to?(:namespace) ? client.namespace.inspect : 'n/a'}")
158
+ log.info "Connecting Vault cluster #{name} via Kerberos auth_path=#{auth_path}"
139
159
 
140
160
  result = Legion::Crypt::KerberosAuth.login(
141
161
  vault_client: client,
@@ -152,29 +172,27 @@ module Legion
152
172
  log_cluster_connected(name, config)
153
173
  true
154
174
  rescue Legion::Crypt::KerberosAuth::GemMissingError => e
175
+ handle_exception(e, level: :warn, operation: 'crypt.vault_cluster.connect_kerberos_cluster', cluster_name: name)
155
176
  log_vault_warn(name, e.message)
156
177
  config[:connected] = false
157
178
  false
158
179
  rescue Legion::Crypt::KerberosAuth::AuthError => e
180
+ handle_exception(e, level: :warn, operation: 'crypt.vault_cluster.connect_kerberos_cluster', cluster_name: name)
159
181
  log_vault_warn(name, "Kerberos auth failed: #{e.message}")
160
182
  config[:connected] = false
161
183
  false
162
184
  end
163
185
 
164
186
  def log_cluster_connected(name, config)
165
- Legion::Logging.info "Vault cluster connected: #{name} at #{config[:address]}" if defined?(Legion::Logging)
187
+ log.info "Vault cluster connected: #{name} at #{config[:address]}"
166
188
  end
167
189
 
168
190
  def log_vault_warn(name, message)
169
- if defined?(Legion::Logging)
170
- Legion::Logging.warn("Vault cluster #{name}: #{message}")
171
- else
172
- warn("Vault cluster #{name}: #{message}")
173
- end
191
+ log.warn("Vault cluster #{name}: #{message}")
174
192
  end
175
193
 
176
194
  def log_vault_debug(message)
177
- Legion::Logging.debug(message) if defined?(Legion::Logging)
195
+ log.debug(message)
178
196
  end
179
197
  end
180
198
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/logging/helper'
4
+
3
5
  module Legion
4
6
  module Crypt
5
7
  # Vault JWT auth backend integration.
@@ -18,6 +20,8 @@ module Legion
18
20
 
19
21
  class AuthError < StandardError; end
20
22
 
23
+ extend Legion::Logging::Helper
24
+
21
25
  # Authenticate to Vault using a JWT token.
22
26
  # Returns a Vault token string on success.
23
27
  #
@@ -28,6 +32,7 @@ module Legion
28
32
  def self.login(jwt:, role: DEFAULT_ROLE, auth_path: DEFAULT_AUTH_PATH)
29
33
  raise AuthError, 'Vault is not connected' unless vault_connected?
30
34
 
35
+ log.info "[crypt:vault_jwt] authenticating role=#{role} auth_path=#{auth_path}"
31
36
  response = ::Vault.logical.write(
32
37
  auth_path,
33
38
  role: role,
@@ -44,11 +49,18 @@ module Legion
44
49
  metadata: response.auth.metadata
45
50
  }
46
51
  rescue ::Vault::HTTPClientError => e
47
- Legion::Logging.warn "Vault JWT auth failed (client error): role=#{role}, #{e.message}" if defined?(Legion::Logging)
52
+ handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
53
+ category: 'client_error')
48
54
  raise AuthError, "Vault JWT auth failed: #{e.message}"
49
55
  rescue ::Vault::HTTPServerError => e
50
- Legion::Logging.warn "Vault JWT auth failed (server error): role=#{role}, #{e.message}" if defined?(Legion::Logging)
56
+ handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
57
+ category: 'server_error')
51
58
  raise AuthError, "Vault server error during JWT auth: #{e.message}"
59
+ rescue StandardError => e
60
+ handle_exception(e, level: :error, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path)
61
+ raise if e.is_a?(AuthError)
62
+
63
+ raise AuthError, "Vault JWT auth failed: #{e.message}"
52
64
  end
53
65
 
54
66
  # Authenticate and set the Vault client token for subsequent operations.
@@ -58,7 +70,7 @@ module Legion
58
70
  def self.login!(jwt:, role: DEFAULT_ROLE, auth_path: DEFAULT_AUTH_PATH)
59
71
  result = login(jwt: jwt, role: role, auth_path: auth_path)
60
72
  ::Vault.token = result[:token]
61
- Legion::Logging.info "[crypt:vault_jwt] authenticated via JWT auth, policies=#{result[:policies].join(',')}"
73
+ log.info "[crypt:vault_jwt] authenticated via JWT auth, policies=#{result[:policies].join(',')}"
62
74
  result
63
75
  end
64
76
 
@@ -70,6 +82,7 @@ module Legion
70
82
  # @param role [String] Vault JWT auth role name
71
83
  # @return [Hash] Same as login
72
84
  def self.worker_login(worker_id:, owner_msid:, role: DEFAULT_ROLE)
85
+ log.info "[crypt:vault_jwt] worker login requested role=#{role} worker_id=#{worker_id}"
73
86
  jwt = Legion::Crypt::JWT.issue(
74
87
  { worker_id: worker_id, sub: owner_msid, scope: 'vault', aud: 'legion' },
75
88
  signing_key: Legion::Crypt.cluster_secret,
@@ -85,7 +98,7 @@ module Legion
85
98
  defined?(Legion::Settings) &&
86
99
  Legion::Settings[:crypt][:vault][:connected] == true
87
100
  rescue StandardError => e
88
- Legion::Logging.debug("Legion::Crypt::VaultJwtAuth#vault_connected? failed: #{e.message}") if defined?(Legion::Logging)
101
+ handle_exception(e, level: :debug, operation: 'crypt.vault_jwt_auth.vault_connected?')
89
102
  false
90
103
  end
91
104
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/logging/helper'
4
+
3
5
  module Legion
4
6
  module Crypt
5
7
  module VaultKerberosAuth
@@ -7,9 +9,12 @@ module Legion
7
9
 
8
10
  class AuthError < StandardError; end
9
11
 
12
+ extend Legion::Logging::Helper
13
+
10
14
  def self.login(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
11
15
  raise AuthError, 'Vault is not connected' unless vault_connected?
12
16
 
17
+ log.info "[crypt:vault_kerberos] login requested auth_path=#{auth_path}"
13
18
  response = ::Vault.logical.write(auth_path, authorization: "Negotiate #{spnego_token}")
14
19
  raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
15
20
 
@@ -21,12 +26,17 @@ module Legion
21
26
  metadata: response.auth.metadata
22
27
  }
23
28
  rescue ::Vault::HTTPClientError => e
29
+ handle_exception(e, level: :warn, operation: 'crypt.vault_kerberos_auth.login', auth_path: auth_path)
24
30
  raise AuthError, "Vault Kerberos auth failed: #{e.message}"
31
+ rescue StandardError => e
32
+ handle_exception(e, level: :error, operation: 'crypt.vault_kerberos_auth.login', auth_path: auth_path)
33
+ raise
25
34
  end
26
35
 
27
36
  def self.login!(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
28
37
  result = login(spnego_token: spnego_token, auth_path: auth_path)
29
38
  ::Vault.token = result[:token]
39
+ log.info "[crypt:vault_kerberos] authenticated via Kerberos auth, policies=#{result[:policies].join(',')}"
30
40
  result
31
41
  end
32
42
 
@@ -34,7 +44,7 @@ module Legion
34
44
  defined?(::Vault) && defined?(Legion::Settings) &&
35
45
  Legion::Settings[:crypt][:vault][:connected] == true
36
46
  rescue StandardError => e
37
- Legion::Logging.debug("Legion::Crypt::VaultKerberosAuth#vault_connected? failed: #{e.message}") if defined?(Legion::Logging)
47
+ handle_exception(e, level: :debug, operation: 'crypt.vault_kerberos_auth.vault_connected')
38
48
  false
39
49
  end
40
50
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.4.28'
5
+ VERSION = '1.5.0'
6
6
  end
7
7
  end