legion-settings 1.3.16 → 1.3.18

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: 4263d479bb6b10a4fad1e5e0337153a8ff3f4ae6ff29beaf1089968e70b15f0c
4
- data.tar.gz: 3f760ac986406ee7bd6ea9bdad044674e9e591936a01db682c315e63c9d52dfd
3
+ metadata.gz: 6864083fb9f9f31a4019e85d6e1798afa56fc0bb53123b8388002024803bc11d
4
+ data.tar.gz: 843ad56f1af0c905c4f53335d20b4300513b97b82e8f1b65aaf971c7c21f7b7f
5
5
  SHA512:
6
- metadata.gz: 3739c42b861c346d5e5915680bcd2a80277e9ecc77ca784106051003e7a03af6720e348a291c689b9f66ac5611bb4d74b41c24f5b5abbb1182493661d172bbfe
7
- data.tar.gz: 2c1710786b2786c4ba263a7eb49d67701d20ad332245a99fa8fee2b28ce128faeffc0895efe52af0bc4512a4e2fc0dc068f0f2db71e0a6c3b8334bd664c8aaf0
6
+ metadata.gz: d1116e961f4b82d4d5380a27c3f798e45022b72792e44108eff17f1d5ad74b4a93d744e245690ef90895d2a320c862ee646fb4b72f0bf3bc3df218a0886549ed
7
+ data.tar.gz: 88ebea0500835477eb45550cb48e39b202c1c54eb8bddd6af9e09c93548ccf6864ca366bae9b3dd352c29d0b322c8c00f0949ad2ace90f2229ae8f7a7017c993
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Legion::Settings Changelog
2
2
 
3
+ ## [1.3.18] - 2026-03-24
4
+
5
+ ### Added
6
+ - `Legion::Settings::Validators::Tls` — validates TLS settings blocks (transport, data, api, security) with warnings for weak verify modes, errors for insecure sslmode in production, and missing cert paths
7
+
8
+ ## [1.3.17] - 2026-03-24
9
+
10
+ ### Changed
11
+ - `load` is now idempotent: reuses existing Loader, only sets `@loaded` when config files are provided, skips on subsequent calls unless `force: true`
12
+ - `[]`, `dig`, `merge_settings`, `set_prop`, `validate!`, `resolve_secrets!`, `errors` now use lightweight `ensure_loader` (env vars only, no DNS bootstrap) instead of triggering full `load`
13
+ - Module merges via `merge_settings` at require-time no longer trigger DNS bootstrap or create a new Loader
14
+
15
+ ### Added
16
+ - `loaded?` class method to check if settings have been fully loaded with config files
17
+ - `reset!` class method to clear all state (loader, schema, cross-validations) for testing
18
+ - `ensure_loader` private method: creates minimal Loader with env vars only, no DNS bootstrap
19
+
3
20
  ## [1.3.16] - 2026-03-24
4
21
 
5
22
  ### Fixed
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Settings
5
+ module Validators
6
+ module Tls
7
+ TLS_BLOCKS = %i[transport data cache security api].freeze
8
+
9
+ class << self
10
+ def validate(settings)
11
+ warnings = []
12
+ errors = []
13
+
14
+ validate_transport_tls(settings, warnings)
15
+ validate_data_tls(settings, warnings, errors)
16
+ validate_security_mtls(settings, warnings)
17
+ validate_api_tls(settings, warnings, errors)
18
+
19
+ { valid: errors.empty?, warnings: warnings, errors: errors }
20
+ end
21
+
22
+ private
23
+
24
+ def validate_transport_tls(settings, warnings)
25
+ tls = dig_tls(settings, :transport)
26
+ return unless tls[:enabled]
27
+
28
+ warnings << 'transport.tls: verify is none — peer verification disabled, connections are unauthenticated' if tls[:verify].to_s == 'none'
29
+
30
+ check_cert_path(tls[:cert], 'transport.tls.cert', warnings)
31
+ check_cert_path(tls[:key], 'transport.tls.key', warnings)
32
+ check_cert_path(tls[:ca], 'transport.tls.ca', warnings)
33
+ end
34
+
35
+ def validate_data_tls(settings, warnings, errors)
36
+ tls = dig_tls(settings, :data)
37
+ return unless tls[:enabled]
38
+
39
+ sslmode = tls[:sslmode].to_s
40
+ return if sslmode.empty? || sslmode == 'verify-full'
41
+
42
+ env = settings[:env].to_s
43
+ msg = "data.tls: sslmode '#{sslmode}' should be 'verify-full' to prevent MITM attacks"
44
+ if env == 'production'
45
+ errors << msg
46
+ else
47
+ warnings << msg
48
+ end
49
+ end
50
+
51
+ def validate_security_mtls(settings, warnings)
52
+ mtls = settings.dig(:security, :mtls) || {}
53
+ mtls = symbolize_keys(mtls)
54
+ return unless mtls[:enabled]
55
+
56
+ check_cert_path(mtls[:cert], 'security.mtls.cert', warnings)
57
+ check_cert_path(mtls[:key], 'security.mtls.key', warnings)
58
+ check_cert_path(mtls[:ca], 'security.mtls.ca', warnings)
59
+ end
60
+
61
+ def validate_api_tls(settings, _warnings, errors)
62
+ tls = dig_tls(settings, :api)
63
+ return unless tls[:enabled]
64
+
65
+ cert = tls[:cert]
66
+ key = tls[:key]
67
+
68
+ errors << 'api.tls: enabled but api.tls.cert is not set' if cert.nil? || cert.to_s.empty?
69
+
70
+ errors << 'api.tls: enabled but api.tls.key is not set' if key.nil? || key.to_s.empty?
71
+ end
72
+
73
+ def dig_tls(settings, component)
74
+ raw = settings.dig(component, :tls) || {}
75
+ symbolize_keys(raw)
76
+ rescue StandardError
77
+ {}
78
+ end
79
+
80
+ def check_cert_path(path, label, warnings)
81
+ return if path.nil? || path.to_s.empty?
82
+ return if path.to_s.start_with?('vault://', 'env://', 'lease://')
83
+ return if ::File.exist?(path.to_s)
84
+
85
+ warnings << "#{label}: path '#{path}' does not exist"
86
+ end
87
+
88
+ def symbolize_keys(hash)
89
+ return {} unless hash.is_a?(Hash)
90
+
91
+ hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Settings
5
- VERSION = '1.3.16'
5
+ VERSION = '1.3.18'
6
6
  end
7
7
  end
@@ -16,25 +16,40 @@ module Legion
16
16
  attr_accessor :loader
17
17
 
18
18
  def load(options = {})
19
- @loader = Legion::Settings::Loader.new
20
- @loader.load_env
21
- @loader.load_dns_bootstrap
19
+ has_config = options[:config_file] || options[:config_dir] || options[:config_dirs]&.any?
20
+
21
+ # Already fully loaded with config files — skip unless forced
22
+ return @loader if @loaded && !options[:force]
23
+
24
+ # Create Loader once; reuse for subsequent calls (preserves module merges)
25
+ if @loader.nil? || options[:force]
26
+ @loader = Legion::Settings::Loader.new
27
+ @loader.load_env
28
+ @loader.load_dns_bootstrap
29
+ end
30
+
22
31
  @loader.load_file(options[:config_file]) if options[:config_file]
23
32
  @loader.load_directory(options[:config_dir]) if options[:config_dir]
24
33
  options[:config_dirs]&.each do |directory|
25
34
  @loader.load_directory(directory)
26
35
  end
36
+
37
+ @loaded = true if has_config
27
38
  logger.info("Settings loaded from #{@loader.loaded_files.size} files")
28
39
  @loader
29
40
  end
30
41
 
42
+ def loaded?
43
+ @loaded == true
44
+ end
45
+
31
46
  def get(options = {})
32
47
  @loader || @loader = load(options)
33
48
  end
34
49
 
35
50
  def [](key)
36
51
  logger.info('Legion::Settings was not loading, auto loading now!') if @loader.nil?
37
- @loader = load if @loader.nil?
52
+ ensure_loader
38
53
  @loader[key]
39
54
  rescue NoMethodError, TypeError => e
40
55
  Legion::Logging.debug("Legion::Settings#[] key=#{key} failed: #{e.message}") if defined?(Legion::Logging)
@@ -42,7 +57,7 @@ module Legion
42
57
  end
43
58
 
44
59
  def dig(*keys)
45
- @loader = load if @loader.nil?
60
+ ensure_loader
46
61
  @loader.dig(*keys)
47
62
  rescue NoMethodError, TypeError => e
48
63
  Legion::Logging.debug("Legion::Settings#dig keys=#{keys.inspect} failed: #{e.message}") if defined?(Legion::Logging)
@@ -50,12 +65,12 @@ module Legion
50
65
  end
51
66
 
52
67
  def set_prop(key, value)
53
- @loader = load if @loader.nil?
68
+ ensure_loader
54
69
  @loader[key] = value
55
70
  end
56
71
 
57
72
  def merge_settings(key, hash)
58
- @loader = load if @loader.nil?
73
+ ensure_loader
59
74
  thing = {}
60
75
  thing[key.to_sym] = hash
61
76
  @loader.load_module_settings(thing)
@@ -90,7 +105,7 @@ module Legion
90
105
  end
91
106
 
92
107
  def validate!
93
- @loader = load if @loader.nil?
108
+ ensure_loader
94
109
  revalidate_all_modules
95
110
  run_cross_validations
96
111
  detect_unknown_keys
@@ -108,7 +123,7 @@ module Legion
108
123
  end
109
124
 
110
125
  def resolve_secrets!
111
- @loader = load if @loader.nil?
126
+ ensure_loader
112
127
  require 'legion/settings/resolver'
113
128
  Resolver.resolve_secrets!(@loader.to_hash)
114
129
  logger.debug('Secret resolution complete')
@@ -119,10 +134,17 @@ module Legion
119
134
  end
120
135
 
121
136
  def errors
122
- @loader = load if @loader.nil?
137
+ ensure_loader
123
138
  @loader.errors
124
139
  end
125
140
 
141
+ def reset!
142
+ @loader = nil
143
+ @loaded = nil
144
+ @schema = nil
145
+ @cross_validations = nil
146
+ end
147
+
126
148
  def logger
127
149
  @logger = if ::Legion.const_defined?('Logging')
128
150
  ::Legion::Logging
@@ -138,6 +160,14 @@ module Legion
138
160
 
139
161
  private
140
162
 
163
+ def ensure_loader
164
+ return @loader if @loader
165
+
166
+ @loader = Legion::Settings::Loader.new
167
+ @loader.load_env
168
+ @loader
169
+ end
170
+
141
171
  def cross_validations
142
172
  @cross_validations ||= []
143
173
  end
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.16
4
+ version: 1.3.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -57,6 +57,7 @@ files:
57
57
  - lib/legion/settings/resolver.rb
58
58
  - lib/legion/settings/schema.rb
59
59
  - lib/legion/settings/validation_error.rb
60
+ - lib/legion/settings/validators/tls.rb
60
61
  - lib/legion/settings/version.rb
61
62
  - sonar-project.properties
62
63
  homepage: https://github.com/LegionIO/legion-settings