legion-settings 1.3.0 → 1.3.2

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: b2fb957d328761484c232467b3f37dfea86359a31726a6f3910e2e02838e3977
4
- data.tar.gz: f10dcf26575430c481b466ebf490e3331d2d1c8ba841b438ca15560c8554c965
3
+ metadata.gz: 905fefabffcdb64f2d654827742feb7c2651a3603625eb7f4e2eb71930416c8e
4
+ data.tar.gz: e19c105841be33b885565d5dac97d2549ee9813897ea4286bf68977e35e95e63
5
5
  SHA512:
6
- metadata.gz: b69cabe6c5265040c40791eb9b0c9c284c42438b11b9c146113b57fa9dc6c87d4c28072a3ce569cf9a5f34d3151bac4adaf8ed0a923fd8cecbcf40b8aae6103c
7
- data.tar.gz: cbeaf56feeaca1cb4a02fdf76cf501e82d5619d489228570ec0906033e437249e3d85e98db652072c73b145bb80162f66f206dc6e1b0c79266606b7e647bf922
6
+ metadata.gz: 71a352c5cd2a45145619ae24d31f9b9f3280c33c0d2f864c21c9549f7a78a4a48b45f06d4172bc6ed30d314a355ab1548bc141efa63638917734816b1c66c0ba
7
+ data.tar.gz: 6a7a29a655bc77c13352631a62763bc20c8cf9d2a7e84efeaa43b92cc70301425081aa8ed82e6d34a2c77aac9944500ff241ce404f09d0af76bb4de9aaf68baf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Legion::Settings Changelog
2
2
 
3
+ ## [1.3.2] - 2026-03-17
4
+
5
+ ### Added
6
+ - `dig(*keys)` method on `Legion::Settings` and `Legion::Settings::Loader` for nested key access
7
+
8
+ ## [1.3.1] - 2026-03-16
9
+
10
+ ### Added
11
+ - `lease://name#key` URI scheme in secret resolver for dynamic Vault leases
12
+ - Delegates to `Legion::Crypt::LeaseManager` for lease data lookup
13
+ - Registers reverse references for push-back on credential rotation
14
+
3
15
  ## [1.3.0] - 2026-03-16
4
16
 
5
17
  ### Added
data/CLAUDE.md CHANGED
@@ -77,7 +77,7 @@ Legion::Settings (singleton module)
77
77
  | `spec/legion/settings/validation_error_spec.rb` | Error formatting tests |
78
78
  | `spec/legion/settings/integration_spec.rb` | End-to-end validation tests |
79
79
  | `spec/legion/settings/role_defaults_spec.rb` | Role profile default settings tests |
80
- | `spec/legion/settings/resolver_spec.rb` | Secret resolver tests (env://, vault://, fallback chains) |
80
+ | `spec/legion/settings/resolver_spec.rb` | Secret resolver tests (env://, vault://, lease://, fallback chains) |
81
81
 
82
82
  ## Secret Resolution
83
83
 
@@ -87,8 +87,9 @@ Settings values can reference external secret sources using URI syntax. Resolved
87
87
 
88
88
  | Scheme | Format | Resolution |
89
89
  |--------|--------|------------|
90
- | `vault://` | `vault://path/to/secret#key` | `Legion::Crypt.read(path)[key]` |
90
+ | `vault://` | `vault://path/to/secret#key` | `Legion::Crypt.read(path)[key]` (static KV secrets) |
91
91
  | `env://` | `env://ENV_VAR_NAME` | `ENV['ENV_VAR_NAME']` |
92
+ | `lease://` | `lease://name#key` | `Legion::Crypt::LeaseManager.instance.fetch(name, key)` (dynamic Vault leases) |
92
93
  | *(plain string)* | `"guest"` | Returned as-is |
93
94
 
94
95
  ### Fallback Chains
data/README.md CHANGED
@@ -28,11 +28,41 @@ Legion::Settings[:transport][:connection][:host]
28
28
  ### Config Paths (checked in order)
29
29
 
30
30
  1. `/etc/legionio/`
31
- 2. `~/legionio/`
32
- 3. `./settings/`
31
+ 2. `~/.legionio/settings/`
32
+ 3. `~/legionio/`
33
+ 4. `./settings/`
33
34
 
34
35
  Each Legion module registers its own defaults via `merge_settings` during startup.
35
36
 
37
+ ### Secret Resolution
38
+
39
+ Settings values can reference external secret sources using URI syntax. Three schemes are supported:
40
+
41
+ | Scheme | Format | Resolution |
42
+ |--------|--------|------------|
43
+ | `vault://` | `vault://path/to/secret#key` | Reads static KV secrets from HashiCorp Vault via `Legion::Crypt` |
44
+ | `env://` | `env://ENV_VAR_NAME` | Reads from environment variable |
45
+ | `lease://` | `lease://name#key` | Reads from dynamic Vault leases via `Legion::Crypt::LeaseManager` |
46
+
47
+ Array values act as fallback chains — the first non-nil result wins:
48
+
49
+ ```json
50
+ {
51
+ "transport": {
52
+ "connection": {
53
+ "password": ["vault://secret/data/rabbitmq#password", "env://RABBITMQ_PASSWORD", "guest"]
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ Call `Legion::Settings.resolve_secrets!` to resolve all URIs in-place. In the LegionIO boot sequence this is called automatically after `Legion::Crypt.start`. The `env://` scheme works even when Vault is not connected.
60
+
61
+ ```ruby
62
+ Legion::Settings.resolve_secrets!
63
+ # All vault://, env://, and lease:// references are now replaced with their resolved values
64
+ ```
65
+
36
66
  ### Schema Validation
37
67
 
38
68
  Types are inferred automatically from default values. Optional constraints can be added:
@@ -69,6 +69,10 @@ module Legion
69
69
  to_hash[key]
70
70
  end
71
71
 
72
+ def dig(*keys)
73
+ to_hash.dig(*keys)
74
+ end
75
+
72
76
  def []=(key, value)
73
77
  @settings[key] = value
74
78
  @indifferent_access = false
@@ -3,9 +3,10 @@
3
3
  module Legion
4
4
  module Settings
5
5
  module Resolver
6
- VAULT_PATTERN = %r{\Avault://(.+?)#(.+)\z}
7
- ENV_PATTERN = %r{\Aenv://(.+)\z}
8
- URI_PATTERN = %r{\A(?:vault|env)://}
6
+ VAULT_PATTERN = %r{\Avault://(.+?)#(.+)\z}
7
+ ENV_PATTERN = %r{\Aenv://(.+)\z}
8
+ LEASE_PATTERN = %r{\Alease://(.+?)#(.+)\z}
9
+ URI_PATTERN = %r{\A(?:vault|env|lease)://}
9
10
 
10
11
  module_function
11
12
 
@@ -18,6 +19,9 @@ module Legion
18
19
  vault_count = count_vault_refs(settings_hash)
19
20
  log_warn("Vault not connected — #{vault_count} vault:// reference(s) will not be resolved") if vault_count.positive? && !@vault_available
20
21
 
22
+ lease_count = count_lease_refs(settings_hash)
23
+ log_warn("LeaseManager not available — #{lease_count} lease:// reference(s) will not be resolved") if lease_count.positive? && !lease_manager_available?
24
+
21
25
  resolved = 0
22
26
  unresolved = 0
23
27
  walk(settings_hash, path: '') do |result|
@@ -51,6 +55,8 @@ module Legion
51
55
  def resolve_single(str)
52
56
  if (m = str.match(VAULT_PATTERN))
53
57
  resolve_vault(m[1], m[2])
58
+ elsif (m = str.match(LEASE_PATTERN))
59
+ resolve_lease(m[1], m[2])
54
60
  elsif (m = str.match(ENV_PATTERN))
55
61
  ENV.fetch(m[1], nil)
56
62
  else
@@ -112,6 +118,7 @@ module Legion
112
118
  block&.call(:unresolved)
113
119
  else
114
120
  hash[key] = resolved
121
+ register_lease_ref(value, current_path) if value.match?(LEASE_PATTERN)
115
122
  block&.call(:resolved)
116
123
  end
117
124
  when Array
@@ -123,6 +130,7 @@ module Legion
123
130
  block&.call(:unresolved)
124
131
  else
125
132
  hash[key] = resolved
133
+ register_lease_refs_from_chain(value, current_path)
126
134
  block&.call(:resolved)
127
135
  end
128
136
  end
@@ -145,10 +153,60 @@ module Legion
145
153
  data[key.to_sym] || data[key.to_s]
146
154
  end
147
155
 
156
+ def resolve_lease(name, key)
157
+ return nil unless lease_manager_available?
158
+
159
+ Legion::Crypt::LeaseManager.instance.fetch(name, key)
160
+ rescue StandardError => e
161
+ log_debug("Settings resolver: lease fetch failed for #{name}##{key}: #{e.message}")
162
+ nil
163
+ end
164
+
165
+ def lease_manager_available?
166
+ defined?(Legion::Crypt::LeaseManager)
167
+ rescue StandardError
168
+ false
169
+ end
170
+
148
171
  def resolvable_chain?(arr)
149
172
  arr.any? { |v| v.is_a?(String) && v.match?(URI_PATTERN) }
150
173
  end
151
174
 
175
+ def register_lease_ref(value, path_string)
176
+ return unless lease_manager_available?
177
+
178
+ m = value.match(LEASE_PATTERN)
179
+ return unless m
180
+
181
+ path_parts = path_string.split('.').map(&:to_sym)
182
+ Legion::Crypt::LeaseManager.instance.register_ref(m[1], m[2], path_parts)
183
+ rescue StandardError
184
+ nil
185
+ end
186
+
187
+ def register_lease_refs_from_chain(arr, path_string)
188
+ return unless lease_manager_available?
189
+
190
+ arr.each do |entry|
191
+ next unless entry.is_a?(String)
192
+
193
+ register_lease_ref(entry, path_string) if entry.match?(LEASE_PATTERN)
194
+ end
195
+ end
196
+
197
+ def count_lease_refs(hash)
198
+ return 0 unless hash.is_a?(Hash)
199
+
200
+ hash.sum do |_key, value|
201
+ case value
202
+ when String then value.match?(LEASE_PATTERN) ? 1 : 0
203
+ when Array then value.count { |v| v.is_a?(String) && v.match?(LEASE_PATTERN) }
204
+ when Hash then count_lease_refs(value)
205
+ else 0
206
+ end
207
+ end
208
+ end
209
+
152
210
  def log_info(message)
153
211
  if defined?(Legion::Logging)
154
212
  Legion::Logging.info(message)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Settings
5
- VERSION = '1.3.0'
5
+ VERSION = '1.3.2'
6
6
  end
7
7
  end
@@ -37,6 +37,13 @@ module Legion
37
37
  nil
38
38
  end
39
39
 
40
+ def dig(*keys)
41
+ @loader = load if @loader.nil?
42
+ @loader.dig(*keys)
43
+ rescue NoMethodError, TypeError
44
+ nil
45
+ end
46
+
40
47
  def set_prop(key, value)
41
48
  @loader = load if @loader.nil?
42
49
  @loader[key] = value
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity