secret_config 0.3.1 → 0.4.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: 91d5d3ac77971215c58fd3738fab99f0a80df34535b31f46eb1f51e8b33f6a49
4
- data.tar.gz: 002a92d28752f9d7f6677c90233dd4362e84ff2dba961d42e3f12f3a8c6ad746
3
+ metadata.gz: 2d046c2ff5be65ccdd09c1fdb82fe28df303ec584ce89c4201320529d53025da
4
+ data.tar.gz: 5f4973a7b9ae821ac9b511569873dff21f427793d59e846999d419473efdc022
5
5
  SHA512:
6
- metadata.gz: 81a5075b68b5e13d2c541c03787e91691f4d3f41b63547e0d1240150bd187d50ef4f6ddaebf350cc157c0fa0c6297cf503e3b4c106397568934b8d8dcdf902a4
7
- data.tar.gz: 7cd829cc7ae114d96f96402956f178a40d9df0a66fecc948902bed60696968ed9da1c5cf4d6c4e35eccc711d4aa6487775e86daf430110df6b833b558275f19e
6
+ metadata.gz: 47788fa5c66536ba5bb36eed0ba86866375400375d0474e5b566da775db3cf7142682ac34075cb9a18c787bba0c31a892613db5283e48e47191ea98b0a5a97b7
7
+ data.tar.gz: f75ce0f2542a8dbde1e912a5a7bd1fec02e9b2c97fed42018dab8b7bbd84e9850a2c872aac8a0497f9bfea6042a566bc12f6345e3c26a84a5e1f8baf13513125
data/README.md CHANGED
@@ -78,11 +78,24 @@ to flatten everything into a single level.
78
78
 
79
79
  Note: Do not put any production credentials into this file.
80
80
 
81
- ### Usage
81
+ ### Environment Variables
82
+
83
+ Any of the above values can be overridden with an environment variable.
84
+
85
+ To overwrite any of these settings with an environment variable:
86
+
87
+ * Join the keys together with an '_'
88
+ * Convert to uppercase
89
+
90
+ For example, `mysql/host` can be overridden with the env var:
91
+
92
+ export MYSQL_HOST=test.server
93
+
94
+ ### Applying to existing config files
82
95
 
83
96
  Go through all the configuration files and look for sensitive data such as passwords:
84
97
 
85
- Example `database.yml`:
98
+ Example, an unchanged common `database.yml`:
86
99
 
87
100
  ~~~yaml
88
101
  defaults: &defaults
@@ -136,6 +149,45 @@ production:
136
149
 
137
150
  Since the secrets are externalized the configuration between environments is simpler.
138
151
 
152
+ ### Replacing custom config files
153
+
154
+ When writing new components or gems, instead of requiring a proprietary config file, refer
155
+ to the settings programmatically:
156
+
157
+ For example, somewhere in your codebase you need a persistent http connection:
158
+
159
+ ~~~ruby
160
+ def http_client
161
+ @http_client ||=
162
+ PersistentHTTP.new(
163
+ name: 'HTTPClient',
164
+ url: SecretConfig.fetch('http_client/url'),
165
+ logger: logger,
166
+ pool_size: SecretConfig.fetch('http_client/pool_size', type: :integer, default: 10),
167
+ warn_timeout: SecretConfig.fetch('http_client/warn_timeout', type: :float, default: 0.25),
168
+ open_timeout: SecretConfig.fetch('http_client/open_timeout', type: :float, default: 30),
169
+ read_timeout: SecretConfig.fetch('http_client/read_timeout', type: :float, default: 30),
170
+ force_retry: true
171
+ )
172
+ end
173
+ ~~~
174
+
175
+ Then the application that uses the above library / gem just needs to add the relevant entries to their
176
+ `application.rb` file:
177
+
178
+ ~~~yaml
179
+ http_client:
180
+ url: https://test.example.com
181
+ pool_size: 20
182
+ open_timeout: secret_configrules
183
+ ~~~
184
+
185
+ This avoids a custom config file just for the above library.
186
+
187
+ Additionally the values can be overridden with environment variables at any time:
188
+
189
+ export HTTP_CLIENT_URL=https://production.example.com
190
+
139
191
  ## Configuration
140
192
 
141
193
  Add the following line to Gemfile
@@ -143,7 +195,7 @@ Add the following line to Gemfile
143
195
  gem "secret_config"
144
196
 
145
197
  Out of the box Secret Config will look in the local file system for the file `config/application.yml`
146
- as covered above. By default it will use env var `RAILS_ENV` to define the root path to look under for settings.
198
+ as covered above. By default it will use env var `RAILS_ENV` to define the path to look under for settings.
147
199
 
148
200
  The default settings are great for getting started in development and test, but should not be used in production.
149
201
 
@@ -153,11 +205,11 @@ AWS System Manager Parameter Store:
153
205
  ~~~ruby
154
206
  Rails.application.configure do
155
207
  # Read configuration from AWS Parameter Store
156
- config.secret_config.use :ssm, root: '/production/my_application'
208
+ config.secret_config.use :ssm, path: '/production/my_application'
157
209
  end
158
210
  ~~~
159
211
 
160
- `root` is the path from which the configuration data will be read. This path uniquely identifies the
212
+ `path` is the path from which the configuration data will be read. This path uniquely identifies the
161
213
  configuration for this instance of the application.
162
214
 
163
215
  If we need 2 completely separate instances of the application running in a single AWS account then we could use
@@ -169,8 +221,8 @@ multiple paths. For example:
169
221
  /production/instance1/my_application
170
222
  /production/instance2/my_application
171
223
 
172
- The root path is completely flexible, but must be unique for every AWS account under which the application will run.
173
- The same root path can be used in different AWS accounts though. It is also not replicated across regions.
224
+ The `path` is completely flexible, but must be unique for every AWS account under which the application will run.
225
+ The same `path` can be used in different AWS accounts though. It is also not replicated across regions.
174
226
 
175
227
  When writing settings to the parameter store, it is recommended to use a custom KMS key to encrypt the values.
176
228
  To supply the key to encrypt the values with, add the `key_id` parameter:
@@ -180,7 +232,7 @@ Rails.application.configure do
180
232
  # Read configuration from AWS Parameter Store
181
233
  config.secret_config.use :ssm,
182
234
  key_id: 'alias/production/myapplication',
183
- root: '/production/my_application'
235
+ path: '/production/my_application'
184
236
  end
185
237
  ~~~
186
238
 
@@ -191,7 +243,7 @@ will only read from the parameter store.
191
243
 
192
244
  ### Authorization
193
245
 
194
- The following policy needs to be added to the AMI Group under which the application will be running:
246
+ The following policy needs to be added to the IAM Group under which the application will be running:
195
247
 
196
248
  ~~~json
197
249
  {
@@ -219,6 +271,56 @@ to view and modify parameters:
219
271
  - `ssm:GetParameters`
220
272
  - `ssm:GetParameter`
221
273
 
274
+ ## Command Line Interface
275
+
276
+ Secret Config has a command line interface for exporting, importing and copying between paths in the registry.
277
+
278
+ ~~~
279
+ secret_config [options]
280
+ -e, --export [FILE_NAME] Export configuration to a file or stdout if no file_name supplied.
281
+ -i, --import [FILE_NAME] Import configuration from a file or stdin if no file_name supplied.
282
+ -c, --copy SOURCE_PATH Import configuration from a file or stdin if no file_name supplied.
283
+ -p, --path PATH Path to import from / export to.
284
+ -P, --provider PROVIDER Provider to use. [ssm | file]. Default: ssm
285
+ -U, --no-filter Do not filter passwords and keys.
286
+ -k, --key KEY_ID | KEY_ALIAS AWS KMS Key id or Key Alias to use when importing configuration values. Default: AWS Default key.
287
+ -r, --region REGION AWS Region to use. Default: AWS_REGION env var.
288
+ -R, --random_size INTEGER Size to use when generating random values. Whenever $random is encountered during an import. Default: 32
289
+ -v, --version Display Symmetric Encryption version.
290
+ -h, --help Prints this help.
291
+ ~~~
292
+
293
+ ### CLI Examples
294
+
295
+ Export from a path in AWS SSM Parameter Store to a yaml file, where passwords are filtered:
296
+
297
+ secret_config --export test.yml --path /test/my_application
298
+
299
+ Export from a path in AWS SSM Parameter Store to a yaml file, _without_ filtering out passwords:
300
+
301
+ secret_config --export test.yml --path /test/my_application --no-filter
302
+
303
+ Export from a path in AWS SSM Parameter Store to a json file, where passwords are filtered:
304
+
305
+ secret_config --export test.json --path /test/my_application
306
+
307
+ Import a yaml file, into a path in AWS SSM Parameter Store:
308
+
309
+ secret_config --import test.yml --path /production/my_application
310
+
311
+ Import a yaml file, into a path in AWS SSM Parameter Store, using a custom KMS key to encrypt the values:
312
+
313
+ secret_config --import test.yml --path /production/my_application --key_id "arn:aws:kms:us-east-1:23643632463:key/UUID"
314
+
315
+ Copy configuration from one path in AWS SSM Parameter Store to another path in AWS SSM Parameter Store:
316
+
317
+ secret_config --copy /test/my_application --path /production/my_application
318
+
319
+ During an `import` or `copy` if any of the source values consist only of `$random`,
320
+ they will be replaced with a secure 32 byte random value.
321
+ This is deal for when a secure random password needs to be generated.
322
+ Use `--random_size` to adjust the length of the randomized string.
323
+
222
324
  ## Versioning
223
325
 
224
326
  This project adheres to [Semantic Versioning](http://semver.org/).
data/bin/secret_config ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
4
+
5
+ require 'secret_config'
6
+
7
+ SecretConfig::CLI.run!(ARGV)
@@ -0,0 +1,232 @@
1
+ require 'optparse'
2
+ require 'fileutils'
3
+ require 'erb'
4
+ require 'yaml'
5
+ require 'json'
6
+ require 'securerandom'
7
+
8
+ module SecretConfig
9
+ class CLI
10
+ attr_reader :path, :region, :provider,
11
+ :export, :no_filter,
12
+ :import, :key_id, :random_size, :prune, :overwrite,
13
+ :copy_path,
14
+ :show_version
15
+
16
+ PROVIDERS = %i[ssm].freeze
17
+
18
+ def self.run!(argv)
19
+ new(argv).run!
20
+ end
21
+
22
+ def initialize(argv)
23
+ @export = false
24
+ @import = false
25
+ @path = nil
26
+ @key_id = nil
27
+ @region = ENV['AWS_REGION']
28
+ @provider = :ssm
29
+ @random_size = 32
30
+ @no_filter = false
31
+ @prune = false
32
+ @replace = false
33
+ @copy_path = nil
34
+ @show_version = false
35
+
36
+ if argv.empty?
37
+ puts parser
38
+ exit(-10)
39
+ end
40
+ parser.parse!(argv)
41
+ end
42
+
43
+ def run!
44
+ if show_version
45
+ puts "Secret Config v#{VERSION}"
46
+ puts "Region: #{region}"
47
+ elsif export
48
+ run_export(export, filtered: !no_filter)
49
+ elsif import
50
+ run_import(import)
51
+ elsif copy_path
52
+ run_copy(copy_path, path)
53
+ else
54
+ puts parser
55
+ end
56
+ end
57
+
58
+ def parser
59
+ @parser ||= OptionParser.new do |opts|
60
+ opts.banner = <<~BANNER
61
+ Secret Config v#{VERSION}
62
+
63
+ For more information, see: https://rocketjob.github.io/secret_config/
64
+
65
+ secret_config [options]
66
+ BANNER
67
+
68
+ opts.on '-e', '--export [FILE_NAME]', 'Export configuration to a file or stdout if no file_name supplied.' do |file_name|
69
+ @export = file_name || STDOUT
70
+ end
71
+
72
+ opts.on '-i', '--import [FILE_NAME]', 'Import configuration from a file or stdin if no file_name supplied.' do |file_name|
73
+ @import = file_name || STDIN
74
+ end
75
+
76
+ opts.on '-c', '--copy SOURCE_PATH', 'Import configuration from a file or stdin if no file_name supplied.' do |path|
77
+ @copy_path = path
78
+ end
79
+
80
+ opts.on '-p', '--path PATH', 'Path to import from / export to.' do |path|
81
+ @path = path
82
+ end
83
+
84
+ opts.on '-P', '--provider PROVIDER', 'Provider to use. [ssm | file]. Default: ssm' do |provider|
85
+ @provider = provider.to_sym
86
+ end
87
+
88
+ opts.on '-U', '--no-filter', 'Do not filter passwords and keys.' do
89
+ @no_filter = true
90
+ end
91
+
92
+ # TODO:
93
+ # opts.on '-Q', '--prune', 'During import delete all existing keys for which their is no key in the import file' do
94
+ # @prune = true
95
+ # end
96
+ #
97
+ # opts.on '-r', '--replace', 'During import replace existing keys if present' do
98
+ # @replace = true
99
+ # end
100
+
101
+ opts.on '-k', '--key_id KEY_ID', 'AWS KMS Key id or Key Alias to use when importing configuration values. Default: AWS Default key.' do |key_id|
102
+ @key_id = key_id
103
+ end
104
+
105
+ opts.on '-r', '--region REGION', 'AWS Region to use. Default: AWS_REGION env var.' do |region|
106
+ @region = region
107
+ end
108
+
109
+ opts.on '-R', '--random_size INTEGER', 'Size to use when generating random values. Whenever $random is encountered during an import. Default: 32' do |region|
110
+ @random_size = random_size
111
+ end
112
+
113
+ opts.on '-v', '--version', 'Display Symmetric Encryption version.' do
114
+ @show_version = true
115
+ end
116
+
117
+ opts.on('-h', '--help', 'Prints this help.') do
118
+ puts opts
119
+ exit
120
+ end
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def provider_instance
127
+ @provider_instance ||= begin
128
+ case provider
129
+ when :ssm
130
+ Providers::Ssm.new(key_id: key_id)
131
+ else
132
+ raise ArgumentError, "Invalid provider: #{provider}"
133
+ end
134
+ end
135
+ end
136
+
137
+ def run_export(file_name, filtered: true)
138
+ config = fetch_config(path, filtered: filtered)
139
+
140
+ format = file_format(file_name)
141
+ data = render(config, format)
142
+ write_file(file_name, data)
143
+
144
+ puts("Exported #{path} from #{provider} to #{file_name}") if file_name.is_a?(String)
145
+ end
146
+
147
+ def run_import(file_name)
148
+ format = file_format(file_name)
149
+ data = read_file(file_name)
150
+ config = parse(data, format)
151
+ set_config(config, path)
152
+
153
+ puts("Imported #{file_name} to #{provider} at #{path}") if file_name.is_a?(String)
154
+ end
155
+
156
+ def run_copy(source_path, target_path)
157
+ config = fetch_config(source_path, filtered: false)
158
+
159
+ set_config(config, target_path)
160
+
161
+ puts "Copied #{source_path} to #{target_path} using #{provider}"
162
+ end
163
+
164
+ def set_config(config, path)
165
+ # TODO: prune, replace
166
+ Utils.flatten_each(config, path) do |key, value|
167
+ value = random_password if value.strip == '$random'
168
+ puts "Setting: #{key}"
169
+ provider_instance.set(key, value)
170
+ end
171
+ end
172
+
173
+ def fetch_config(path, filtered: true)
174
+ registry = Registry.new(path: path, provider: provider_instance)
175
+ filtered ? registry.configuration : registry.configuration(filters: nil)
176
+ end
177
+
178
+ def read_file(file_name_or_io)
179
+ return file_name_or_io.read unless file_name_or_io.is_a?(String)
180
+
181
+ ::File.new(file_name_or_io).read
182
+ end
183
+
184
+ def write_file(file_name_or_io, data)
185
+ return file_name_or_io.write(data) unless file_name_or_io.is_a?(String)
186
+
187
+ output_path = ::File.dirname(file_name_or_io)
188
+ FileUtils.mkdir_p(output_path) unless ::File.exist?(output_path)
189
+
190
+ ::File.open(file_name_or_io, 'w') { |io| io.write(data) }
191
+ end
192
+
193
+ def render(hash, format)
194
+ case format
195
+ when :yml
196
+ hash.to_yaml
197
+ when :json
198
+ hash.to_json
199
+ else
200
+ raise ArgumentError, "Invalid format: #{format.inspect}"
201
+ end
202
+ end
203
+
204
+ def parse(data, format)
205
+ case format
206
+ when :yml
207
+ YAML.load(ERB.new(data).result)
208
+ when :json
209
+ JSON.parse(data)
210
+ else
211
+ raise ArgumentError, "Invalid format: #{format.inspect}"
212
+ end
213
+ end
214
+
215
+ def file_format(file_name)
216
+ return :yml unless file_name.is_a?(String)
217
+
218
+ case File.extname(file_name).downcase
219
+ when '.yml', '.yaml'
220
+ :yml
221
+ when '.json'
222
+ :json
223
+ else
224
+ raise ArgumentError, "Import/Export file name must end with '.yml' or '.json'"
225
+ end
226
+ end
227
+
228
+ def random_password
229
+ SecureRandom.urlsafe_base64(random_size)
230
+ end
231
+ end
232
+ end
@@ -20,7 +20,7 @@ module SecretConfig
20
20
 
21
21
  raise(ConfigError, "Path #{paths.join(".")} not found in file: #{file_name}") unless settings
22
22
 
23
- flatten_each(path, settings, &block)
23
+ Utils.flatten_each(settings, path, &block)
24
24
  nil
25
25
  end
26
26
 
@@ -28,18 +28,6 @@ module SecretConfig
28
28
  raise NotImplementedError
29
29
  end
30
30
 
31
- private
32
-
33
- def flatten_each(path, hash, &block)
34
- hash.each_pair do |key, value|
35
- if value.is_a?(Hash)
36
- flatten_each("#{path}/#{key}", value, &block)
37
- else
38
- key = "#{path}/#{key}" unless key.start_with?('/')
39
- yield(key, value)
40
- end
41
- end
42
- end
43
31
  end
44
32
  end
45
33
  end
@@ -5,7 +5,7 @@ module SecretConfig
5
5
  # @example Set up configuration in the Rails app.
6
6
  # module MyApplication
7
7
  # class Application < Rails::Application
8
- # config.secret_config.use :file, root: '/development'
8
+ # config.secret_config.use :file, path: '/development'
9
9
  # end
10
10
  # end
11
11
  config.secret_config = SecretConfig
@@ -5,20 +5,21 @@ module SecretConfig
5
5
  # Centralized configuration with values stored in AWS System Manager Parameter Store
6
6
  class Registry
7
7
  attr_reader :provider
8
- attr_accessor :root
8
+ attr_accessor :path
9
9
 
10
- def initialize(root:, provider:)
11
- # TODO: Validate root starts with /, etc
12
- @root = root
13
- @provider = provider
14
- @registry = Concurrent::Map.new
10
+ def initialize(path: nil, provider: :file, provider_args: nil)
11
+ @path = path || default_path
12
+ raise(UndefinedRootError, 'Root must start with /') unless @path.start_with?('/')
13
+
14
+ @provider = create_provider(provider, provider_args)
15
+ @cache = Concurrent::Map.new
15
16
  refresh!
16
17
  end
17
18
 
18
19
  # Returns [Hash] a copy of the in memory configuration data.
19
20
  def configuration(relative: true, filters: SecretConfig.filters)
20
21
  h = {}
21
- registry.each_pair do |key, value|
22
+ cache.each_pair do |key, value|
22
23
  key = relative_key(key) if relative
23
24
  value = filter_value(key, value, filters)
24
25
  decompose(key, value, h)
@@ -28,19 +29,19 @@ module SecretConfig
28
29
 
29
30
  # Returns [String] configuration value for the supplied key, or nil when missing.
30
31
  def [](key)
31
- registry[expand_key(key)]
32
+ cache[expand_key(key)]
32
33
  end
33
34
 
34
35
  # Returns [String] configuration value for the supplied key, or nil when missing.
35
36
  def key?(key)
36
- registry.key?(expand_key(key))
37
+ cache.key?(expand_key(key))
37
38
  end
38
39
 
39
40
  # Returns [String] configuration value for the supplied key
40
41
  def fetch(key, default: nil, type: :string, encoding: nil)
41
42
  value = self[key]
42
43
  if value.nil?
43
- raise(MissingMandatoryKey, "Missing configuration value for #{root}/#{key}") unless default
44
+ raise(MissingMandatoryKey, "Missing configuration value for #{path}/#{key}") unless default
44
45
 
45
46
  value = default.respond_to?(:call) ? default.call : default
46
47
  end
@@ -53,28 +54,28 @@ module SecretConfig
53
54
  def set(key:, value:, encrypt: true)
54
55
  key = expand_key(key)
55
56
  provider.set(key, value, encrypt: true)
56
- registry[key] = value
57
+ cache[key] = value
57
58
  end
58
59
 
59
60
  # Refresh the in-memory cached copy of the centralized configuration information.
60
61
  # Environment variable values will take precendence over the central store values.
61
62
  def refresh!
62
- existing_keys = registry.keys
63
+ existing_keys = cache.keys
63
64
  updated_keys = []
64
- provider.each(root) do |key, value|
65
- registry[key] = env_var_override(key, value)
65
+ provider.each(path) do |key, value|
66
+ cache[key] = env_var_override(key, value)
66
67
  updated_keys << key
67
68
  end
68
69
 
69
- # Remove keys deleted from the registry.
70
- (existing_keys - updated_keys).each { |key| registry.delete(key) }
70
+ # Remove keys deleted from the central registry.
71
+ (existing_keys - updated_keys).each { |key| provider.delete(key) }
71
72
 
72
73
  true
73
74
  end
74
75
 
75
76
  private
76
77
 
77
- attr_reader :registry
78
+ attr_reader :cache
78
79
 
79
80
  # Returns the value from an env var if it is present,
80
81
  # Otherwise the value is returned unchanged.
@@ -83,22 +84,21 @@ module SecretConfig
83
84
  ENV[env_var_name] || value
84
85
  end
85
86
 
86
- # Add the root to the path if it is a relative path.
87
+ # Add the path to the path if it is a relative path.
87
88
  def expand_key(key)
88
- key.start_with?('/') ? key : "#{root}/#{key}"
89
+ key.start_with?('/') ? key : "#{path}/#{key}"
89
90
  end
90
91
 
91
- # Convert the key to a relative path by removing the
92
- # root path.
92
+ # Convert the key to a relative path by removing the path.
93
93
  def relative_key(key)
94
- key.start_with?('/') ? key.sub("#{root}/", '') : key
94
+ key.start_with?('/') ? key.sub("#{path}/", '') : key
95
95
  end
96
96
 
97
97
  def filter_value(key, value, filters)
98
98
  return value unless filters
99
99
 
100
100
  _, name = File.split(key)
101
- filter = filters.any? do |filter|
101
+ filter = filters.any? do |filter|
102
102
  filter.is_a?(Regexp) ? name =~ filter : name == filter
103
103
  end
104
104
 
@@ -144,5 +144,21 @@ module SecretConfig
144
144
  end
145
145
  end
146
146
 
147
+ # Create a new provider instance unless it is alread a provider instance.
148
+ def create_provider(provider, args = nil)
149
+ return provider if provider.respond_to?(:each) && provider.respond_to?(:set)
150
+
151
+ klass = Utils.constantize_symbol(provider)
152
+ args && args.size > 0 ? klass.new(**args) : klass.new
153
+ end
154
+
155
+ def default_path
156
+ path = ENV["SECRET_CONFIG_PATH"] || ENV["RAILS_ENV"]
157
+ path = Rails.env if path.nil? && defined?(Rails) && Rails.respond_to?(:env)
158
+
159
+ raise(UndefinedRootError, "Either set env var 'SECRET_CONFIG_PATH' or call SecretConfig.use") unless path
160
+
161
+ path.start_with?('/') ? path : "/#{path}"
162
+ end
147
163
  end
148
164
  end
@@ -0,0 +1,30 @@
1
+ module SecretConfig
2
+ module Utils
3
+ # Takes a hierarchical structure and flattens it to a single level
4
+ # If path is supplied it is prepended to every key returned
5
+ def self.flatten_each(hash, path = nil, &block)
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)
9
+ end
10
+ end
11
+
12
+ def self.constantize_symbol(symbol, namespace = 'SecretConfig::Providers')
13
+ klass = "#{namespace}::#{camelize(symbol.to_s)}"
14
+ begin
15
+ Object.const_get(klass)
16
+ rescue NameError
17
+ raise(ArgumentError, "Could not convert symbol: #{symbol.inspect} to a class in: #{namespace}. Looking for: #{klass}")
18
+ end
19
+ end
20
+
21
+ # Borrow from Rails, when not running Rails
22
+ def self.camelize(term)
23
+ string = term.to_s
24
+ string = string.sub(/^[a-z\d]*/, &:capitalize)
25
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
26
+ string.gsub!('/'.freeze, '::'.freeze)
27
+ string
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module SecretConfig
2
- VERSION = '0.3.1'
2
+ VERSION = '0.4.0'
3
3
  end
data/lib/secret_config.rb CHANGED
@@ -11,6 +11,9 @@ module SecretConfig
11
11
  autoload :Ssm, 'secret_config/providers/ssm'
12
12
  end
13
13
 
14
+ autoload :CLI, 'secret_config/cli'
15
+ autoload :Utils, 'secret_config/utils'
16
+
14
17
  class << self
15
18
  extend Forwardable
16
19
 
@@ -25,33 +28,16 @@ module SecretConfig
25
28
  end
26
29
 
27
30
  # Which provider to use along with any arguments
28
- # The root will be overriden by env var `SECRET_CONFIG_ROOT` if present.
29
- def self.use(provider, root: nil, **args)
30
- @provider = create_provider(provider, args)
31
- @root = ENV["SECRET_CONFIG_ROOT"] || root
32
- @registry = nil if @registry
33
- end
34
-
35
- def self.root
36
- @root ||= begin
37
- root = ENV["SECRET_CONFIG_ROOT"] || ENV["RAILS_ENV"]
38
- root = Rails.env if root.nil? && defined?(Rails) && Rails.respond_to?(:env)
39
- raise(UndefinedRootError, "Either set env var 'SECRET_CONFIG_ROOT' or call SecretConfig.use") unless root
40
- root = "/#{root}" unless root.start_with?('/')
41
- root
42
- end
43
- end
44
-
45
- # Returns the current provider.
46
- # If `SecretConfig.use` was not called previously it automatically use the file based provider.
47
- def self.provider
48
- @provider ||= begin
49
- create_provider(:file)
50
- end
31
+ # The path will be overriden by env var `SECRET_CONFIG_PATH` if present.
32
+ def self.use(provider, path: nil, **args)
33
+ path ||= ENV["SECRET_CONFIG_PATH"]
34
+ @registry = SecretConfig::Registry.new(path: path, provider: provider, provider_args: args)
51
35
  end
52
36
 
37
+ # Returns the global registry.
38
+ # Unless `.use` was called above, it will default to a file provider.
53
39
  def self.registry
54
- @registry ||= SecretConfig::Registry.new(root: root, provider: provider)
40
+ @registry ||= SecretConfig::Registry.new
55
41
  end
56
42
 
57
43
  # Filters to apply when returning the configuration
@@ -77,34 +63,4 @@ module SecretConfig
77
63
 
78
64
  @check_env_var = true
79
65
  @filters = [/password/, 'key', /secret_key/]
80
-
81
- # Create a new provider instance unless it is alread a provider instance.
82
- def self.create_provider(provider, args = nil)
83
- return provider if provider.respond_to?(:each) && provider.respond_to?(:set)
84
-
85
- klass = constantize_symbol(provider)
86
- args && args.size > 0 ? klass.new(**args) : klass.new
87
- end
88
-
89
- def implementation
90
- @implementation ||= constantize_symbol(provider).new
91
- end
92
-
93
- def self.constantize_symbol(symbol, namespace = 'SecretConfig::Providers')
94
- klass = "#{namespace}::#{camelize(symbol.to_s)}"
95
- begin
96
- Object.const_get(klass)
97
- rescue NameError
98
- raise(ArgumentError, "Could not convert symbol: #{symbol.inspect} to a class in: #{namespace}. Looking for: #{klass}")
99
- end
100
- end
101
-
102
- # Borrow from Rails, when not running Rails
103
- def self.camelize(term)
104
- string = term.to_s
105
- string = string.sub(/^[a-z\d]*/, &:capitalize)
106
- string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
107
- string.gsub!('/'.freeze, '::'.freeze)
108
- string
109
- end
110
66
  end
@@ -2,7 +2,7 @@
2
2
  # These are for development and test only.
3
3
 
4
4
  #
5
- # Development - Local - Root: '/development/my_application'
5
+ # Development - Local - Root: '/test/my_application'
6
6
  #
7
7
  development:
8
8
  my_application:
@@ -7,23 +7,23 @@ module Providers
7
7
  File.join(File.dirname(__FILE__), '..', 'config', 'application.yml')
8
8
  end
9
9
 
10
- let :root do
11
- "/development/my_application"
10
+ let :path do
11
+ "/test/my_application"
12
12
  end
13
13
 
14
14
  let :expected do
15
15
  {
16
- "/development/my_application/mongo/database" => "secret_config_development",
17
- "/development/my_application/mongo/primary" => "127.0.0.1:27017",
18
- "/development/my_application/mongo/secondary" => "127.0.0.1:27018",
19
- "/development/my_application/mysql/database" => "secret_config_development",
20
- "/development/my_application/mysql/password" => "secret_configrules",
21
- "/development/my_application/mysql/username" => "secret_config",
22
- "/development/my_application/mysql/host" => "127.0.0.1",
23
- "/development/my_application/secrets/secret_key_base" => "somereallylongstring",
24
- "/development/my_application/symmetric_encryption/key" => "QUJDREVGMTIzNDU2Nzg5MEFCQ0RFRjEyMzQ1Njc4OTA=",
25
- "/development/my_application/symmetric_encryption/version" => 2,
26
- "/development/my_application/symmetric_encryption/iv" => "QUJDREVGMTIzNDU2Nzg5MA=="
16
+ "/test/my_application/mongo/database" => "secret_config_test",
17
+ "/test/my_application/mongo/primary" => "127.0.0.1:27017",
18
+ "/test/my_application/mongo/secondary" => "127.0.0.1:27018",
19
+ "/test/my_application/mysql/database" => "secret_config_test",
20
+ "/test/my_application/mysql/password" => "secret_configrules",
21
+ "/test/my_application/mysql/username" => "secret_config",
22
+ "/test/my_application/mysql/host" => "127.0.0.1",
23
+ "/test/my_application/secrets/secret_key_base" => "somereallylongteststring",
24
+ "/test/my_application/symmetric_encryption/key" => "QUJDREVGMTIzNDU2Nzg5MEFCQ0RFRjEyMzQ1Njc4OTA=",
25
+ "/test/my_application/symmetric_encryption/version" => 2,
26
+ "/test/my_application/symmetric_encryption/iv" => "QUJDREVGMTIzNDU2Nzg5MA=="
27
27
  }
28
28
  end
29
29
 
@@ -31,7 +31,7 @@ module Providers
31
31
  it 'file' do
32
32
  file_provider = SecretConfig::Providers::File.new(file_name: file_name)
33
33
  paths = {}
34
- file_provider.each(root) { |key, value| paths[key] = value }
34
+ file_provider.each(path) { |key, value| paths[key] = value }
35
35
 
36
36
  expected.each_pair do |key, value|
37
37
  assert_equal value, paths[key], "Path: #{key}"
@@ -7,23 +7,23 @@ module Providers
7
7
  File.join(File.dirname(__FILE__), '..', 'config', 'application.yml')
8
8
  end
9
9
 
10
- let :root do
11
- "/development/my_application"
10
+ let :path do
11
+ "/test/my_application"
12
12
  end
13
13
 
14
14
  let :expected do
15
15
  {
16
- "/development/my_application/mongo/database" => "secret_config_development",
17
- "/development/my_application/mongo/primary" => "127.0.0.1:27017",
18
- "/development/my_application/mongo/secondary" => "127.0.0.1:27018",
19
- "/development/my_application/mysql/database" => "secret_config_development",
20
- "/development/my_application/mysql/password" => "secret_configrules",
21
- "/development/my_application/mysql/username" => "secret_config",
22
- "/development/my_application/mysql/host" => "127.0.0.1",
23
- "/development/my_application/secrets/secret_key_base" => "somereallylongstring",
24
- "/development/my_application/symmetric_encryption/key" => "QUJDREVGMTIzNDU2Nzg5MEFCQ0RFRjEyMzQ1Njc4OTA=",
25
- "/development/my_application/symmetric_encryption/version" => "2",
26
- "/development/my_application/symmetric_encryption/iv" => "QUJDREVGMTIzNDU2Nzg5MA=="
16
+ "/test/my_application/mongo/database" => "secret_config_test",
17
+ "/test/my_application/mongo/primary" => "127.0.0.1:27017",
18
+ "/test/my_application/mongo/secondary" => "127.0.0.1:27018",
19
+ "/test/my_application/mysql/database" => "secret_config_test",
20
+ "/test/my_application/mysql/password" => "secret_configrules",
21
+ "/test/my_application/mysql/username" => "secret_config",
22
+ "/test/my_application/mysql/host" => "127.0.0.1",
23
+ "/test/my_application/secrets/secret_key_base" => "somereallylongteststring",
24
+ "/test/my_application/symmetric_encryption/key" => "QUJDREVGMTIzNDU2Nzg5MEFCQ0RFRjEyMzQ1Njc4OTA=",
25
+ "/test/my_application/symmetric_encryption/version" => "2",
26
+ "/test/my_application/symmetric_encryption/iv" => "QUJDREVGMTIzNDU2Nzg5MA=="
27
27
  }
28
28
  end
29
29
 
@@ -37,11 +37,11 @@ module Providers
37
37
  it 'fetches all keys in path' do
38
38
  ssm = SecretConfig::Providers::Ssm.new
39
39
  paths = {}
40
- ssm.each(root) { |key, value| paths[key] = value }
40
+ ssm.each(path) { |key, value| paths[key] = value }
41
41
 
42
42
  if paths.empty?
43
- upload_settings(ssm) unless ssm.key?("/development/my_application/mongo/database")
44
- ssm.each(root) { |key, value| paths[key] = value }
43
+ upload_settings(ssm) unless paths.key?("/test/my_application/mongo/database")
44
+ ssm.each(path) { |key, value| paths[key] = value }
45
45
  end
46
46
 
47
47
  expected.each_pair do |key, value|
@@ -52,7 +52,7 @@ module Providers
52
52
 
53
53
  def upload_settings(ssm)
54
54
  file_provider = SecretConfig::Providers::File.new(file_name: file_name)
55
- file_provider.each(root) { |key, value| ap key; ssm.set(key, value) }
55
+ file_provider.each(path) { |key, value| ap key; ssm.set(key, value) }
56
56
  end
57
57
  end
58
58
  end
@@ -6,8 +6,8 @@ class RegistryTest < Minitest::Test
6
6
  File.join(File.dirname(__FILE__), 'config', 'application.yml')
7
7
  end
8
8
 
9
- let :root do
10
- "/development/my_application"
9
+ let :path do
10
+ "/test/my_application"
11
11
  end
12
12
 
13
13
  let :provider do
@@ -15,22 +15,22 @@ class RegistryTest < Minitest::Test
15
15
  end
16
16
 
17
17
  let :registry do
18
- SecretConfig::Registry.new(root: root, provider: provider)
18
+ SecretConfig::Registry.new(path: path, provider: provider)
19
19
  end
20
20
 
21
21
  let :expected do
22
22
  {
23
- "/development/my_application/mongo/database" => "secret_config_development",
24
- "/development/my_application/mongo/primary" => "127.0.0.1:27017",
25
- "/development/my_application/mongo/secondary" => "127.0.0.1:27018",
26
- "/development/my_application/mysql/database" => "secret_config_development",
27
- "/development/my_application/mysql/password" => "secret_configrules",
28
- "/development/my_application/mysql/username" => "secret_config",
29
- "/development/my_application/mysql/host" => "127.0.0.1",
30
- "/development/my_application/secrets/secret_key_base" => "somereallylongstring",
31
- "/development/my_application/symmetric_encryption/key" => "QUJDREVGMTIzNDU2Nzg5MEFCQ0RFRjEyMzQ1Njc4OTA=",
32
- "/development/my_application/symmetric_encryption/version" => 2,
33
- "/development/my_application/symmetric_encryption/iv" => "QUJDREVGMTIzNDU2Nzg5MA=="
23
+ "/test/my_application/mongo/database" => "secret_config_test",
24
+ "/test/my_application/mongo/primary" => "127.0.0.1:27017",
25
+ "/test/my_application/mongo/secondary" => "127.0.0.1:27018",
26
+ "/test/my_application/mysql/database" => "secret_config_test",
27
+ "/test/my_application/mysql/password" => "secret_configrules",
28
+ "/test/my_application/mysql/username" => "secret_config",
29
+ "/test/my_application/mysql/host" => "127.0.0.1",
30
+ "/test/my_application/secrets/secret_key_base" => "somereallylongteststring",
31
+ "/test/my_application/symmetric_encryption/key" => "QUJDREVGMTIzNDU2Nzg5MEFCQ0RFRjEyMzQ1Njc4OTA=",
32
+ "/test/my_application/symmetric_encryption/version" => 2,
33
+ "/test/my_application/symmetric_encryption/iv" => "QUJDREVGMTIzNDU2Nzg5MA=="
34
34
  }
35
35
  end
36
36
 
@@ -51,7 +51,7 @@ class RegistryTest < Minitest::Test
51
51
  describe '#key?' do
52
52
  it 'has key' do
53
53
  expected.each_pair do |key, value|
54
- key = key.sub("#{root}/", "")
54
+ key = key.sub("#{path}/", "")
55
55
  assert registry.key?(key), "Path: #{key}"
56
56
  end
57
57
  end
@@ -61,14 +61,14 @@ class RegistryTest < Minitest::Test
61
61
  end
62
62
 
63
63
  it 'returns nil with missing full key' do
64
- refute registry.key?("/development/invalid/path")
64
+ refute registry.key?("/test/invalid/path")
65
65
  end
66
66
  end
67
67
 
68
68
  describe '#[]' do
69
69
  it 'returns values' do
70
70
  expected.each_pair do |key, value|
71
- key = key.sub("#{root}/", "")
71
+ key = key.sub("#{path}/", "")
72
72
  assert_equal value, registry[key], "Path: #{key}"
73
73
  end
74
74
  end
@@ -78,14 +78,14 @@ class RegistryTest < Minitest::Test
78
78
  end
79
79
 
80
80
  it 'returns nil with missing full key' do
81
- assert_nil registry["/development/invalid/path"]
81
+ assert_nil registry["/test/invalid/path"]
82
82
  end
83
83
  end
84
84
 
85
85
  describe '#fetch' do
86
86
  it 'returns values' do
87
87
  expected.each_pair do |key, value|
88
- key = key.sub("#{root}/", "")
88
+ key = key.sub("#{path}/", "")
89
89
  assert_equal value, registry.fetch(key), "Path: #{key}"
90
90
  end
91
91
  end
@@ -98,12 +98,12 @@ class RegistryTest < Minitest::Test
98
98
 
99
99
  it 'returns nil with missing full key' do
100
100
  assert_raises SecretConfig::MissingMandatoryKey do
101
- registry.fetch("/development/invalid/path")
101
+ registry.fetch("/test/invalid/path")
102
102
  end
103
103
  end
104
104
 
105
105
  it 'returns default with missing key' do
106
- assert_equal "default_value", registry.fetch("/development/invalid/path", default: "default_value")
106
+ assert_equal "default_value", registry.fetch("/test/invalid/path", default: "default_value")
107
107
  end
108
108
 
109
109
  it 'converts to integer' do
@@ -6,12 +6,12 @@ class SecretConfigTest < Minitest::Test
6
6
  File.join(File.dirname(__FILE__), 'config', 'application.yml')
7
7
  end
8
8
 
9
- let :root do
10
- "/development/my_application"
9
+ let :path do
10
+ "/test/my_application"
11
11
  end
12
12
 
13
13
  before do
14
- SecretConfig.use :file, root: root, file_name: file_name
14
+ SecretConfig.use :file, path: path, file_name: file_name
15
15
  end
16
16
 
17
17
  describe '#configuration' do
@@ -28,19 +28,24 @@ class SecretConfigTest < Minitest::Test
28
28
 
29
29
  describe '#[]' do
30
30
  it 'returns values' do
31
- assert_equal "secret_config_development", SecretConfig["mysql/database"]
31
+ assert_equal "secret_config_test", SecretConfig["mysql/database"]
32
32
  end
33
33
  end
34
34
 
35
35
  describe '#fetch' do
36
+ after do
37
+ ENV['MYSQL_DATABASE'] = nil
38
+ end
39
+
36
40
  it 'fetches values' do
37
- assert_equal "secret_config_development", SecretConfig.fetch("mysql/database")
41
+ assert_equal "secret_config_test", SecretConfig.fetch("mysql/database")
38
42
  end
39
43
 
40
44
  it 'can be overridden by an environment variable' do
41
45
  ENV['MYSQL_DATABASE'] = 'other'
46
+
47
+ SecretConfig.use :file, path: path, file_name: file_name
42
48
  assert_equal "other", SecretConfig.fetch("mysql/database")
43
- ENV['MYSQL_DATABASE'] = nil
44
49
  end
45
50
  end
46
51
  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.3.1
4
+ version: 0.4.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-04-17 00:00:00.000000000 Z
11
+ date: 2019-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -34,12 +34,15 @@ files:
34
34
  - LICENSE
35
35
  - README.md
36
36
  - Rakefile
37
+ - bin/secret_config
37
38
  - lib/secret_config.rb
39
+ - lib/secret_config/cli.rb
38
40
  - lib/secret_config/errors.rb
39
41
  - lib/secret_config/providers/file.rb
40
42
  - lib/secret_config/providers/ssm.rb
41
43
  - lib/secret_config/railtie.rb
42
44
  - lib/secret_config/registry.rb
45
+ - lib/secret_config/utils.rb
43
46
  - lib/secret_config/version.rb
44
47
  - test/config/application.yml
45
48
  - test/providers/file_test.rb