lex-identity-entra 0.5.4 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a50147c3bd7a673e611e5191168b9ade2fe468041ad2b320fccba89dc267a999
4
- data.tar.gz: a0889be0de3da89dedae7360058f0463ae8570a735b5f759750f74bf0e720974
3
+ metadata.gz: ea7d2caf79ed2c6025cbad1ee6daae0a5f5250aada92cf26f42c71bf8e849680
4
+ data.tar.gz: 3f7afe033551273e7ebe6345c40e24f824529b0f4df79a1f3e7677002dcb006c
5
5
  SHA512:
6
- metadata.gz: bf3ff78771dc93dea6cb326c1577d6421382575e714eb4ef25ae67ca07cbeceab37f5b616dd9a611eea85e3ff31d868964878f85409ced0e8688510d3231e8d8
7
- data.tar.gz: 74f7b159ecc89ba0e306273e3e6438b81d8cb1ad09c52d5e2f9c1fb1d6cf4de95e06de62317c8253c8a69b9bfd9cb365ca5785ffede017d0e365dfbfd9502e61
6
+ metadata.gz: 55af5175673b2d9371c0e1d8b8ff03479299e7425b8f6f79b88b941e2b31bcb44069f026bcae90d898a87a7a6511b69a2c0618279ed73b1ac6631369d9733db2
7
+ data.tar.gz: fc672dfff7aa30a9325b7046752f5cdefe737f39b5e8bb69066657c0873d2f8ae600bf758a2ad42fcc382cb5a47467090e074931581486124e89eb52b66dd53b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0] - 2026-09-25
4
+ ### Changed
5
+ - **Breaking**: Vault token persistence is now opt-in in both directions. New per-pattern settings `identity.entra.<pattern>.token.vault_read_enabled` and `vault_write_enabled`, both defaulting to `false`. With the defaults, Entra tokens are never read from or written to Vault. Set the flags explicitly to restore 0.5.x Vault behavior (each direction is independent).
6
+ - **Breaking**: Removed local disk token persistence (`~/.legionio/tokens/entra_<qualifier>.json`) and its `local_token_path` setting. Tokens are held in-memory by default and in Vault only when explicitly opted in — nothing is ever written to disk. In the default configuration tokens no longer survive a process restart: re-auth via `auto_authenticate` or `legion lex exec entra auth login`.
7
+ - Upgrades from 0.5.x: tokens that exist only in Vault are not read by default (expect a one-time re-auth, or set `vault_read_enabled: true`), and stale on-disk token files are no longer read or written.
8
+
3
9
  ## [0.5.4] - 2026-08-04
4
10
  ### Fixed
5
11
  - Stop writing delegated Entra tokens to bootstrap Vault paths. Token persistence now uses only the canonical `users/<identity>/entra/<qualifier>/auth` Vault path and falls back to local disk until a canonical identity is available.
data/README.md CHANGED
@@ -7,7 +7,7 @@ and workload identity authentication patterns.
7
7
  ## Features
8
8
 
9
9
  - **Delegated auth** — OAuth2 PKCE browser flow with local callback server; device-code fallback
10
- - **Token persistence** — Vault-first (`kv/data/users/<identity>/entra/<qualifier>/auth`), disk fallback, in-memory fallback; disk file deleted once Vault write succeeds
10
+ - **Token persistence** — in-memory by default; Vault (`kv/data/users/<identity>/entra/<qualifier>/auth`) reading and writing are separate opt-ins via `vault_read_enabled` / `vault_write_enabled`. Tokens are never written to local disk
11
11
  - **Scope fingerprinting** — MD5 fingerprint of active scopes stored with token; any scope change forces re-authentication on next boot
12
12
  - **Identity integration** — `AuthValidator` upgrades `Legion::Identity::Process` via `Resolver.upgrade!` and registers with `Legion::Identity::Broker` for cross-extension token access
13
13
  - **Application credentials** — client credentials flow for service-to-service auth
@@ -36,6 +36,8 @@ identity:
36
36
  auto_authenticate: false # set true to open browser automatically at boot
37
37
  callback_timeout: 120
38
38
  token:
39
+ vault_read_enabled: false # set true to read the token from Vault
40
+ vault_write_enabled: false # set true to write the token to Vault
39
41
  refresh_buffer: 60
40
42
  refresh_interval: 900
41
43
  scopes:
@@ -98,11 +100,10 @@ identity:
98
100
 
99
101
  ## Token storage
100
102
 
101
- | Backend | Path | Priority |
102
- |---------|------|----------|
103
- | HashiCorp Vault | `kv/data/users/<identity>/entra/delegated/auth` | 1 (preferred) |
104
- | Local disk | `~/.legionio/tokens/entra_delegated.json` | 2 (fallback, deleted when Vault succeeds) |
105
- | Memory | In-process store | 3 (runtime fallback) |
103
+ | Backend | Path | When used |
104
+ |---------|------|-----------|
105
+ | HashiCorp Vault | `kv/data/users/<identity>/entra/delegated/auth` | Only when opted in: `vault_read_enabled` / `vault_write_enabled` (both default `false`) |
106
+ | Memory | In-process store | Default persistence; tokens are never written to local disk |
106
107
 
107
108
  ## Identity provider contract
108
109
 
@@ -32,10 +32,11 @@ module Legion
32
32
  category_overrides: {}
33
33
  },
34
34
  token: {
35
- vault_path: nil,
36
- local_token_path: nil,
37
- refresh_buffer: 60,
38
- refresh_interval: 2700
35
+ vault_read_enabled: false,
36
+ vault_write_enabled: false,
37
+ vault_path: nil,
38
+ refresh_buffer: 60,
39
+ refresh_interval: 2700
39
40
  }
40
41
  }
41
42
  end
@@ -139,8 +139,7 @@ module Legion
139
139
  end
140
140
 
141
141
  def previously_authenticated?
142
- path = Legion::Extensions::Identity::Entra::Helpers::TokenManager.local_path(:delegated)
143
- File.exist?(path)
142
+ !stored_token_data.nil?
144
143
  end
145
144
 
146
145
  def stored_token_data
@@ -57,7 +57,6 @@ module Legion
57
57
 
58
58
  def stored_raw_data
59
59
  Legion::Extensions::Identity::Entra::Helpers::TokenManager.from_vault_data(:delegated) ||
60
- Legion::Extensions::Identity::Entra::Helpers::TokenManager.from_local_data(:delegated) ||
61
60
  Legion::Extensions::Identity::Entra::Helpers::TokenManager.from_memory(:delegated)
62
61
  end
63
62
 
@@ -35,10 +35,11 @@ module Legion
35
35
  category_overrides: {}
36
36
  },
37
37
  token: {
38
- vault_path: nil,
39
- local_token_path: nil,
40
- refresh_buffer: 60,
41
- refresh_interval: 900
38
+ vault_read_enabled: false,
39
+ vault_write_enabled: false,
40
+ vault_path: nil,
41
+ refresh_buffer: 60,
42
+ refresh_interval: 900
42
43
  },
43
44
  browser_auth: {
44
45
  auto_authenticate: false,
@@ -16,19 +16,13 @@ module Legion
16
16
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
17
17
  Legion::Extensions::Helpers.const_defined?(:Lex, false)
18
18
 
19
- # Returns an array of qualifier symbols for which tokens exist locally.
19
+ # Returns an array of qualifier symbols for which tokens exist in-process.
20
20
  def discovered_qualifiers
21
- (local_qualifiers + broker_qualifiers).uniq
21
+ (memory_qualifiers + broker_qualifiers).uniq
22
22
  end
23
23
 
24
- def local_qualifiers
25
- return [] unless File.directory?(Legion::Extensions::Identity::Entra::Helpers::TokenManager::TOKEN_DIR)
26
-
27
- Dir.glob(File.join(Legion::Extensions::Identity::Entra::Helpers::TokenManager::TOKEN_DIR, 'entra_*.json')).filter_map do |path|
28
- basename = File.basename(path, '.json')
29
- match = basename.match(/\Aentra_(.+)\z/)
30
- match[1].to_sym if match
31
- end
24
+ def memory_qualifiers
25
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.memory_store.keys
32
26
  end
33
27
 
34
28
  def broker_qualifiers
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'concurrent'
4
4
  require 'digest'
5
- require 'fileutils'
6
5
  require 'time'
7
6
 
8
7
  module Legion
@@ -16,7 +15,6 @@ module Legion
16
15
  include Legion::Settings::Helper
17
16
  include Legion::JSON::Helper
18
17
 
19
- TOKEN_DIR = File.join(Dir.home, '.legionio', 'tokens')
20
18
  REFRESH_BUFFER = 60
21
19
  MEMORY_STORE = Concurrent::Hash.new
22
20
 
@@ -34,20 +32,7 @@ module Legion
34
32
 
35
33
  def token_data(qualifier = :delegated, refresh: true)
36
34
  log.debug("TokenManager.token_data: qualifier=#{qualifier} refresh=#{refresh}")
37
- vault_data = from_vault_data(qualifier)
38
- other_data = vault_data || from_local_data(qualifier) || from_memory(qualifier)
39
- if other_data && !vault_data && vault_available? && canonical_name_available?
40
- log.info("TokenManager.token_data: backfilling #{qualifier} token to vault")
41
- backfill_saved = save_to_vault(qualifier, access_token: other_data[:access_token],
42
- refresh_token: other_data[:refresh_token],
43
- expires_at: other_data[:expires_at],
44
- scopes: other_data[:scopes],
45
- tenant_id: other_data[:tenant_id],
46
- client_id: other_data[:client_id],
47
- scope_fingerprint: other_data[:scope_fingerprint])
48
- delete_local(qualifier) if backfill_saved
49
- end
50
- data = other_data
35
+ data = from_vault_data(qualifier) || from_memory(qualifier)
51
36
  return nil unless data
52
37
 
53
38
  if scope_fingerprint_stale?(qualifier, data)
@@ -70,18 +55,10 @@ module Legion
70
55
  log.debug("TokenManager.save_token: qualifier=#{qualifier} expires_in=#{expires_in}")
71
56
  expires_at ||= Time.now + expires_in.to_i if expires_in
72
57
  fingerprint = current_scope_fingerprint(qualifier)
73
- vault_saved = save_to_vault(qualifier, access_token: access_token, refresh_token: refresh_token,
74
- expires_at: expires_at, scopes: scopes,
75
- tenant_id: tenant_id, client_id: client_id,
76
- scope_fingerprint: fingerprint)
77
- if vault_saved
78
- delete_local(qualifier)
79
- else
80
- save_to_local(qualifier, access_token: access_token, refresh_token: refresh_token,
81
- expires_at: expires_at, scopes: scopes,
82
- tenant_id: tenant_id, client_id: client_id,
83
- scope_fingerprint: fingerprint)
84
- end
58
+ save_to_vault(qualifier, access_token: access_token, refresh_token: refresh_token,
59
+ expires_at: expires_at, scopes: scopes,
60
+ tenant_id: tenant_id, client_id: client_id,
61
+ scope_fingerprint: fingerprint)
85
62
  save_to_memory(qualifier, access_token: access_token, refresh_token: refresh_token,
86
63
  expires_at: expires_at, scopes: scopes,
87
64
  tenant_id: tenant_id, client_id: client_id,
@@ -89,6 +66,7 @@ module Legion
89
66
  end
90
67
 
91
68
  def from_vault_data(qualifier)
69
+ return nil unless vault_read_enabled?(qualifier)
92
70
  return nil unless vault_available? && canonical_name_available?
93
71
 
94
72
  read_vault_token(qualifier, vault_path(qualifier))
@@ -106,20 +84,9 @@ module Legion
106
84
  nil
107
85
  end
108
86
 
109
- def from_local_data(qualifier)
110
- path = local_path(qualifier)
111
- return nil unless File.exist?(path)
112
-
113
- log.debug("TokenManager.from_local_data: reading #{path}")
114
- normalize_token_data(json_load(File.read(path)))
115
- rescue StandardError => e
116
- handle_exception(e, level: :warn, operation: 'token_manager.from_local_data',
117
- qualifier: qualifier, path: path)
118
- nil
119
- end
120
-
121
87
  def save_to_vault(qualifier, access_token:, refresh_token:, expires_at:,
122
88
  scopes: nil, tenant_id: nil, client_id: nil, scope_fingerprint: nil)
89
+ return unless vault_write_enabled?(qualifier)
123
90
  return unless vault_available?
124
91
  return unless canonical_name_available?
125
92
 
@@ -144,38 +111,6 @@ module Legion
144
111
  nil
145
112
  end
146
113
 
147
- def save_to_local(qualifier, access_token:, refresh_token:, expires_at:,
148
- scopes: nil, tenant_id: nil, client_id: nil, scope_fingerprint: nil)
149
- path = local_path(qualifier)
150
- log.debug("TokenManager.save_to_local: writing #{path}")
151
- FileUtils.mkdir_p(File.dirname(path))
152
- File.write(path, json_dump({
153
- access_token: access_token,
154
- refresh_token: refresh_token,
155
- expires_at: expires_at&.utc&.iso8601,
156
- scopes: scopes,
157
- tenant_id: tenant_id,
158
- client_id: client_id,
159
- scope_fingerprint: scope_fingerprint
160
- }))
161
- File.chmod(0o600, path)
162
- log.debug('TokenManager.save_to_local: success')
163
- rescue StandardError => e
164
- handle_exception(e, level: :warn, operation: 'token_manager.save_to_local',
165
- qualifier: qualifier, path: path)
166
- nil
167
- end
168
-
169
- def delete_local(qualifier)
170
- path = local_path(qualifier)
171
- return unless File.exist?(path)
172
-
173
- File.delete(path)
174
- log.info("TokenManager.delete_local: removed #{path} (vault is authoritative)")
175
- rescue StandardError => e
176
- handle_exception(e, level: :warn, operation: 'token_manager.delete_local', qualifier: qualifier)
177
- end
178
-
179
114
  def from_memory(qualifier)
180
115
  data = TokenManager.memory_store[qualifier.to_sym]
181
116
  return nil unless data
@@ -204,7 +139,7 @@ module Legion
204
139
  end
205
140
 
206
141
  def scope_fingerprint_stale?(qualifier, data = nil)
207
- data ||= from_local_data(qualifier) || from_memory(qualifier)
142
+ data ||= from_memory(qualifier)
208
143
  return false unless data
209
144
 
210
145
  stored = data[:scope_fingerprint]
@@ -280,6 +215,14 @@ module Legion
280
215
  !load_token(qualifier).nil?
281
216
  end
282
217
 
218
+ def vault_read_enabled?(qualifier)
219
+ Legion::Settings.dig(:identity, :entra, qualifier.to_sym, :token, :vault_read_enabled)
220
+ end
221
+
222
+ def vault_write_enabled?(qualifier)
223
+ Legion::Settings.dig(:identity, :entra, qualifier.to_sym, :token, :vault_write_enabled)
224
+ end
225
+
283
226
  def vault_available?
284
227
  defined?(Legion::Crypt) &&
285
228
  Legion::Crypt.respond_to?(:vault_connected?) &&
@@ -296,14 +239,6 @@ module Legion
296
239
  "users/#{Legion::Identity::Process.canonical_name}/entra/#{qualifier}/auth"
297
240
  end
298
241
 
299
- def local_path(qualifier)
300
- auth = settings_auth
301
- pattern_settings = auth[qualifier.to_sym]
302
- return File.expand_path(pattern_settings[:local_token_path]) if pattern_settings.is_a?(Hash) && pattern_settings[:local_token_path]
303
-
304
- File.join(TOKEN_DIR, "entra_#{qualifier}.json")
305
- end
306
-
307
242
  def settings_auth
308
243
  identity_entra = Legion::Settings.dig(:identity, :entra) || {}
309
244
  auth = identity_entra[:auth].is_a?(Hash) ? identity_entra[:auth].dup : identity_entra.dup
@@ -28,10 +28,11 @@ module Legion
28
28
  category_overrides: {}
29
29
  },
30
30
  token: {
31
- vault_path: nil,
32
- local_token_path: nil,
33
- refresh_buffer: 60,
34
- refresh_interval: 2700
31
+ vault_read_enabled: false,
32
+ vault_write_enabled: false,
33
+ vault_path: nil,
34
+ refresh_buffer: 60,
35
+ refresh_interval: 2700
35
36
  }
36
37
  }
37
38
  end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Identity
6
6
  module Entra
7
- VERSION = '0.5.4'
7
+ VERSION = '0.6.0'
8
8
  end
9
9
  end
10
10
  end
@@ -29,10 +29,11 @@ module Legion
29
29
  category_overrides: {}
30
30
  },
31
31
  token: {
32
- vault_path: nil,
33
- local_token_path: nil,
34
- refresh_buffer: 60,
35
- refresh_interval: 2700
32
+ vault_read_enabled: false,
33
+ vault_write_enabled: false,
34
+ vault_path: nil,
35
+ refresh_buffer: 60,
36
+ refresh_interval: 2700
36
37
  }
37
38
  }
38
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-identity-entra
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity