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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +7 -6
- data/lib/legion/extensions/identity/entra/application.rb +5 -4
- data/lib/legion/extensions/identity/entra/delegated/actors/auth_validator.rb +1 -2
- data/lib/legion/extensions/identity/entra/delegated/actors/token_refresher.rb +0 -1
- data/lib/legion/extensions/identity/entra/delegated.rb +5 -4
- data/lib/legion/extensions/identity/entra/helpers/account_discovery.rb +4 -10
- data/lib/legion/extensions/identity/entra/helpers/token_manager.rb +16 -81
- data/lib/legion/extensions/identity/entra/managed_identity.rb +5 -4
- data/lib/legion/extensions/identity/entra/version.rb +1 -1
- data/lib/legion/extensions/identity/entra/workload_identity.rb +5 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea7d2caf79ed2c6025cbad1ee6daae0a5f5250aada92cf26f42c71bf8e849680
|
|
4
|
+
data.tar.gz: 3f7afe033551273e7ebe6345c40e24f824529b0f4df79a1f3e7677002dcb006c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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** —
|
|
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 |
|
|
102
|
-
|
|
103
|
-
| HashiCorp Vault | `kv/data/users/<identity>/entra/delegated/auth` |
|
|
104
|
-
|
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
19
|
+
# Returns an array of qualifier symbols for which tokens exist in-process.
|
|
20
20
|
def discovered_qualifiers
|
|
21
|
-
(
|
|
21
|
+
(memory_qualifiers + broker_qualifiers).uniq
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 ||=
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
@@ -29,10 +29,11 @@ module Legion
|
|
|
29
29
|
category_overrides: {}
|
|
30
30
|
},
|
|
31
31
|
token: {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|