secret_config 0.2.0 → 0.3.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: 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