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,58 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'ed25519'
4
+ require 'legion/logging/helper'
4
5
 
5
6
  module Legion
6
7
  module Crypt
7
8
  module Ed25519
9
+ extend Legion::Logging::Helper
10
+
8
11
  class << self
9
12
  def generate_keypair
10
13
  signing_key = ::Ed25519::SigningKey.generate
11
- Legion::Logging.debug 'Ed25519 keypair generated' if defined?(Legion::Logging)
14
+ log.info 'Ed25519 keypair generated'
12
15
  {
13
16
  private_key: signing_key.to_bytes,
14
17
  public_key: signing_key.verify_key.to_bytes,
15
18
  public_key_hex: signing_key.verify_key.to_bytes.unpack1('H*')
16
19
  }
20
+ rescue StandardError => e
21
+ handle_exception(e, level: :error, operation: 'crypt.ed25519.generate_keypair')
22
+ raise
17
23
  end
18
24
 
19
25
  def sign(message, private_key_bytes)
20
26
  signing_key = ::Ed25519::SigningKey.new(private_key_bytes)
21
27
  result = signing_key.sign(message)
22
- Legion::Logging.debug 'Ed25519 sign complete' if defined?(Legion::Logging)
28
+ log.debug 'Ed25519 sign complete'
23
29
  result
30
+ rescue StandardError => e
31
+ handle_exception(e, level: :error, operation: 'crypt.ed25519.sign')
32
+ raise
24
33
  end
25
34
 
26
35
  def verify(message, signature, public_key_bytes)
27
36
  verify_key = ::Ed25519::VerifyKey.new(public_key_bytes)
28
37
  verify_key.verify(signature, message)
29
- Legion::Logging.debug 'Ed25519 verify success' if defined?(Legion::Logging)
38
+ log.debug 'Ed25519 verify success'
30
39
  true
31
40
  rescue ::Ed25519::VerifyError => e
32
- Legion::Logging.debug("Legion::Crypt::Ed25519.verify signature mismatch: #{e.message}") if defined?(Legion::Logging)
41
+ handle_exception(e, level: :debug, operation: 'crypt.ed25519.verify.signature_mismatch')
42
+ log.warn 'Ed25519 signature verification failed'
33
43
  false
44
+ rescue StandardError => e
45
+ handle_exception(e, level: :error, operation: 'crypt.ed25519.verify')
46
+ raise
34
47
  end
35
48
 
36
49
  def store_keypair(agent_id:, keypair: nil)
37
50
  keypair ||= generate_keypair
38
- vault_path = "#{key_prefix}/#{agent_id}"
39
- if defined?(Legion::Crypt::Vault)
40
- Legion::Logging.debug "Ed25519 storing keypair at #{vault_path}" if defined?(Legion::Logging)
41
- Legion::Crypt::Vault.write(vault_path, {
42
- private_key: keypair[:private_key].unpack1('H*'),
43
- public_key: keypair[:public_key_hex]
44
- })
51
+ if Legion::Crypt.respond_to?(:write)
52
+ log.info "Ed25519 storing keypair for agent #{agent_id}"
53
+ Legion::Crypt.write(
54
+ vault_key_path(agent_id),
55
+ private_key: keypair[:private_key].unpack1('H*'),
56
+ public_key: keypair[:public_key_hex]
57
+ )
58
+ else
59
+ log.warn "Ed25519 keypair generated for agent #{agent_id} but Vault is unavailable"
45
60
  end
46
61
  keypair
62
+ rescue StandardError => e
63
+ handle_exception(e, level: :error, operation: 'crypt.ed25519.store_keypair', agent_id: agent_id)
64
+ raise
47
65
  end
48
66
 
49
67
  def load_private_key(agent_id:)
50
- vault_path = "#{key_prefix}/#{agent_id}"
51
- Legion::Logging.debug "Ed25519 loading private key from #{vault_path}" if defined?(Legion::Logging)
52
- data = Legion::Crypt::Vault.read(vault_path)
53
- [data[:private_key]].pack('H*') if data&.dig(:private_key)
68
+ log.debug "Ed25519 loading private key for agent #{agent_id}"
69
+ return nil unless Legion::Crypt.respond_to?(:get)
70
+
71
+ data = Legion::Crypt.get(vault_key_path(agent_id))
72
+ if data&.dig(:private_key)
73
+ log.info "Ed25519 private key loaded for agent #{agent_id}"
74
+ [data[:private_key]].pack('H*')
75
+ else
76
+ log.warn "Ed25519 private key missing for agent #{agent_id}"
77
+ nil
78
+ end
54
79
  rescue StandardError => e
55
- Legion::Logging.warn("Legion::Crypt::Ed25519#load_private_key failed: #{e.message}") if defined?(Legion::Logging)
80
+ handle_exception(e, level: :warn, operation: 'crypt.ed25519.load_private_key', agent_id: agent_id)
56
81
  nil
57
82
  end
58
83
 
@@ -62,9 +87,24 @@ module Legion
62
87
  begin
63
88
  Legion::Settings[:crypt][:ed25519][:vault_key_prefix]
64
89
  rescue StandardError => e
65
- Legion::Logging.debug("Legion::Crypt::Ed25519#key_prefix settings lookup failed: #{e.message}") if defined?(Legion::Logging)
90
+ handle_exception(e, level: :debug, operation: 'crypt.ed25519.key_prefix')
66
91
  nil
67
- end || 'secret/data/legion/keys'
92
+ end || 'keys'
93
+ end
94
+
95
+ def vault_key_path(agent_id)
96
+ normalize_kv_path("#{key_prefix}/#{agent_id}")
97
+ end
98
+
99
+ def normalize_kv_path(path)
100
+ kv_path = Legion::Settings.dig(:crypt, :vault, :kv_path)
101
+ return path if kv_path.nil? || kv_path.empty?
102
+
103
+ normalized = path.sub(%r{\Asecret/data/#{Regexp.escape(kv_path)}/}, '')
104
+ normalized.sub(%r{\A#{Regexp.escape(kv_path)}/}, '')
105
+ rescue StandardError => e
106
+ handle_exception(e, level: :debug, operation: 'crypt.ed25519.normalize_kv_path')
107
+ path
68
108
  end
69
109
  end
70
110
  end
@@ -1,29 +1,42 @@
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 Erasure
8
+ extend Legion::Logging::Helper
9
+
6
10
  class << self
7
11
  def erase_tenant(tenant_id:)
8
12
  key_path = "#{tenant_prefix}/#{tenant_id}/master_key"
9
13
 
10
- delete_vault_key(key_path) if defined?(Legion::Crypt::Vault)
14
+ log.info "[crypt] Erasing tenant #{tenant_id}"
15
+ if Legion::Crypt.respond_to?(:delete)
16
+ Legion::Crypt.delete(key_path)
17
+ elsif defined?(Legion::Crypt::Vault)
18
+ delete_vault_key(key_path)
19
+ end
11
20
  Legion::Events.emit('crypt.tenant_erased', { tenant_id: tenant_id, erased_at: Time.now.utc }) if defined?(Legion::Events)
12
- Legion::Logging.warn "[crypt] Tenant #{tenant_id} cryptographically erased" if defined?(Legion::Logging)
21
+ log.warn "[crypt] Tenant #{tenant_id} cryptographically erased"
13
22
 
14
23
  { erased: true, tenant_id: tenant_id, path: key_path }
15
24
  rescue StandardError => e
16
- Legion::Logging.error("Legion::Crypt::Erasure#erase_tenant failed: #{e.message}") if defined?(Legion::Logging)
25
+ handle_exception(e, level: :error, operation: 'crypt.erasure.erase_tenant', tenant_id: tenant_id)
17
26
  { erased: false, tenant_id: tenant_id, error: e.message }
18
27
  end
19
28
 
20
29
  def verify_erasure(tenant_id:)
21
30
  key_path = "#{tenant_prefix}/#{tenant_id}/master_key"
22
- data = Legion::Crypt::Vault.read(key_path)
23
- { erased: data.nil?, tenant_id: tenant_id }
31
+ raise 'Legion::Crypt.read is unavailable' unless Legion::Crypt.respond_to?(:read)
32
+
33
+ data = Legion::Crypt.read(key_path, nil)
34
+ erased = data.nil?
35
+ log.info "Tenant erasure verification completed for #{tenant_id}: erased=#{erased}"
36
+ { erased: erased, tenant_id: tenant_id }
24
37
  rescue StandardError => e
25
- Legion::Logging.warn("Legion::Crypt::Erasure#verify_erasure failed: #{e.message}") if defined?(Legion::Logging)
26
- { erased: true, tenant_id: tenant_id }
38
+ handle_exception(e, level: :warn, operation: 'crypt.erasure.verify_erasure', tenant_id: tenant_id)
39
+ { erased: false, tenant_id: tenant_id, error: e.message }
27
40
  end
28
41
 
29
42
  private
@@ -36,7 +49,7 @@ module Legion
36
49
  begin
37
50
  Legion::Settings[:crypt][:partition_keys][:vault_tenant_prefix]
38
51
  rescue StandardError => e
39
- Legion::Logging.debug("Legion::Crypt::Erasure#tenant_prefix settings lookup failed: #{e.message}") if defined?(Legion::Logging)
52
+ handle_exception(e, level: :debug, operation: 'crypt.erasure.tenant_prefix')
40
53
  nil
41
54
  end || 'secret/data/legion/tenants'
42
55
  end
@@ -5,6 +5,7 @@ require 'uri'
5
5
  require 'json'
6
6
  require 'openssl'
7
7
  require 'jwt'
8
+ require 'legion/logging/helper'
8
9
 
9
10
  module Legion
10
11
  module Crypt
@@ -12,33 +13,40 @@ module Legion
12
13
  CACHE_TTL = 3600
13
14
 
14
15
  @cache = {}
15
- @mutex = Mutex.new
16
+ @cache_mutex = Mutex.new
17
+ @locks = {}
18
+ @locks_mutex = Mutex.new
16
19
 
17
20
  class << self
21
+ include Legion::Logging::Helper
22
+
18
23
  def fetch_keys(jwks_url)
19
- @mutex.synchronize do
20
- Legion::Logging.debug "JWKS fetch: #{jwks_url}" if defined?(Legion::Logging)
24
+ with_url_lock(jwks_url) do
25
+ log.debug "JWKS fetch: #{jwks_url}"
21
26
  response = http_get(jwks_url)
22
27
  jwks_data = parse_response(response)
23
28
  keys = parse_jwks(jwks_data)
24
29
 
25
- @cache[jwks_url] = { keys: keys, fetched_at: Time.now }
30
+ cache_write(jwks_url, keys)
31
+ log.info "JWKS fetched url=#{jwks_url} keys=#{keys.size}"
26
32
  keys
27
33
  end
28
34
  rescue StandardError => e
29
- Legion::Logging.warn "JWKS fetch failed for #{jwks_url}: #{e.message}" if defined?(Legion::Logging)
35
+ handle_exception(e, level: :warn, operation: 'crypt.jwks.fetch_keys', jwks_url: jwks_url)
30
36
  raise
31
37
  end
32
38
 
33
39
  def find_key(jwks_url, kid)
34
- cached = @mutex.synchronize { @cache[jwks_url] }
40
+ cached = cache_read(jwks_url)
35
41
 
36
42
  if cached && !expired?(cached[:fetched_at])
37
43
  key = cached[:keys][kid]
38
44
  if key
39
- Legion::Logging.debug "JWKS cache hit: kid=#{kid}" if defined?(Legion::Logging)
45
+ log.debug "JWKS cache hit: kid=#{kid}"
40
46
  return key
41
47
  end
48
+
49
+ log.debug "JWKS cache miss for kid=#{kid}; refreshing keys"
42
50
  end
43
51
 
44
52
  keys = fetch_keys(jwks_url)
@@ -49,17 +57,31 @@ module Legion
49
57
  end
50
58
 
51
59
  def clear_cache
52
- @mutex.synchronize { @cache = {} }
60
+ @cache_mutex.synchronize { @cache = {} }
61
+ @locks_mutex.synchronize { @locks = {} }
62
+ log.info 'JWKS cache cleared'
53
63
  end
54
64
 
55
65
  private
56
66
 
67
+ def cache_read(jwks_url)
68
+ @cache_mutex.synchronize { @cache[jwks_url] }
69
+ end
70
+
71
+ def cache_write(jwks_url, keys)
72
+ @cache_mutex.synchronize do
73
+ @cache[jwks_url] = { keys: keys, fetched_at: Time.now }
74
+ end
75
+ end
76
+
57
77
  def expired?(fetched_at)
58
78
  Time.now - fetched_at > CACHE_TTL
59
79
  end
60
80
 
61
81
  def http_get(url)
62
82
  uri = URI.parse(url)
83
+ raise Legion::Crypt::JWT::Error, 'failed to fetch JWKS: HTTPS is required' unless uri.scheme == 'https'
84
+
63
85
  http = Net::HTTP.new(uri.host, uri.port)
64
86
  http.use_ssl = uri.scheme == 'https'
65
87
  http.open_timeout = 10
@@ -83,6 +105,7 @@ module Legion
83
105
 
84
106
  parsed
85
107
  rescue ::JSON::ParserError => e
108
+ handle_exception(e, level: :warn, operation: 'crypt.jwks.parse_response')
86
109
  raise Legion::Crypt::JWT::Error, "invalid JWKS response: #{e.message}"
87
110
  end
88
111
 
@@ -96,12 +119,17 @@ module Legion
96
119
  jwk = ::JWT::JWK.new(jwk_hash)
97
120
  keys[kid] = jwk.public_key
98
121
  rescue StandardError => e
99
- Legion::Logging.debug("Legion::Crypt::JwksClient#parse_jwks skipping malformed key kid=#{kid}: #{e.message}") if defined?(Legion::Logging)
122
+ handle_exception(e, level: :debug, operation: 'crypt.jwks.parse_jwks', kid: kid)
100
123
  next
101
124
  end
102
125
 
103
126
  keys
104
127
  end
128
+
129
+ def with_url_lock(jwks_url, &)
130
+ lock = @locks_mutex.synchronize { @locks[jwks_url] ||= Mutex.new }
131
+ lock.synchronize(&)
132
+ end
105
133
  end
106
134
  end
107
135
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'jwt'
4
4
  require 'securerandom'
5
+ require 'legion/logging/helper'
5
6
  require 'legion/crypt/jwks_client'
6
7
 
7
8
  module Legion
@@ -14,20 +15,25 @@ module Legion
14
15
 
15
16
  SUPPORTED_ALGORITHMS = %w[HS256 RS256].freeze
16
17
 
18
+ extend Legion::Logging::Helper
19
+
17
20
  def self.issue(payload, signing_key:, algorithm: 'HS256', ttl: 3600, issuer: 'legion')
18
21
  validate_algorithm!(algorithm)
19
22
 
20
23
  now = Time.now.to_i
21
- claims = {
24
+ claims = sanitize_payload(payload).merge(
22
25
  iss: issuer,
23
26
  iat: now,
24
27
  exp: now + ttl,
25
28
  jti: SecureRandom.uuid
26
- }.merge(payload)
29
+ )
27
30
 
28
31
  token = ::JWT.encode(claims, signing_key, algorithm)
29
- Legion::Logging.info "JWT issued: sub=#{claims[:sub]}, exp=#{Time.at(claims[:exp]).utc.iso8601}, alg=#{algorithm}" if defined?(Legion::Logging)
32
+ log.info "JWT issued: sub=#{claims[:sub]}, exp=#{Time.at(claims[:exp]).utc.iso8601}, alg=#{algorithm}"
30
33
  token
34
+ rescue StandardError => e
35
+ handle_exception(e, level: :error, operation: 'crypt.jwt.issue', algorithm: algorithm, issuer: issuer)
36
+ raise
31
37
  end
32
38
 
33
39
  def self.verify(token, verification_key:, **opts)
@@ -47,24 +53,34 @@ module Legion
47
53
 
48
54
  payload, _header = ::JWT.decode(token, verification_key, true, decode_opts)
49
55
  result = symbolize_keys(payload)
50
- Legion::Logging.debug "JWT verify success: sub=#{result[:sub]}, jti=#{result[:jti]}" if defined?(Legion::Logging)
56
+ log.debug "JWT verify success: sub=#{result[:sub]}, jti=#{result[:jti]}"
51
57
  result
52
- rescue ::JWT::ExpiredSignature
53
- Legion::Logging.warn 'JWT verify failed: token has expired' if defined?(Legion::Logging)
58
+ rescue ::JWT::ExpiredSignature => e
59
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.verify.expired', algorithm: algorithm)
60
+ log.warn 'JWT verify failed: token has expired'
54
61
  raise ExpiredTokenError, 'token has expired'
55
- rescue ::JWT::VerificationError, ::JWT::IncorrectAlgorithm
56
- Legion::Logging.warn 'JWT verify failed: signature verification failed' if defined?(Legion::Logging)
62
+ rescue ::JWT::VerificationError, ::JWT::IncorrectAlgorithm => e
63
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.verify.signature', algorithm: algorithm)
64
+ log.warn 'JWT verify failed: signature verification failed'
57
65
  raise InvalidTokenError, 'token signature verification failed'
58
66
  rescue ::JWT::DecodeError => e
59
- Legion::Logging.warn "JWT verify failed: #{e.message}" if defined?(Legion::Logging)
67
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.verify.decode', algorithm: algorithm)
68
+ log.warn "JWT verify failed: #{e.message}"
60
69
  raise DecodeError, "failed to decode token: #{e.message}"
70
+ rescue StandardError => e
71
+ handle_exception(e, level: :error, operation: 'crypt.jwt.verify', algorithm: algorithm)
72
+ raise
61
73
  end
62
74
 
63
75
  def self.decode(token)
64
76
  payload, _header = ::JWT.decode(token, nil, false)
65
77
  symbolize_keys(payload)
66
78
  rescue ::JWT::DecodeError => e
79
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.decode')
67
80
  raise DecodeError, "failed to decode token: #{e.message}"
81
+ rescue StandardError => e
82
+ handle_exception(e, level: :error, operation: 'crypt.jwt.decode')
83
+ raise
68
84
  end
69
85
 
70
86
  def self.verify_with_jwks(token, jwks_url:, **opts)
@@ -81,41 +97,44 @@ module Legion
81
97
  verify_expiration = opts.fetch(:verify_expiration, true)
82
98
  issuers = opts[:issuers]
83
99
  audience = opts[:audience]
100
+ validate_external_requirements!(issuers: issuers, audience: audience)
84
101
 
85
102
  decode_opts = {
86
103
  algorithm: algorithm,
87
- verify_expiration: verify_expiration
104
+ verify_expiration: verify_expiration,
105
+ verify_iss: true,
106
+ iss: issuers,
107
+ verify_aud: true,
108
+ aud: audience
88
109
  }
89
110
 
90
- if issuers
91
- decode_opts[:verify_iss] = true
92
- decode_opts[:iss] = issuers
93
- end
94
-
95
- if audience
96
- decode_opts[:verify_aud] = true
97
- decode_opts[:aud] = audience
98
- end
99
-
100
111
  payload, _header = ::JWT.decode(token, public_key, true, decode_opts)
101
112
  result = symbolize_keys(payload)
102
- Legion::Logging.debug "JWT JWKS verify success: sub=#{result[:sub]}, kid=#{kid}" if defined?(Legion::Logging)
113
+ log.info "JWT JWKS verify success: sub=#{result[:sub]}, kid=#{kid}"
103
114
  result
104
- rescue ::JWT::ExpiredSignature
105
- Legion::Logging.warn "JWT JWKS verify failed: token has expired, kid=#{kid}" if defined?(Legion::Logging)
115
+ rescue ::JWT::ExpiredSignature => e
116
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.verify_with_jwks.expired', jwks_url: jwks_url, kid: kid)
117
+ log.warn "JWT JWKS verify failed: token has expired, kid=#{kid}"
106
118
  raise ExpiredTokenError, 'token has expired'
107
- rescue ::JWT::VerificationError, ::JWT::IncorrectAlgorithm
108
- Legion::Logging.warn "JWT JWKS verify failed: signature verification failed, kid=#{kid}" if defined?(Legion::Logging)
119
+ rescue ::JWT::VerificationError, ::JWT::IncorrectAlgorithm => e
120
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.verify_with_jwks.signature', jwks_url: jwks_url, kid: kid)
121
+ log.warn "JWT JWKS verify failed: signature verification failed, kid=#{kid}"
109
122
  raise InvalidTokenError, 'token signature verification failed'
110
- rescue ::JWT::InvalidIssuerError
111
- Legion::Logging.warn "JWT JWKS verify failed: issuer not allowed, kid=#{kid}" if defined?(Legion::Logging)
123
+ rescue ::JWT::InvalidIssuerError => e
124
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.verify_with_jwks.issuer', jwks_url: jwks_url, kid: kid)
125
+ log.warn "JWT JWKS verify failed: issuer not allowed, kid=#{kid}"
112
126
  raise InvalidTokenError, 'token issuer not allowed'
113
- rescue ::JWT::InvalidAudError
114
- Legion::Logging.warn "JWT JWKS verify failed: audience mismatch, kid=#{kid}" if defined?(Legion::Logging)
127
+ rescue ::JWT::InvalidAudError => e
128
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.verify_with_jwks.audience', jwks_url: jwks_url, kid: kid)
129
+ log.warn "JWT JWKS verify failed: audience mismatch, kid=#{kid}"
115
130
  raise InvalidTokenError, 'token audience mismatch'
116
131
  rescue ::JWT::DecodeError => e
117
- Legion::Logging.warn "JWT JWKS verify failed: #{e.message}, kid=#{kid}" if defined?(Legion::Logging)
132
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.verify_with_jwks.decode', jwks_url: jwks_url, kid: kid)
133
+ log.warn "JWT JWKS verify failed: #{e.message}, kid=#{kid}"
118
134
  raise DecodeError, "failed to decode token: #{e.message}"
135
+ rescue StandardError => e
136
+ handle_exception(e, level: :error, operation: 'crypt.jwt.verify_with_jwks', jwks_url: jwks_url, kid: kid)
137
+ raise
119
138
  end
120
139
 
121
140
  def self.decode_header(token)
@@ -125,7 +144,11 @@ module Legion
125
144
  header_json = Base64.urlsafe_decode64(parts[0])
126
145
  ::JSON.parse(header_json)
127
146
  rescue ::JSON::ParserError, ArgumentError => e
147
+ handle_exception(e, level: :warn, operation: 'crypt.jwt.decode_header')
128
148
  raise DecodeError, "failed to decode token header: #{e.message}"
149
+ rescue StandardError => e
150
+ handle_exception(e, level: :error, operation: 'crypt.jwt.decode_header')
151
+ raise
129
152
  end
130
153
 
131
154
  def self.validate_algorithm!(algorithm)
@@ -138,7 +161,28 @@ module Legion
138
161
  hash.transform_keys(&:to_sym)
139
162
  end
140
163
 
141
- private_class_method :validate_algorithm!, :symbolize_keys, :decode_header
164
+ def self.sanitize_payload(payload)
165
+ payload.each_with_object({}) do |(key, value), sanitized|
166
+ next if %w[iss iat exp jti].include?(key.to_s)
167
+
168
+ sanitized[key] = value
169
+ end
170
+ end
171
+
172
+ def self.validate_external_requirements!(issuers:, audience:)
173
+ raise ArgumentError, 'issuers is required for JWKS verification' if blank_external_requirement?(issuers)
174
+ raise ArgumentError, 'audience is required for JWKS verification' if blank_external_requirement?(audience)
175
+ end
176
+
177
+ def self.blank_external_requirement?(value)
178
+ return true if value.nil?
179
+ return true if value.respond_to?(:empty?) && value.empty?
180
+
181
+ false
182
+ end
183
+
184
+ private_class_method :validate_algorithm!, :symbolize_keys, :decode_header, :sanitize_payload,
185
+ :validate_external_requirements!, :blank_external_requirement?
142
186
  end
143
187
  end
144
188
  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
  module KerberosAuth
@@ -9,6 +11,7 @@ module Legion
9
11
  DEFAULT_AUTH_PATH = 'auth/kerberos/login'
10
12
 
11
13
  @kerberos_principal = nil
14
+ extend Legion::Logging::Helper
12
15
 
13
16
  class << self
14
17
  attr_reader :kerberos_principal
@@ -17,19 +20,21 @@ module Legion
17
20
  def self.login(vault_client:, service_principal:, auth_path: DEFAULT_AUTH_PATH)
18
21
  raise GemMissingError, 'lex-kerberos gem is required for Kerberos auth' unless spnego_available?
19
22
 
20
- log_debug("login: SPN=#{service_principal}, auth_path=#{auth_path}")
23
+ log.info "KerberosAuth login requested auth_path=#{auth_path}"
24
+ log.debug("KerberosAuth: login: SPN=#{service_principal}, auth_path=#{auth_path}")
21
25
  addr = vault_client.respond_to?(:address) ? vault_client.address : 'n/a'
22
26
  ns = vault_client.respond_to?(:namespace) ? vault_client.namespace.inspect : 'n/a'
23
- log_debug("login: vault_client.address=#{addr}, namespace=#{ns}")
27
+ log.debug("KerberosAuth: login: vault_client.address=#{addr}, namespace=#{ns}")
24
28
 
25
29
  @kerberos_principal = nil
26
30
  token = obtain_token(service_principal)
27
- log_debug("login: SPNEGO token obtained (#{token.length} chars)")
31
+ log.debug("KerberosAuth: login: SPNEGO token obtained (#{token.length} chars)")
28
32
 
29
33
  result = exchange_token(vault_client, token, auth_path)
30
34
  @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")
35
+ log.debug("KerberosAuth: login: authenticated as #{@kerberos_principal.inspect}, policies=#{result[:policies].inspect}")
36
+ log.debug("KerberosAuth: login: renewable=#{result[:renewable]}, ttl=#{result[:lease_duration]}s")
37
+ log.info "KerberosAuth login success principal=#{@kerberos_principal || 'unknown'} auth_path=#{auth_path}"
33
38
  result
34
39
  end
35
40
 
@@ -39,7 +44,8 @@ module Legion
39
44
  @spnego_available = begin
40
45
  require 'legion/extensions/kerberos/helpers/spnego'
41
46
  true
42
- rescue LoadError
47
+ rescue LoadError => e
48
+ handle_exception(e, level: :debug, operation: 'crypt.kerberos_auth.spnego_available')
43
49
  # check if constant was already defined (e.g. stubbed in tests or loaded via another path)
44
50
  defined?(Legion::Extensions::Kerberos::Helpers::Spnego) ? true : false
45
51
  end
@@ -50,11 +56,6 @@ module Legion
50
56
  @kerberos_principal = nil
51
57
  end
52
58
 
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
-
58
59
  class << self
59
60
  private
60
61
 
@@ -63,7 +64,11 @@ module Legion
63
64
  result = helper.obtain_spnego_token(service_principal: service_principal)
64
65
  raise AuthError, "SPNEGO token acquisition failed: #{result[:error]}" unless result[:success]
65
66
 
67
+ log.info 'KerberosAuth obtained SPNEGO token'
66
68
  result[:token]
69
+ rescue StandardError => e
70
+ handle_exception(e, level: :error, operation: 'crypt.kerberos_auth.obtain_token', auth_method: 'kerberos')
71
+ raise
67
72
  end
68
73
 
69
74
  def exchange_token(vault_client, spnego_token, auth_path)
@@ -72,7 +77,8 @@ module Legion
72
77
 
73
78
  # The Vault Kerberos plugin reads the SPNEGO token from the HTTP
74
79
  # 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'})")
80
+ namespace = vault_client.respond_to?(:namespace) ? vault_client.namespace.inspect : 'n/a'
81
+ log.debug("KerberosAuth: exchange_token: PUT /v1/#{auth_path} (namespace=#{namespace})")
76
82
  json = vault_client.put(
77
83
  "/v1/#{auth_path}",
78
84
  '{}',
@@ -90,8 +96,12 @@ module Legion
90
96
  metadata: auth.metadata
91
97
  }
92
98
  rescue ::Vault::HTTPClientError => e
93
- log_debug("exchange_token: HTTP error: #{e.message}")
99
+ handle_exception(e, level: :warn, operation: 'crypt.kerberos_auth.exchange_token', auth_path: auth_path)
100
+ log.debug("KerberosAuth: exchange_token: HTTP error: #{e.message}")
94
101
  raise AuthError, "Vault Kerberos auth failed: #{e.message}"
102
+ rescue StandardError => e
103
+ handle_exception(e, level: :error, operation: 'crypt.kerberos_auth.exchange_token', auth_path: auth_path)
104
+ raise
95
105
  end
96
106
  end
97
107
  end
@@ -1,10 +1,15 @@
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 LdapAuth
8
+ include Legion::Logging::Helper
9
+
6
10
  def ldap_login(cluster_name:, username:, password:)
7
11
  cluster_name = cluster_name.to_sym
12
+ log.info "LDAP login requested user=#{username} cluster=#{cluster_name}"
8
13
  client = vault_client(cluster_name)
9
14
  secret = client.logical.write("auth/ldap/login/#{username}", password: password)
10
15
  auth = secret.auth
@@ -12,13 +17,14 @@ module Legion
12
17
 
13
18
  clusters[cluster_name][:token] = token
14
19
  clusters[cluster_name][:connected] = true
15
- mark_vault_connected
20
+ client.token = token if client.respond_to?(:token=)
16
21
 
17
- Legion::Logging.info "LDAP login success: user=#{username}, cluster=#{cluster_name}" if defined?(Legion::Logging)
22
+ log.info "LDAP login success: user=#{username}, cluster=#{cluster_name}"
18
23
  { token: token, lease_duration: auth.lease_duration,
19
24
  renewable: auth.renewable?, policies: auth.policies }
20
25
  rescue StandardError => e
21
- Legion::Logging.warn "LDAP login failed: user=#{username}, cluster=#{cluster_name}: #{e.message}" if defined?(Legion::Logging)
26
+ handle_exception(e, level: :error, operation: 'crypt.ldap_auth.ldap_login', cluster_name: cluster_name, username: username)
27
+ log.error "LDAP login failed: user=#{username}, cluster=#{cluster_name}: #{e.message}"
22
28
  raise
23
29
  end
24
30
 
@@ -29,9 +35,11 @@ module Legion
29
35
 
30
36
  results[name] = ldap_login(cluster_name: name, username: username, password: password)
31
37
  rescue StandardError => e
32
- Legion::Logging.warn("Legion::Crypt::LdapAuth#ldap_login_all cluster=#{name} failed: #{e.message}") if defined?(Legion::Logging)
38
+ handle_exception(e, level: :warn, operation: 'crypt.ldap_auth.ldap_login_all', cluster_name: name, username: username)
39
+ log.warn("Legion::Crypt::LdapAuth#ldap_login_all cluster=#{name} failed: #{e.message}")
33
40
  results[name] = { error: e.message }
34
41
  end
42
+ log.info "LDAP login_all complete successes=#{results.count { |_, result| result.is_a?(Hash) && !result.key?(:error) }} attempted=#{results.size}"
35
43
  results
36
44
  end
37
45
  end