secret_config 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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