secret_config 0.5.3 → 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: 2f0d63fa6c5c401060b46f0295181765357cd8f9ec6627d005566db84b84ee36
4
- data.tar.gz: d4985e50bfc36057932f30734ae16f48583eaddcee7b8d5d69a3e5721493d88a
3
+ metadata.gz: d00a0e1e7ae87745d69e70f1517d150b3005b5c1598da329a0db829f8d9a770f
4
+ data.tar.gz: 87158952204f0aaf9a029f4df3b3c97e38c64fe7b4d46b62a32c4887ce483ed7
5
5
  SHA512:
6
- metadata.gz: 35e03c3cd275db6e03a3719aaae155b3a9c12f31a1f01c0d37f320a50a589a4e582139ead7befc16b52510eca57833a2be09128647b6a6666ef86b941680e6bf
7
- data.tar.gz: 202df652fdaca983f8f2736df883957891043a42fe6751914175145af99a43699f0e18e6883b677a25e08b3bef48fc218294f7cc9526f8d6f355e8b4ccf837c5
6
+ metadata.gz: 84a4820ed6012d08553cd6659b43efe10420fcf37ce00d7bc4ea93cf16e9e0055979971cec82d45b8f78ba4476c583d438564d4ad36a81ddb21800759a2126c5
7
+ data.tar.gz: 4b4d6a5d9598f9fe1c6db1f9158016124258da6d6d3eac17cde7dbcd5263efb7eec74615b22371b4f196416112e48a4e2b90d93ff460e73a17b3a3e37a79a079
@@ -1,16 +1,17 @@
1
- require 'optparse'
2
- require 'fileutils'
3
- require 'erb'
4
- require 'yaml'
5
- require 'json'
6
- require 'securerandom'
7
- require 'irb'
1
+ require "optparse"
2
+ require "fileutils"
3
+ require "erb"
4
+ require "yaml"
5
+ require "json"
6
+ require "securerandom"
7
+ require "irb"
8
8
 
9
9
  module SecretConfig
10
10
  class CLI
11
11
  attr_reader :path, :region, :provider,
12
12
  :export, :no_filter,
13
- :import, :key_id, :random_size, :prune, :overwrite,
13
+ :import, :key_id, :key_alias, :random_size, :prune, :overwrite,
14
+ :fetch_key, :delete_key, :set_key, :set_value, :delete_path,
14
15
  :copy_path, :diff,
15
16
  :console,
16
17
  :show_version
@@ -26,7 +27,8 @@ module SecretConfig
26
27
  @import = false
27
28
  @path = nil
28
29
  @key_id = nil
29
- @region = ENV['AWS_REGION']
30
+ @key_alias = nil
31
+ @region = ENV["AWS_REGION"]
30
32
  @provider = :ssm
31
33
  @random_size = 32
32
34
  @no_filter = false
@@ -36,6 +38,11 @@ module SecretConfig
36
38
  @show_version = false
37
39
  @console = false
38
40
  @diff = false
41
+ @set_key = nil
42
+ @set_value = nil
43
+ @fetch_key = nil
44
+ @delete_key = nil
45
+ @delete_path = nil
39
46
 
40
47
  if argv.empty?
41
48
  puts parser
@@ -60,6 +67,14 @@ module SecretConfig
60
67
  run_copy(copy_path, path)
61
68
  elsif diff
62
69
  run_diff(diff)
70
+ elsif set_key
71
+ run_set(set_key, set_value)
72
+ elsif fetch_key
73
+ run_fetch(fetch_key)
74
+ elsif delete_key
75
+ run_delete(delete_key)
76
+ elsif delete_path
77
+ run_delete_path(delete_path)
63
78
  else
64
79
  puts parser
65
80
  end
@@ -75,67 +90,82 @@ module SecretConfig
75
90
  secret_config [options]
76
91
  BANNER
77
92
 
78
- opts.on '-e', '--export [FILE_NAME]', 'Export configuration to a file or stdout if no file_name supplied.' do |file_name|
93
+ opts.on "-e", "--export [FILE_NAME]", "Export configuration to a file or stdout if no file_name supplied." do |file_name|
79
94
  @export = file_name || STDOUT
80
95
  end
81
96
 
82
- opts.on '-i', '--import [FILE_NAME]', 'Import configuration from a file or stdin if no file_name supplied.' do |file_name|
97
+ opts.on "-i", "--import [FILE_NAME]", "Import configuration from a file or stdin if no file_name supplied." do |file_name|
83
98
  @import = file_name || STDIN
84
99
  end
85
100
 
86
- opts.on '--copy SOURCE_PATH', 'Import configuration from a file or stdin if no file_name supplied.' do |path|
101
+ opts.on "--copy SOURCE_PATH", "Import configuration from a file or stdin if no file_name supplied." do |path|
87
102
  @copy_path = path
88
103
  end
89
104
 
90
- opts.on '--diff [FILE_NAME]', 'Compare configuration from a file or stdin if no file_name supplied.' do |file_name|
105
+ opts.on "--diff [FILE_NAME]", "Compare configuration from a file or stdin if no file_name supplied." do |file_name|
91
106
  @diff = file_name
92
107
  end
93
108
 
94
- opts.on '-c', '--console', 'Start interactive console.' do
109
+ opts.on "-s", "--set KEY=VALUE", "Set one key to value. Example: --set mysql/database=localhost" do |param|
110
+ @set_key, @set_value = param.split("=")
111
+ unless @set_key && @set_value
112
+ raise(ArgumentError, "Supply key and value separated by '='. Example: --set mysql/database=localhost")
113
+ end
114
+ end
115
+
116
+ opts.on "-f", "--fetch KEY", "Fetch the value for one setting. Example: --get mysql/database. " do |key|
117
+ @fetch_key = key
118
+ end
119
+
120
+ opts.on "-d", "--delete KEY", "Delete one specific key. See --delete-path to delete all keys under a specific path " do |key|
121
+ @delete_key = key
122
+ end
123
+
124
+ opts.on "-r", "--delete-path PATH", "Recursively delete all keys under the specified path.. " do |path|
125
+ @delete_path = path
126
+ end
127
+
128
+ opts.on "-c", "--console", "Start interactive console." do
95
129
  @console = true
96
130
  end
97
131
 
98
- opts.on '-p', '--path PATH', 'Path to import from / export to.' do |path|
132
+ opts.on "-p", "--path PATH", "Path to import from / export to." do |path|
99
133
  @path = path
100
134
  end
101
135
 
102
- opts.on '--provider PROVIDER', 'Provider to use. [ssm | file]. Default: ssm' do |provider|
136
+ opts.on "--provider PROVIDER", "Provider to use. [ssm | file]. Default: ssm" do |provider|
103
137
  @provider = provider.to_sym
104
138
  end
105
139
 
106
- opts.on '--no-filter', 'Do not filter passwords and keys.' do
140
+ opts.on "--no-filter", "Do not filter passwords and keys." do
107
141
  @no_filter = true
108
142
  end
109
143
 
110
- opts.on '--prune', 'During import delete all existing keys for which there is no key in the import file.' do
144
+ opts.on "--prune", "During import delete all existing keys for which there is no key in the import file." do
111
145
  @prune = true
112
146
  end
113
147
 
114
- opts.on '--key_id KEY_ID', 'Encrypt config settings with this AWS KMS key id. Default: AWS Default key.' do |key_id|
148
+ opts.on "--key_id KEY_ID", "Encrypt config settings with this AWS KMS key id. Default: AWS Default key." do |key_id|
115
149
  @key_id = key_id
116
150
  end
117
151
 
118
- opts.on '--key_alias KEY_ALIAS', 'Encrypt config settings with this AWS KMS alias.' do |key_alias|
119
- if key_alias =~ /^alias\//
120
- @key_id = key_alias
121
- else
122
- @key_id = "alias/#{key_alias}"
123
- end
152
+ opts.on "--key_alias KEY_ALIAS", "Encrypt config settings with this AWS KMS alias." do |key_alias|
153
+ @key_alias = key_alias
124
154
  end
125
155
 
126
- opts.on '--region REGION', 'AWS Region to use. Default: AWS_REGION env var.' do |region|
156
+ opts.on "--region REGION", "AWS Region to use. Default: AWS_REGION env var." do |region|
127
157
  @region = region
128
158
  end
129
159
 
130
- opts.on '--random_size INTEGER', Integer, 'Size to use when generating random values. Whenever $random is encountered during an import. Default: 32' do |region|
160
+ opts.on "--random_size INTEGER", Integer, "Size to use when generating random values. Whenever #{RANDOM} is encountered during an import. Default: 32" do |random_size|
131
161
  @random_size = random_size
132
162
  end
133
163
 
134
- opts.on '-v', '--version', 'Display Symmetric Encryption version.' do
164
+ opts.on "-v", "--version", "Display Symmetric Encryption version." do
135
165
  @show_version = true
136
166
  end
137
167
 
138
- opts.on('-h', '--help', 'Prints this help.') do
168
+ opts.on("-h", "--help", "Prints this help.") do
139
169
  puts opts
140
170
  exit
141
171
  end
@@ -148,7 +178,7 @@ module SecretConfig
148
178
  @provider_instance ||= begin
149
179
  case provider
150
180
  when :ssm
151
- Providers::Ssm.new(key_id: key_id)
181
+ Providers::Ssm.new(key_id: key_id, key_alias: key_alias)
152
182
  else
153
183
  raise ArgumentError, "Invalid provider: #{provider}"
154
184
  end
@@ -208,9 +238,9 @@ module SecretConfig
208
238
  (file.keys + registry.keys).sort.uniq.each do |key|
209
239
  if registry.key?(key)
210
240
  if file.key?(key)
211
- if file[key].to_s != registry[key].to_s
212
- puts "* #{key}: #{registry[key]} => #{file[key]}"
213
- end
241
+ value = file[key].to_s
242
+ # Ignore filtered values
243
+ puts "* #{key}: #{registry[key]} => #{file[key]}" if (value != registry[key].to_s) && (value != FILTERED)
214
244
  else
215
245
  puts "- #{key}"
216
246
  end
@@ -226,6 +256,19 @@ module SecretConfig
226
256
  IRB.start
227
257
  end
228
258
 
259
+ def run_delete(key)
260
+ provider_instance.delete(key)
261
+ end
262
+
263
+ def run_fetch(key)
264
+ value = provider_instance.fetch(key)
265
+ puts value if value
266
+ end
267
+
268
+ def run_set(key, value)
269
+ provider_instance.set(key, value)
270
+ end
271
+
229
272
  def current_values
230
273
  @current_values ||= Utils.flatten(fetch_config(path, filtered: false), path)
231
274
  end
@@ -247,9 +290,13 @@ module SecretConfig
247
290
  next if value.nil?
248
291
  next if current_values[key].to_s == value.to_s
249
292
 
250
- if value.to_s.strip == '$random'
293
+ if value.to_s.strip == RANDOM
251
294
  next if current_values[key]
295
+
252
296
  value = random_password
297
+ elsif value == FILTERED
298
+ # Ignore filtered values
299
+ next
253
300
  end
254
301
  puts "Setting: #{key}"
255
302
  provider_instance.set(key, value)
@@ -258,7 +305,7 @@ module SecretConfig
258
305
 
259
306
  def fetch_config(path, filtered: true)
260
307
  registry = Registry.new(path: path, provider: provider_instance)
261
- config = filtered ? registry.configuration : registry.configuration(filters: nil)
308
+ config = filtered ? registry.configuration : registry.configuration(filters: nil)
262
309
  sort_hash_by_key!(config)
263
310
  end
264
311
 
@@ -274,7 +321,7 @@ module SecretConfig
274
321
  output_path = ::File.dirname(file_name_or_io)
275
322
  FileUtils.mkdir_p(output_path) unless ::File.exist?(output_path)
276
323
 
277
- ::File.open(file_name_or_io, 'w') { |io| io.write(data) }
324
+ ::File.open(file_name_or_io, "w") { |io| io.write(data) }
278
325
  end
279
326
 
280
327
  def render(hash, format)
@@ -292,7 +339,7 @@ module SecretConfig
292
339
  config =
293
340
  case format
294
341
  when :yml
295
- YAML.load(ERB.new(data).result)
342
+ YAML.safe_load(ERB.new(data).result)
296
343
  when :json
297
344
  JSON.parse(data)
298
345
  else
@@ -305,9 +352,9 @@ module SecretConfig
305
352
  return :yml unless file_name.is_a?(String)
306
353
 
307
354
  case File.extname(file_name).downcase
308
- when '.yml', '.yaml'
355
+ when ".yml", ".yaml"
309
356
  :yml
310
- when '.json'
357
+ when ".json"
311
358
  :json
312
359
  else
313
360
  raise ArgumentError, "Import/Export file name must end with '.yml' or '.json'"
@@ -325,6 +372,5 @@ module SecretConfig
325
372
  end
326
373
  h
327
374
  end
328
-
329
375
  end
330
376
  end
@@ -1,5 +1,5 @@
1
- require 'yaml'
2
- require 'erb'
1
+ require "yaml"
2
+ require "erb"
3
3
 
4
4
  module SecretConfig
5
5
  module Providers
@@ -13,12 +13,12 @@ module SecretConfig
13
13
  end
14
14
 
15
15
  def each(path, &block)
16
- config = YAML.load(ERB.new(::File.new(file_name).read).result)
16
+ config = YAML.safe_load(ERB.new(::File.new(file_name).read).result)
17
17
 
18
- paths = path.sub(/\A\/*/, '').sub(/\/*\Z/, '').split("/")
18
+ paths = path.sub(%r{\A/*}, "").sub(%r{/*\Z}, "").split("/")
19
19
  settings = config.dig(*paths)
20
20
 
21
- raise(ConfigurationError, "Path #{paths.join("/")} not found in file: #{file_name}") unless settings
21
+ raise(ConfigurationError, "Path #{paths.join('/')} not found in file: #{file_name}") unless settings
22
22
 
23
23
  Utils.flatten_each(settings, path, &block)
24
24
  nil
@@ -2,11 +2,19 @@ module SecretConfig
2
2
  module Providers
3
3
  # Abstract Base provider
4
4
  class Provider
5
- def set(key, value)
5
+ def delete(_key)
6
6
  raise NotImplementedError
7
7
  end
8
8
 
9
- def delete(key)
9
+ def each(_path)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def fetch(_key)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def set(_key, _value)
10
18
  raise NotImplementedError
11
19
  end
12
20
 
@@ -1,30 +1,55 @@
1
1
  begin
2
- require 'aws-sdk-ssm'
3
- rescue LoadError => exc
4
- raise(LoadError, "Install gem 'aws-sdk-ssm' to use AWS Parameter Store: #{exc.message}")
2
+ require "aws-sdk-ssm"
3
+ rescue LoadError => e
4
+ raise(LoadError, "Install gem 'aws-sdk-ssm' to use AWS Parameter Store: #{e.message}")
5
5
  end
6
6
 
7
7
  module SecretConfig
8
8
  module Providers
9
9
  # Use the AWS System Manager Parameter Store for Centralized Configuration / Secrets Management
10
10
  class Ssm < Provider
11
- attr_reader :client, :key_id
11
+ attr_reader :client, :key_id, :retry_count, :retry_max_ms, :logger
12
12
 
13
- def initialize(key_id: ENV["AWS_ACCESS_KEY_ID"])
14
- @key_id = key_id
15
- logger = SemanticLogger['Aws::SSM'] if defined?(SemanticLogger)
16
- @client = Aws::SSM::Client.new(logger: logger)
13
+ def initialize(key_id: ENV["AWS_ACCESS_KEY_ID"], key_alias: ENV["AWS_ACCESS_KEY_ALIAS"], retry_count: 10, retry_max_ms: 3_000)
14
+ @key_id =
15
+ if key_alias
16
+ key_alias =~ %r{^alias/} ? key_alias : "alias/#{key_alias}"
17
+ else
18
+ key_id
19
+ end
20
+ @retry_count = retry_count
21
+ @retry_max_ms = retry_max_ms
22
+ @logger = SemanticLogger["Aws::SSM"] if defined?(SemanticLogger)
23
+ @client = Aws::SSM::Client.new(logger: logger)
17
24
  end
18
25
 
19
26
  def each(path)
20
- token = nil
27
+ retries = 0
28
+ token = nil
21
29
  loop do
22
- resp = client.get_parameters_by_path(
23
- path: path,
24
- recursive: true,
25
- with_decryption: true,
26
- next_token: token
27
- )
30
+ begin
31
+ resp = client.get_parameters_by_path(
32
+ path: path,
33
+ recursive: true,
34
+ with_decryption: true,
35
+ next_token: token
36
+ )
37
+ rescue Aws::SSM::Errors::ThrottlingException => e
38
+ # The free tier allows 40 calls per second.
39
+ # The Higher Throughput tier for additional cost is still limited to 100 calls per second.
40
+ # Using a random formula since this limit is normally only exceeded during a high volume restart period
41
+ # so we want to spread out the retries of the multiple servers.
42
+ retries += 1
43
+ if retry_limit > retries
44
+ sleep_seconds = rand(retry_max_ms) / 1000.0
45
+ logger&.info("SSM Parameter Store GetParametersByPath API Requests throttle exceeded, retry: #{retries}, sleeping #{sleep_seconds} seconds.")
46
+ sleep(sleep_interval)
47
+ retry
48
+ end
49
+ logger&.info("SSM Parameter Store GetParametersByPath API Requests throttle exceeded, retries exhausted.")
50
+ raise(e)
51
+ end
52
+
28
53
  resp.parameters.each { |param| yield(param.name, param.value) }
29
54
  token = resp.next_token
30
55
  break if token.nil?
@@ -41,8 +66,17 @@ module SecretConfig
41
66
  )
42
67
  end
43
68
 
69
+ # Deletes the key.
70
+ # Nothing is done if the key was not found.
44
71
  def delete(key)
45
72
  client.delete_parameter(name: key)
73
+ rescue Aws::SSM::Errors::ParameterNotFound
74
+ end
75
+
76
+ # Returns the value or `nil` if not found
77
+ def fetch(key)
78
+ client.get_parameter(name: key, with_decryption: true).parameter.value
79
+ rescue Aws::SSM::Errors::ParameterNotFound
46
80
  end
47
81
  end
48
82
  end
@@ -1,5 +1,5 @@
1
- require 'base64'
2
- require 'concurrent-ruby'
1
+ require "base64"
2
+ require "concurrent-ruby"
3
3
 
4
4
  module SecretConfig
5
5
  # Centralized configuration with values stored in AWS System Manager Parameter Store
@@ -9,7 +9,7 @@ module SecretConfig
9
9
 
10
10
  def initialize(path: nil, provider: nil, provider_args: nil)
11
11
  @path = default_path(path)
12
- raise(UndefinedRootError, 'Root must start with /') unless @path.start_with?('/')
12
+ raise(UndefinedRootError, "Root must start with /") unless @path.start_with?("/")
13
13
 
14
14
  resolved_provider = default_provider(provider)
15
15
  provider_args = nil if resolved_provider != provider
@@ -25,7 +25,7 @@ module SecretConfig
25
25
  cache.each_pair do |key, value|
26
26
  key = relative_key(key) if relative
27
27
  value = filter_value(key, value, filters)
28
- decompose(key, value, h)
28
+ Utils.decompose(key, value, h)
29
29
  end
30
30
  h
31
31
  end
@@ -95,48 +95,26 @@ module SecretConfig
95
95
  # Returns the value from an env var if it is present,
96
96
  # Otherwise the value is returned unchanged.
97
97
  def env_var_override(key, value)
98
- env_var_name = relative_key(key).upcase.gsub('/', '_')
98
+ env_var_name = relative_key(key).upcase.gsub("/", "_")
99
99
  ENV[env_var_name] || value
100
100
  end
101
101
 
102
102
  # Add the path to the path if it is a relative path.
103
103
  def expand_key(key)
104
- key.start_with?('/') ? key : "#{path}/#{key}"
104
+ key.start_with?("/") ? key : "#{path}/#{key}"
105
105
  end
106
106
 
107
107
  # Convert the key to a relative path by removing the path.
108
108
  def relative_key(key)
109
- key.start_with?('/') ? key.sub("#{path}/", '') : key
109
+ key.start_with?("/") ? key.sub("#{path}/", "") : key
110
110
  end
111
111
 
112
112
  def filter_value(key, value, filters)
113
113
  return value unless filters
114
114
 
115
- _, name = File.split(key)
116
- filter = filters.any? do |filter|
117
- filter.is_a?(Regexp) ? name =~ filter : name == filter
118
- end
119
-
120
- filter ? '[FILTERED]' : value
121
- end
122
-
123
- def decompose(key, value, h = {})
124
- path, name = File.split(key)
125
- if path == '.'
126
- h[key] = value
127
- return h
128
- end
129
- last = path.split('/').reduce(h) do |target, path|
130
- if path == ''
131
- target
132
- elsif target.key?(path)
133
- target[path]
134
- else
135
- target[path] = {}
136
- end
137
- end
138
- last[name] = value
139
- h
115
+ _, name = File.split(key)
116
+ filtered = filters.any? { |filter| filter.is_a?(Regexp) ? name =~ filter : name == filter }
117
+ filtered ? FILTERED : value
140
118
  end
141
119
 
142
120
  def convert_encoding(encoding, value)
@@ -159,7 +137,7 @@ module SecretConfig
159
137
  when :boolean
160
138
  %w[true 1 t].include?(value.to_s.downcase)
161
139
  when :symbol
162
- value.to_sym unless value.nil? || value.to_s.strip == ''
140
+ value.to_sym unless value.nil? || value.to_s.strip == ""
163
141
  end
164
142
  end
165
143
 
@@ -168,7 +146,7 @@ module SecretConfig
168
146
  return provider if provider.respond_to?(:each) && provider.respond_to?(:set)
169
147
 
170
148
  klass = Utils.constantize_symbol(provider)
171
- args && args.size > 0 ? klass.new(**args) : klass.new
149
+ args && !args.empty? ? klass.new(**args) : klass.new
172
150
  end
173
151
 
174
152
  def default_path(configured_path)
@@ -177,11 +155,11 @@ module SecretConfig
177
155
 
178
156
  raise(UndefinedRootError, "Either set env var 'SECRET_CONFIG_PATH' or call SecretConfig.use") unless path
179
157
 
180
- path.start_with?('/') ? path : "/#{path}"
158
+ path.start_with?("/") ? path : "/#{path}"
181
159
  end
182
160
 
183
161
  def default_provider(provider)
184
- provider = (ENV["SECRET_CONFIG_PROVIDER"] || provider || 'file')
162
+ provider = (ENV["SECRET_CONFIG_PROVIDER"] || provider || "file")
185
163
 
186
164
  return provider if provider.respond_to?(:each) && provider.respond_to?(:set)
187
165
 
@@ -4,8 +4,12 @@ module SecretConfig
4
4
  # If path is supplied it is prepended to every key returned.
5
5
  def self.flatten_each(hash, path = nil, &block)
6
6
  hash.each_pair do |key, value|
7
- name = path.nil? ? key : "#{path}/#{key}"
8
- value.is_a?(Hash) ? flatten_each(value, name, &block) : yield(name, value)
7
+ if key == NODE_KEY
8
+ yield(path, value)
9
+ else
10
+ name = path.nil? ? key : "#{path}/#{key}"
11
+ value.is_a?(Hash) ? flatten_each(value, name, &block) : yield(name, value)
12
+ end
9
13
  end
10
14
  end
11
15
 
@@ -17,7 +21,35 @@ module SecretConfig
17
21
  h
18
22
  end
19
23
 
20
- def self.constantize_symbol(symbol, namespace = 'SecretConfig::Providers')
24
+ # Takes a flat hash and expands the keys on each `/` into a deep hierarchy.
25
+ def self.hierarchical(flat_hash)
26
+ h = {}
27
+ flat_hash.each_pair { |path, value| decompose(path, value, h) }
28
+ h
29
+ end
30
+
31
+ def self.decompose(key, value, h = {})
32
+ full_path, name = File.split(key)
33
+ if full_path == "."
34
+ h[key] = value
35
+ return h
36
+ end
37
+ last = full_path.split("/").reduce(h) do |target, path|
38
+ if path == ""
39
+ target
40
+ elsif target.key?(path)
41
+ val = target[path]
42
+ val = target[path] = {NODE_KEY => val} unless val.is_a?(Hash)
43
+ val
44
+ else
45
+ target[path] = {}
46
+ end
47
+ end
48
+ last[name] = value
49
+ h
50
+ end
51
+
52
+ def self.constantize_symbol(symbol, namespace = "SecretConfig::Providers")
21
53
  klass = "#{namespace}::#{camelize(symbol.to_s)}"
22
54
  begin
23
55
  Object.const_get(klass)
@@ -30,8 +62,8 @@ module SecretConfig
30
62
  def self.camelize(term)
31
63
  string = term.to_s
32
64
  string = string.sub(/^[a-z\d]*/, &:capitalize)
33
- string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
34
- string.gsub!('/'.freeze, '::'.freeze)
65
+ string.gsub!(%r{(?:_|(/))([a-z\d]*)}i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
66
+ string.gsub!("/".freeze, "::".freeze)
35
67
  string
36
68
  end
37
69
  end
@@ -1,3 +1,3 @@
1
1
  module SecretConfig
2
- VERSION = '0.5.3'
2
+ VERSION = "0.6.0".freeze
3
3
  end
data/lib/secret_config.rb CHANGED
@@ -1,19 +1,24 @@
1
- require 'forwardable'
2
- require 'secret_config/version'
3
- require 'secret_config/errors'
4
- require 'secret_config/registry'
5
- require 'secret_config/railtie' if defined?(Rails)
1
+ require "forwardable"
2
+ require "secret_config/version"
3
+ require "secret_config/errors"
4
+ require "secret_config/registry"
5
+ require "secret_config/railtie" if defined?(Rails)
6
6
 
7
7
  # Centralized Configuration and Secrets Management for Ruby and Rails applications.
8
8
  module SecretConfig
9
+ # When a node is both a value and a hash/branch in the tree, put its value in its hash with the following key:
10
+ NODE_KEY = "__value__".freeze
11
+ FILTERED = "[FILTERED]".freeze
12
+ RANDOM = "$(random)".freeze
13
+
9
14
  module Providers
10
- autoload :File, 'secret_config/providers/file'
11
- autoload :Provider, 'secret_config/providers/provider'
12
- autoload :Ssm, 'secret_config/providers/ssm'
15
+ autoload :File, "secret_config/providers/file"
16
+ autoload :Provider, "secret_config/providers/provider"
17
+ autoload :Ssm, "secret_config/providers/ssm"
13
18
  end
14
19
 
15
- autoload :CLI, 'secret_config/cli'
16
- autoload :Utils, 'secret_config/utils'
20
+ autoload :CLI, "secret_config/cli"
21
+ autoload :Utils, "secret_config/utils"
17
22
 
18
23
  class << self
19
24
  extend Forwardable
@@ -62,5 +67,5 @@ module SecretConfig
62
67
  private
63
68
 
64
69
  @check_env_var = true
65
- @filters = [/password/, 'key', /secret_key/]
70
+ @filters = [/password/, "key", /secret_key/]
66
71
  end
@@ -40,11 +40,11 @@ class RegistryTest < Minitest::Test
40
40
  end
41
41
 
42
42
  it 'filters passwords' do
43
- assert_equal "[FILTERED]", registry.configuration.dig("mysql", "password")
43
+ assert_equal SecretConfig::FILTERED, registry.configuration.dig("mysql", "password")
44
44
  end
45
45
 
46
46
  it 'filters key' do
47
- assert_equal "[FILTERED]", registry.configuration.dig("symmetric_encryption", "key")
47
+ assert_equal SecretConfig::FILTERED, registry.configuration.dig("symmetric_encryption", "key")
48
48
  end
49
49
  end
50
50
 
@@ -118,22 +118,6 @@ class RegistryTest < Minitest::Test
118
118
  assert_equal "ABCDEF1234567890ABCDEF1234567890", registry.fetch("symmetric_encryption/key", encoding: :base64)
119
119
  end
120
120
  end
121
-
122
- private
123
-
124
- def decompose(key, value, h = {})
125
- path, name = File.split(key)
126
- last = path.split('/').reduce(h) do |target, path|
127
- if path == ''
128
- target
129
- elsif target.key?(path)
130
- target[path]
131
- else
132
- target[path] = {}
133
- end
134
- end
135
- last[name] = value
136
- h
137
- end
138
121
  end
139
122
  end
123
+
@@ -0,0 +1,27 @@
1
+ require_relative 'test_helper'
2
+
3
+ class UtilsTest < Minitest::Test
4
+ describe SecretConfig::Utils do
5
+ let :flat_registry do
6
+ {
7
+ "test/my_application/mysql/database" => "secret_config_test",
8
+ "test/my_application/mysql/password" => "secret_configrules",
9
+ "test/my_application/mysql/username" => "secret_config",
10
+ "test/my_application/mysql/host" => "127.0.0.1",
11
+ "test/my_application/secrets" => "both_a_path_and_a_value",
12
+ "test/my_application/secrets/secret_key_base" => "somereallylongteststring",
13
+ }
14
+ end
15
+
16
+ let :hash_registry do
17
+ SecretConfig::Utils.hierarchical(flat_registry)
18
+ end
19
+
20
+ describe '.flatten' do
21
+ it 'returns a copy of the config' do
22
+ h = SecretConfig::Utils.flatten(hash_registry, path = nil)
23
+ assert_equal(flat_registry, h)
24
+ end
25
+ end
26
+ end
27
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secret_config
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-10-02 00:00:00.000000000 Z
11
+ date: 2019-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -52,6 +52,7 @@ files:
52
52
  - test/registry_test.rb
53
53
  - test/secret_config_test.rb
54
54
  - test/test_helper.rb
55
+ - test/utils_test.rb
55
56
  homepage: https://github.com/rocketjob/secret_config
56
57
  licenses:
57
58
  - Apache-2.0
@@ -81,4 +82,5 @@ test_files:
81
82
  - test/providers/file_test.rb
82
83
  - test/registry_test.rb
83
84
  - test/test_helper.rb
85
+ - test/utils_test.rb
84
86
  - test/secret_config_test.rb