secret_config 0.2.0 → 0.3.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: ce78920783d826865d118ba93de56be3e428d766efd502070999f7a5b1ebb069
4
- data.tar.gz: 69bf56d09a5e20e3966531a9e75591c44376c6e5d0413d429a79dbfad11d4823
3
+ metadata.gz: 67325636265a59114b4ff33575572d931c337a0796def120ca68ef7ebfbd340e
4
+ data.tar.gz: 0b3280f1aab15a4301af120dcf1001757d998fc76b0141eebe66c778e8e6b665
5
5
  SHA512:
6
- metadata.gz: dee07a19a4728594c49f9c207d2006dde24ce2d87e25654c274efa11de4a9e0c274b9c66c6e31b4d8043c756fa007df8a2e5db84a189bf36b5ebeef2c596138c
7
- data.tar.gz: 3a1eb219305927565c9920943bbde78b34bb4a1ad2cc7c703cb53bef9a1c7a35a3b9d82736d7969af4c08562de85dc05bc4224f76d6c2f38474ea0d479297ec2
6
+ metadata.gz: f06a83adb1211924f4fca6e73976e8f23bf9bf2784da3bd98a6428327acccfa831b2dd9d1ab735f636e239f8db730210a0fd0d5c8370344145b86a22b47fe66e
7
+ data.tar.gz: 674f8ea7d0f0b7340d462f8d90992edd6c93fe14e2979ad6b347992a472ef2fae75e9ea184291281368e45c407e61c661b1b309b4877f168339eb22909836143
data/README.md CHANGED
@@ -136,6 +136,89 @@ production:
136
136
 
137
137
  Since the secrets are externalized the configuration between environments is simpler.
138
138
 
139
+ ## Configuration
140
+
141
+ Add the following line to Gemfile
142
+
143
+ gem "secret_config"
144
+
145
+ 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.
147
+
148
+ The default settings are great for getting started in development and test, but should not be used in production.
149
+
150
+ Add the setting to `config/environments/production.rb` to make it fetch its settings from
151
+ AWS System Manager Parameter Store:
152
+
153
+ ~~~ruby
154
+ Rails.application.configure do
155
+ # Read configuration from AWS Parameter Store
156
+ config.secret_config.use :ssm, root: '/production/my_application'
157
+ end
158
+ ~~~
159
+
160
+ `root` is the path from which the configuration data will be read. This path uniquely identifies the
161
+ configuration for this instance of the application.
162
+
163
+ If we need 2 completely separate instances of the application running in a single AWS account then we could use
164
+ multiple paths. For example:
165
+
166
+ /production1/my_application
167
+ /production2/my_application
168
+
169
+ /production/instance1/my_application
170
+ /production/instance2/my_application
171
+
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.
174
+
175
+ When writing settings to the parameter store, it is recommended to use a custom KMS key to encrypt the values.
176
+ To supply the key to encrypt the values with, add the `key_id` parameter:
177
+
178
+ ~~~ruby
179
+ Rails.application.configure do
180
+ # Read configuration from AWS Parameter Store
181
+ config.secret_config.use :ssm,
182
+ key_id: 'alias/production/myapplication',
183
+ root: '/production/my_application'
184
+ end
185
+ ~~~
186
+
187
+ Note: The relevant KMS key must be created first prior to using it here.
188
+
189
+ The `key_id` is only used when writing settings to the AWS Parameter store and can be left off when that instance
190
+ will only read from the parameter store.
191
+
192
+ ### Authorization
193
+
194
+ The following policy needs to be added to the AMI Group under which the application will be running:
195
+
196
+ ~~~json
197
+ {
198
+ "Version": "2012-10-17",
199
+ "Statement": [
200
+ {
201
+ "Sid": "VisualEditor0",
202
+ "Effect": "Allow",
203
+ "Action": [
204
+ "ssm:PutParameter",
205
+ "ssm:GetParametersByPath",
206
+ ],
207
+ "Resource": "*"
208
+ }
209
+ ]
210
+ }
211
+ ~~~
212
+
213
+ The above policy restricts read and write access to just the Parameter Store capabilities of AWS System Manager.
214
+
215
+ These additional Actions are not used by Secret Config, but may be useful for anyone using the AWS Console directly
216
+ to view and modify parameters:
217
+ - `ssm:DescribeParameters`
218
+ - `ssm:GetParameterHistory`
219
+ - `ssm:GetParameters`
220
+ - `ssm:GetParameter`
221
+
139
222
  ## Versioning
140
223
 
141
224
  This project adheres to [Semantic Versioning](http://semver.org/).
data/lib/secret_config.rb CHANGED
@@ -1,7 +1,8 @@
1
- require 'sync_attr'
1
+ require 'forwardable'
2
2
  require 'secret_config/version'
3
3
  require 'secret_config/errors'
4
4
  require 'secret_config/registry'
5
+ require 'secret_config/railtie' if defined?(Rails)
5
6
 
6
7
  # Centralized Configuration and Secrets Management for Ruby and Rails applications.
7
8
  module SecretConfig
@@ -23,45 +24,65 @@ module SecretConfig
23
24
  def_delegator :registry, :refresh!
24
25
  end
25
26
 
26
- def self.root
27
- @root ||= ENV["SECRET_CONFIG_ROOT"] ||
28
- raise(UndefinedRootError, "Either set env var 'SECRET_CONFIG_ROOT' or call SecretConfig.root=")
29
- end
30
-
31
- def self.root=(root)
32
- @root = root
27
+ # 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
33
32
  @registry = nil if @registry
34
33
  end
35
34
 
36
- # When provider is not supplied, returns the current provider instance
37
- # When provider is supplied, sets the new provider and stores any arguments
38
- def self.provider(provider = nil, **args)
39
- if provider.nil?
40
- return @provider ||= create_provider((ENV["SECRET_CONFIG_PROVIDER"] || :file).to_sym)
35
+ def self.root
36
+ @root ||= begin
37
+ root = ENV["SECRET_CONFIG_ROOT"] || ENV["RAILS_ENV"]
38
+ raise(UndefinedRootError, "Either set env var 'SECRET_CONFIG_ROOT' or call SecretConfig.use") unless root
39
+ root = "/#{root}" unless root.start_with?('/')
40
+ root
41
41
  end
42
-
43
- @provider = create_provider(provider, args)
44
- @registry = nil if @registry
45
42
  end
46
43
 
47
- def self.provider=(provider)
48
- @provider = provider
49
- @registry = nil if @registry
44
+ # Returns the current provider.
45
+ # If `SecretConfig.use` was not called previously it automatically use the file based provider.
46
+ def self.provider
47
+ @provider ||= begin
48
+ create_provider(:file)
49
+ end
50
50
  end
51
51
 
52
52
  def self.registry
53
53
  @registry ||= SecretConfig::Registry.new(root: root, provider: provider)
54
54
  end
55
55
 
56
+ # Filters to apply when returning the configuration
57
+ def self.filters
58
+ @filters
59
+ end
60
+
61
+ def self.filters=(filters)
62
+ @filters = filters
63
+ end
64
+
65
+ # Check the environment variables for a matching key and override the value returned from
66
+ # the central registry.
67
+ def self.check_env_var?
68
+ @check_env_var
69
+ end
70
+
71
+ def self.check_env_var=(check_env_var)
72
+ @check_env_var = check_env_var
73
+ end
74
+
56
75
  private
57
76
 
77
+ @check_env_var = true
78
+ @filters = [/password/, 'key', /secret_key/]
79
+
80
+ # Create a new provider instance unless it is alread a provider instance.
58
81
  def self.create_provider(provider, args = nil)
82
+ return provider if provider.respond_to?(:each) && provider.respond_to?(:set)
83
+
59
84
  klass = constantize_symbol(provider)
60
- if args && args.size > 0
61
- klass.new(**args)
62
- else
63
- klass.new
64
- end
85
+ args && args.size > 0 ? klass.new(**args) : klass.new
65
86
  end
66
87
 
67
88
  def implementation
@@ -0,0 +1,13 @@
1
+ module SecretConfig
2
+ class Railtie < Rails::Railtie
3
+ # Exposes Secret Config's configuration to the Rails application configuration.
4
+ #
5
+ # @example Set up configuration in the Rails app.
6
+ # module MyApplication
7
+ # class Application < Rails::Application
8
+ # config.secret_config.use :file, root: '/development'
9
+ # end
10
+ # end
11
+ config.secret_config = SecretConfig
12
+ end
13
+ end
@@ -1,4 +1,5 @@
1
1
  require 'base64'
2
+ require 'concurrent-ruby'
2
3
 
3
4
  module SecretConfig
4
5
  # Centralized configuration with values stored in AWS System Manager Parameter Store
@@ -10,13 +11,18 @@ module SecretConfig
10
11
  # TODO: Validate root starts with /, etc
11
12
  @root = root
12
13
  @provider = provider
14
+ @registry = Concurrent::Map.new
13
15
  refresh!
14
16
  end
15
17
 
16
18
  # Returns [Hash] a copy of the in memory configuration data.
17
- def configuration
19
+ def configuration(relative: true, filters: SecretConfig.filters)
18
20
  h = {}
19
- registry.each_pair { |k, v| decompose(k, v, h) }
21
+ registry.each_pair do |key, value|
22
+ key = relative_key(key) if relative
23
+ value = filter_value(key, value, filters)
24
+ decompose(key, value, h)
25
+ end
20
26
  h
21
27
  end
22
28
 
@@ -43,24 +49,62 @@ module SecretConfig
43
49
  type == :string ? value : convert_type(type, value)
44
50
  end
45
51
 
52
+ # Set the value for a key in the centralized configuration store.
46
53
  def set(key:, value:, encrypt: true)
47
- SSM.new(key_id: key_id).set(expand_key(key), value, encrypt: encrypt)
54
+ key = expand_key(key)
55
+ provider.set(key, value, encrypt: true)
56
+ registry[key] = value
48
57
  end
49
58
 
59
+ # Refresh the in-memory cached copy of the centralized configuration information.
60
+ # Environment variable values will take precendence over the central store values.
50
61
  def refresh!
51
- h = {}
52
- provider.each(root) { |k, v| h[k] = v }
53
- @registry = h
62
+ existing_keys = registry.keys
63
+ updated_keys = []
64
+ provider.each(root) do |key, value|
65
+ registry[key] = env_var_override(key, value)
66
+ updated_keys << key
67
+ end
68
+
69
+ # Remove keys deleted from the registry.
70
+ (existing_keys - updated_keys).each { |key| registry.delete(key) }
71
+
72
+ true
54
73
  end
55
74
 
56
75
  private
57
76
 
58
77
  attr_reader :registry
59
78
 
79
+ # Returns the value from an env var if it is present,
80
+ # Otherwise the value is returned unchanged.
81
+ def env_var_override(key, value)
82
+ env_var_name = relative_key(key).upcase.gsub('/', '_')
83
+ ENV[env_var_name] || value
84
+ end
85
+
86
+ # Add the root to the path if it is a relative path.
60
87
  def expand_key(key)
61
88
  key.start_with?('/') ? key : "#{root}/#{key}"
62
89
  end
63
90
 
91
+ # Convert the key to a relative path by removing the
92
+ # root path.
93
+ def relative_key(key)
94
+ key.start_with?('/') ? key.sub("#{root}/", '') : key
95
+ end
96
+
97
+ def filter_value(key, value, filters)
98
+ return value unless filters
99
+
100
+ _, name = File.split(key)
101
+ filter = filters.any? do |filter|
102
+ filter.is_a?(Regexp) ? name =~ filter : name == filter
103
+ end
104
+
105
+ filter ? '[FILTERED]' : value
106
+ end
107
+
64
108
  def decompose(key, value, h = {})
65
109
  path, name = File.split(key)
66
110
  last = path.split('/').reduce(h) do |target, path|
@@ -1,3 +1,3 @@
1
1
  module SecretConfig
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -36,7 +36,15 @@ class RegistryTest < Minitest::Test
36
36
 
37
37
  describe '#configuration' do
38
38
  it 'returns a copy of the config' do
39
- assert_equal "127.0.0.1", registry.configuration.dig("development", "my_application", "mysql", "host")
39
+ assert_equal "127.0.0.1", registry.configuration.dig("mysql", "host")
40
+ end
41
+
42
+ it 'filters passwords' do
43
+ assert_equal "[FILTERED]", registry.configuration.dig("mysql", "password")
44
+ end
45
+
46
+ it 'filters key' do
47
+ assert_equal "[FILTERED]", registry.configuration.dig("symmetric_encryption", "key")
40
48
  end
41
49
  end
42
50
 
@@ -11,13 +11,12 @@ class SecretConfigTest < Minitest::Test
11
11
  end
12
12
 
13
13
  before do
14
- SecretConfig.root = root
15
- SecretConfig.provider :file, file_name: file_name
14
+ SecretConfig.use :file, root: root, file_name: file_name
16
15
  end
17
16
 
18
17
  describe '#configuration' do
19
18
  it 'returns a copy of the config' do
20
- assert_equal "127.0.0.1", SecretConfig.configuration.dig("development", "my_application", "mysql", "host")
19
+ assert_equal "127.0.0.1", SecretConfig.configuration.dig("mysql", "host")
21
20
  end
22
21
  end
23
22
 
@@ -37,6 +36,12 @@ class SecretConfigTest < Minitest::Test
37
36
  it 'fetches values' do
38
37
  assert_equal "secret_config_development", SecretConfig.fetch("mysql/database")
39
38
  end
39
+
40
+ it 'can be overridden by an environment variable' do
41
+ ENV['MYSQL_DATABASE'] = 'other'
42
+ assert_equal "other", SecretConfig.fetch("mysql/database")
43
+ ENV['MYSQL_DATABASE'] = nil
44
+ end
40
45
  end
41
46
  end
42
47
  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.2.0
4
+ version: 0.3.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-15 00:00:00.000000000 Z
11
+ date: 2019-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: sync_attr
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: aws-sdk-ssm
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +52,7 @@ files:
66
52
  - lib/secret_config/errors.rb
67
53
  - lib/secret_config/providers/file.rb
68
54
  - lib/secret_config/providers/ssm.rb
55
+ - lib/secret_config/railtie.rb
69
56
  - lib/secret_config/registry.rb
70
57
  - lib/secret_config/version.rb
71
58
  - test/config/application.yml