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 +4 -4
- data/lib/secret_config/cli.rb +86 -40
- data/lib/secret_config/providers/file.rb +5 -5
- data/lib/secret_config/providers/provider.rb +10 -2
- data/lib/secret_config/providers/ssm.rb +49 -15
- data/lib/secret_config/registry.rb +14 -36
- data/lib/secret_config/utils.rb +37 -5
- data/lib/secret_config/version.rb +1 -1
- data/lib/secret_config.rb +16 -11
- data/test/registry_test.rb +3 -19
- data/test/utils_test.rb +27 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d00a0e1e7ae87745d69e70f1517d150b3005b5c1598da329a0db829f8d9a770f
|
4
|
+
data.tar.gz: 87158952204f0aaf9a029f4df3b3c97e38c64fe7b4d46b62a32c4887ce483ed7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84a4820ed6012d08553cd6659b43efe10420fcf37ce00d7bc4ea93cf16e9e0055979971cec82d45b8f78ba4476c583d438564d4ad36a81ddb21800759a2126c5
|
7
|
+
data.tar.gz: 4b4d6a5d9598f9fe1c6db1f9158016124258da6d6d3eac17cde7dbcd5263efb7eec74615b22371b4f196416112e48a4e2b90d93ff460e73a17b3a3e37a79a079
|
data/lib/secret_config/cli.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
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
|
-
@
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
119
|
-
|
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
|
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
|
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
|
164
|
+
opts.on "-v", "--version", "Display Symmetric Encryption version." do
|
135
165
|
@show_version = true
|
136
166
|
end
|
137
167
|
|
138
|
-
opts.on(
|
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
|
-
|
212
|
-
|
213
|
-
|
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 ==
|
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
|
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,
|
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.
|
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
|
355
|
+
when ".yml", ".yaml"
|
309
356
|
:yml
|
310
|
-
when
|
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
|
2
|
-
require
|
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.
|
16
|
+
config = YAML.safe_load(ERB.new(::File.new(file_name).read).result)
|
17
17
|
|
18
|
-
paths = path.sub(
|
18
|
+
paths = path.sub(%r{\A/*}, "").sub(%r{/*\Z}, "").split("/")
|
19
19
|
settings = config.dig(*paths)
|
20
20
|
|
21
|
-
raise(ConfigurationError, "Path #{paths.join(
|
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
|
5
|
+
def delete(_key)
|
6
6
|
raise NotImplementedError
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
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
|
3
|
-
rescue LoadError =>
|
4
|
-
raise(LoadError, "Install gem 'aws-sdk-ssm' to use AWS Parameter Store: #{
|
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
|
15
|
-
|
16
|
-
|
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
|
-
|
27
|
+
retries = 0
|
28
|
+
token = nil
|
21
29
|
loop do
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
2
|
-
require
|
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,
|
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?(
|
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?(
|
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
|
116
|
-
|
117
|
-
|
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.
|
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?(
|
158
|
+
path.start_with?("/") ? path : "/#{path}"
|
181
159
|
end
|
182
160
|
|
183
161
|
def default_provider(provider)
|
184
|
-
provider = (ENV["SECRET_CONFIG_PROVIDER"] || provider ||
|
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
|
|
data/lib/secret_config/utils.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
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
|
-
|
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!(
|
34
|
-
string.gsub!(
|
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
|
data/lib/secret_config.rb
CHANGED
@@ -1,19 +1,24 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
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,
|
11
|
-
autoload :Provider,
|
12
|
-
autoload :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,
|
16
|
-
autoload :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/,
|
70
|
+
@filters = [/password/, "key", /secret_key/]
|
66
71
|
end
|
data/test/registry_test.rb
CHANGED
@@ -40,11 +40,11 @@ class RegistryTest < Minitest::Test
|
|
40
40
|
end
|
41
41
|
|
42
42
|
it 'filters passwords' do
|
43
|
-
assert_equal
|
43
|
+
assert_equal SecretConfig::FILTERED, registry.configuration.dig("mysql", "password")
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'filters key' do
|
47
|
-
assert_equal
|
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
|
+
|
data/test/utils_test.rb
ADDED
@@ -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.
|
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-
|
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
|