chamber 2.4.0 → 2.7.1
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/README.md +16 -930
- data/Rakefile +6 -0
- data/lib/chamber.rb +11 -7
- data/lib/chamber/binary/heroku.rb +45 -25
- data/lib/chamber/binary/runner.rb +82 -44
- data/lib/chamber/binary/travis.rb +14 -8
- data/lib/chamber/commands/base.rb +1 -2
- data/lib/chamber/commands/comparable.rb +0 -1
- data/lib/chamber/commands/compare.rb +1 -1
- data/lib/chamber/commands/files.rb +0 -1
- data/lib/chamber/commands/heroku.rb +2 -3
- data/lib/chamber/commands/heroku/push.rb +1 -1
- data/lib/chamber/commands/initialize.rb +69 -12
- data/lib/chamber/commands/securable.rb +9 -4
- data/lib/chamber/commands/secure.rb +1 -1
- data/lib/chamber/commands/show.rb +20 -4
- data/lib/chamber/commands/travis.rb +0 -1
- data/lib/chamber/configuration.rb +5 -5
- data/lib/chamber/context_resolver.rb +12 -12
- data/lib/chamber/decryption_key.rb +51 -0
- data/lib/chamber/environmentable.rb +4 -1
- data/lib/chamber/errors/decryption_failure.rb +6 -0
- data/lib/chamber/file.rb +7 -8
- data/lib/chamber/file_set.rb +23 -22
- data/lib/chamber/filters/boolean_conversion_filter.rb +1 -2
- data/lib/chamber/filters/decryption_filter.rb +42 -25
- data/lib/chamber/filters/encryption_filter.rb +7 -5
- data/lib/chamber/filters/environment_filter.rb +7 -7
- data/lib/chamber/filters/failed_decryption_filter.rb +41 -0
- data/lib/chamber/filters/namespace_filter.rb +1 -1
- data/lib/chamber/filters/secure_filter.rb +3 -5
- data/lib/chamber/filters/translate_secure_keys_filter.rb +5 -24
- data/lib/chamber/namespace_set.rb +6 -6
- data/lib/chamber/rails.rb +1 -3
- data/lib/chamber/rails/railtie.rb +6 -3
- data/lib/chamber/settings.rb +34 -32
- data/lib/chamber/version.rb +1 -1
- data/spec/fixtures/settings.yml +1 -0
- data/spec/lib/chamber/commands/files_spec.rb +4 -2
- data/spec/lib/chamber/commands/secure_spec.rb +8 -5
- data/spec/lib/chamber/commands/show_spec.rb +18 -3
- data/spec/lib/chamber/context_resolver_spec.rb +38 -18
- data/spec/lib/chamber/file_set_spec.rb +73 -52
- data/spec/lib/chamber/file_spec.rb +37 -23
- data/spec/lib/chamber/filters/boolean_conversion_filter_spec.rb +35 -33
- data/spec/lib/chamber/filters/decryption_filter_spec.rb +142 -21
- data/spec/lib/chamber/filters/encryption_filter_spec.rb +51 -19
- data/spec/lib/chamber/filters/environment_filter_spec.rb +12 -6
- data/spec/lib/chamber/filters/failed_decryption_filter_spec.rb +53 -0
- data/spec/lib/chamber/filters/insecure_filter_spec.rb +38 -18
- data/spec/lib/chamber/filters/namespace_filter_spec.rb +38 -38
- data/spec/lib/chamber/filters/secure_filter_spec.rb +10 -10
- data/spec/lib/chamber/filters/translate_secure_keys_filter_spec.rb +9 -6
- data/spec/lib/chamber/namespace_set_spec.rb +7 -5
- data/spec/lib/chamber/settings_spec.rb +168 -79
- data/spec/lib/chamber_spec.rb +72 -71
- metadata +22 -21
- data/lib/chamber/errors/undecryptable_value_error.rb +0 -6
- data/templates/settings.yml +0 -14
@@ -4,13 +4,12 @@ require 'chamber/instance'
|
|
4
4
|
module Chamber
|
5
5
|
module Commands
|
6
6
|
module Securable
|
7
|
-
|
8
7
|
def initialize(options = {})
|
9
8
|
super
|
10
9
|
|
11
10
|
ignored_settings_options = options.
|
12
11
|
merge(files: ignored_settings_filepaths).
|
13
|
-
reject { |k,
|
12
|
+
reject { |k, _v| k == 'basepath' }
|
14
13
|
self.ignored_settings_instance = Chamber::Instance.new(ignored_settings_options)
|
15
14
|
self.current_settings_instance = Chamber::Instance.new(options)
|
16
15
|
self.only_sensitive = options[:only_sensitive]
|
@@ -47,9 +46,15 @@ module Securable
|
|
47
46
|
end
|
48
47
|
|
49
48
|
def ignored_settings_filepaths
|
50
|
-
shell_escaped_chamber_filenames = chamber.filenames.map
|
49
|
+
shell_escaped_chamber_filenames = chamber.filenames.map do |filename|
|
50
|
+
Shellwords.escape(filename)
|
51
|
+
end
|
51
52
|
|
52
|
-
`
|
53
|
+
`
|
54
|
+
git ls-files --other --ignored --exclude-from=.gitignore |
|
55
|
+
sed -e "s|^|#{Shellwords.escape(rootpath.to_s)}/|" |
|
56
|
+
grep --colour=never -E '#{shell_escaped_chamber_filenames.join('|')}'
|
57
|
+
`.split("\n")
|
53
58
|
end
|
54
59
|
end
|
55
60
|
end
|
@@ -4,20 +4,36 @@ require 'chamber/commands/base'
|
|
4
4
|
module Chamber
|
5
5
|
module Commands
|
6
6
|
class Show < Chamber::Commands::Base
|
7
|
-
|
8
7
|
def initialize(options = {})
|
9
8
|
super
|
10
9
|
|
11
|
-
self.as_env
|
10
|
+
self.as_env = options[:as_env]
|
11
|
+
self.only_sensitive = options[:only_sensitive]
|
12
12
|
end
|
13
13
|
|
14
14
|
def call
|
15
|
-
as_env
|
15
|
+
if as_env
|
16
|
+
settings.to_s(pair_separator: "\n")
|
17
|
+
else
|
18
|
+
PP.
|
19
|
+
pp(settings.to_hash, StringIO.new, 60).
|
20
|
+
string.
|
21
|
+
chomp
|
22
|
+
end
|
16
23
|
end
|
17
24
|
|
18
25
|
protected
|
19
26
|
|
20
|
-
attr_accessor :as_env
|
27
|
+
attr_accessor :as_env,
|
28
|
+
:only_sensitive
|
29
|
+
|
30
|
+
def settings
|
31
|
+
@settings ||= if only_sensitive
|
32
|
+
chamber.settings.securable
|
33
|
+
else
|
34
|
+
chamber.settings
|
35
|
+
end
|
36
|
+
end
|
21
37
|
end
|
22
38
|
end
|
23
39
|
end
|
@@ -20,11 +20,11 @@ class Configuration
|
|
20
20
|
|
21
21
|
def to_hash
|
22
22
|
{
|
23
|
-
basepath:
|
24
|
-
decryption_key:
|
25
|
-
encryption_key:
|
26
|
-
files:
|
27
|
-
namespaces:
|
23
|
+
basepath: basepath,
|
24
|
+
decryption_key: decryption_key,
|
25
|
+
encryption_key: encryption_key,
|
26
|
+
files: files,
|
27
|
+
namespaces: namespaces,
|
28
28
|
}
|
29
29
|
end
|
30
30
|
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
require 'pathname'
|
2
|
+
require 'socket'
|
2
3
|
require 'hashie/mash'
|
4
|
+
require 'chamber/decryption_key'
|
3
5
|
|
4
6
|
module Chamber
|
5
7
|
class ContextResolver
|
6
|
-
|
7
8
|
def initialize(options = {})
|
8
9
|
self.options = Hashie::Mash.new(options)
|
9
10
|
end
|
@@ -22,7 +23,10 @@ class ContextResolver
|
|
22
23
|
if options[:namespaces] == []
|
23
24
|
require options[:rootpath].join('config', 'application')
|
24
25
|
|
25
|
-
options[:namespaces] = [
|
26
|
+
options[:namespaces] = [
|
27
|
+
::Rails.env,
|
28
|
+
Socket.gethostname,
|
29
|
+
]
|
26
30
|
end
|
27
31
|
else
|
28
32
|
options[:basepath] ||= options[:rootpath]
|
@@ -30,9 +34,8 @@ class ContextResolver
|
|
30
34
|
|
31
35
|
options[:basepath] = Pathname.new(options[:basepath])
|
32
36
|
|
33
|
-
options[:files] ||= [
|
34
|
-
|
35
|
-
options[:basepath] + 'settings' ]
|
37
|
+
options[:files] ||= [options[:basepath] + 'settings*.yml',
|
38
|
+
options[:basepath] + 'settings']
|
36
39
|
|
37
40
|
options
|
38
41
|
rescue LoadError
|
@@ -40,7 +43,7 @@ class ContextResolver
|
|
40
43
|
end
|
41
44
|
|
42
45
|
def self.resolve(options = {})
|
43
|
-
|
46
|
+
new(options).resolve
|
44
47
|
end
|
45
48
|
|
46
49
|
protected
|
@@ -48,9 +51,7 @@ class ContextResolver
|
|
48
51
|
attr_accessor :options
|
49
52
|
|
50
53
|
def resolve_preset
|
51
|
-
if in_a_rails_project?
|
52
|
-
'rails'
|
53
|
-
end
|
54
|
+
'rails' if in_a_rails_project?
|
54
55
|
end
|
55
56
|
|
56
57
|
def resolve_encryption_key(key)
|
@@ -60,9 +61,8 @@ class ContextResolver
|
|
60
61
|
end
|
61
62
|
|
62
63
|
def resolve_decryption_key(key)
|
63
|
-
|
64
|
-
|
65
|
-
key if Pathname.new(key).readable?
|
64
|
+
DecryptionKey.resolve(filename: key,
|
65
|
+
rootpath: options[:rootpath])
|
66
66
|
end
|
67
67
|
|
68
68
|
def in_a_rails_project?
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Chamber
|
4
|
+
class DecryptionKey
|
5
|
+
def initialize(options = {})
|
6
|
+
self.rootpath = options[:rootpath]
|
7
|
+
self.filename = options[:filename] || ''
|
8
|
+
end
|
9
|
+
|
10
|
+
def resolve
|
11
|
+
if filename.readable?
|
12
|
+
filename.read
|
13
|
+
elsif in_environment_variable?
|
14
|
+
environment_variable
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.resolve(*args)
|
19
|
+
new(*args).resolve
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
attr_accessor :filename,
|
25
|
+
:rootpath
|
26
|
+
|
27
|
+
def filename=(other)
|
28
|
+
other_file = Pathname.new(other)
|
29
|
+
|
30
|
+
@filename = if other_file.readable?
|
31
|
+
other_file
|
32
|
+
else
|
33
|
+
default_file
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def in_environment_variable?
|
40
|
+
ENV['CHAMBER_KEY']
|
41
|
+
end
|
42
|
+
|
43
|
+
def environment_variable
|
44
|
+
ENV['CHAMBER_KEY']
|
45
|
+
end
|
46
|
+
|
47
|
+
def default_file
|
48
|
+
Pathname.new(rootpath + '.chamber.pem')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -2,11 +2,14 @@ require 'hashie/mash'
|
|
2
2
|
|
3
3
|
module Chamber
|
4
4
|
module Environmentable
|
5
|
+
SECURE_KEY_TOKEN = /\A_secure_/
|
6
|
+
|
5
7
|
def with_environment(settings, parent_keys, hash_block, value_block)
|
6
8
|
environment_hash = Hashie::Mash.new
|
7
9
|
|
8
10
|
settings.each_pair do |key, value|
|
9
|
-
|
11
|
+
environment_key = key.to_s.gsub(SECURE_KEY_TOKEN, '')
|
12
|
+
environment_keys = parent_keys.dup.push(environment_key)
|
10
13
|
|
11
14
|
if value.respond_to? :each_pair
|
12
15
|
environment_hash.merge!(hash_block.call(key, value, environment_keys))
|
data/lib/chamber/file.rb
CHANGED
@@ -8,7 +8,6 @@ require 'erb'
|
|
8
8
|
#
|
9
9
|
module Chamber
|
10
10
|
class File < Pathname
|
11
|
-
|
12
11
|
###
|
13
12
|
# Internal: Creates a settings file representing a path to a file on the
|
14
13
|
# filesystem.
|
@@ -64,16 +63,16 @@ class File < Pathname
|
|
64
63
|
# ```
|
65
64
|
#
|
66
65
|
def to_settings
|
67
|
-
@data ||= Settings.new(settings:
|
68
|
-
namespaces:
|
69
|
-
decryption_key:
|
70
|
-
encryption_key:
|
66
|
+
@data ||= Settings.new(settings: file_contents_hash,
|
67
|
+
namespaces: namespaces,
|
68
|
+
decryption_key: decryption_key,
|
69
|
+
encryption_key: encryption_key)
|
71
70
|
end
|
72
71
|
|
73
72
|
def secure
|
74
73
|
insecure_settings = to_settings.insecure.to_flattened_name_hash
|
75
74
|
secure_settings = to_settings.insecure.secure.to_flattened_name_hash
|
76
|
-
file_contents =
|
75
|
+
file_contents = read
|
77
76
|
|
78
77
|
insecure_settings.each_pair do |name_pieces, value|
|
79
78
|
secure_value = secure_settings[name_pieces]
|
@@ -87,7 +86,7 @@ class File < Pathname
|
|
87
86
|
"\\1_secure_#{name_pieces.last}\\2:\\3#{secure_value}")
|
88
87
|
end
|
89
88
|
|
90
|
-
|
89
|
+
write(file_contents)
|
91
90
|
end
|
92
91
|
|
93
92
|
protected
|
@@ -99,7 +98,7 @@ class File < Pathname
|
|
99
98
|
private
|
100
99
|
|
101
100
|
def file_contents_hash
|
102
|
-
file_contents =
|
101
|
+
file_contents = read
|
103
102
|
erb_result = ERB.new(file_contents).result
|
104
103
|
|
105
104
|
YAML.load(erb_result) || {}
|
data/lib/chamber/file_set.rb
CHANGED
@@ -6,10 +6,10 @@ require 'chamber/settings'
|
|
6
6
|
###
|
7
7
|
# Internal: Represents a set of settings files that should be considered for
|
8
8
|
# processing. Whether they actually *are* processed depends on their extension
|
9
|
-
# (only *.yml files are processed unless explicitly specified),
|
10
|
-
# their namespace matches one of the namespaces passed to the
|
11
|
-
# after a dash '-' but before the extension is considered the
|
12
|
-
# file).
|
9
|
+
# (only *.yml and *.yml.erb files are processed unless explicitly specified),
|
10
|
+
# and whether their namespace matches one of the namespaces passed to the
|
11
|
+
# FileSet (text after a dash '-' but before the extension is considered the
|
12
|
+
# namespace for the file).
|
13
13
|
#
|
14
14
|
# When converted to settings, files are always processed in the order of least
|
15
15
|
# specific to most specific. So if there are two files:
|
@@ -61,7 +61,7 @@ require 'chamber/settings'
|
|
61
61
|
# # /tmp/settings-green.yml
|
62
62
|
# # /tmp/settings/another.yml
|
63
63
|
# # /tmp/settings/another.json
|
64
|
-
# # /tmp/settings/yet_another-blue.yml
|
64
|
+
# # /tmp/settings/yet_another-blue.yml.erb
|
65
65
|
# # /tmp/settings/yet_another-green.yml
|
66
66
|
# #
|
67
67
|
#
|
@@ -74,7 +74,7 @@ require 'chamber/settings'
|
|
74
74
|
#
|
75
75
|
# ###
|
76
76
|
# # This will all files in the 'settings' directory but will only process
|
77
|
-
# # 'another.yml' and 'yet_another-blue.yml'
|
77
|
+
# # 'another.yml' and 'yet_another-blue.yml.erb'
|
78
78
|
# #
|
79
79
|
# FileSet.new(files: '/tmp/settings',
|
80
80
|
# namespaces: {
|
@@ -110,7 +110,6 @@ require 'chamber/settings'
|
|
110
110
|
#
|
111
111
|
module Chamber
|
112
112
|
class FileSet
|
113
|
-
|
114
113
|
def initialize(options = {})
|
115
114
|
self.namespaces = options[:namespaces] || {}
|
116
115
|
self.decryption_key = options[:decryption_key]
|
@@ -208,17 +207,19 @@ class FileSet
|
|
208
207
|
# duplicates removed.
|
209
208
|
#
|
210
209
|
def files
|
211
|
-
@files ||=
|
210
|
+
@files ||= lambda do
|
212
211
|
sorted_relevant_files = []
|
213
212
|
|
214
213
|
file_globs.each do |glob|
|
215
214
|
current_glob_files = Pathname.glob(glob)
|
216
215
|
relevant_glob_files = relevant_files & current_glob_files
|
217
216
|
|
218
|
-
relevant_glob_files.map!
|
219
|
-
|
220
|
-
|
221
|
-
|
217
|
+
relevant_glob_files.map! do |file|
|
218
|
+
File.new(path: file,
|
219
|
+
namespaces: namespaces,
|
220
|
+
decryption_key: decryption_key,
|
221
|
+
encryption_key: encryption_key)
|
222
|
+
end
|
222
223
|
|
223
224
|
sorted_relevant_files += relevant_glob_files
|
224
225
|
end
|
@@ -243,18 +244,18 @@ class FileSet
|
|
243
244
|
|
244
245
|
def file_globs
|
245
246
|
@file_globs ||= paths.map do |path|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
247
|
+
if path.directory?
|
248
|
+
path + '*.{yml,yml.erb}'
|
249
|
+
else
|
250
|
+
path
|
251
|
+
end
|
252
|
+
end
|
252
253
|
end
|
253
254
|
|
254
255
|
def namespaced_files
|
255
256
|
@namespaced_files ||= all_files.select do |file|
|
256
|
-
|
257
|
-
|
257
|
+
file.basename.fnmatch? '*-*'
|
258
|
+
end
|
258
259
|
end
|
259
260
|
|
260
261
|
def relevant_namespaced_files
|
@@ -262,8 +263,8 @@ class FileSet
|
|
262
263
|
|
263
264
|
namespaces.each do |namespace|
|
264
265
|
file_holder << namespaced_files.select do |file|
|
265
|
-
|
266
|
-
|
266
|
+
file.basename.fnmatch? "*-#{namespace}.???"
|
267
|
+
end
|
267
268
|
end
|
268
269
|
|
269
270
|
file_holder.flatten
|
@@ -1,13 +1,12 @@
|
|
1
1
|
module Chamber
|
2
2
|
module Filters
|
3
3
|
class BooleanConversionFilter
|
4
|
-
|
5
4
|
def initialize(options = {})
|
6
5
|
self.data = options.fetch(:data).dup
|
7
6
|
end
|
8
7
|
|
9
8
|
def self.execute(options = {})
|
10
|
-
|
9
|
+
new(options).send(:execute)
|
11
10
|
end
|
12
11
|
|
13
12
|
protected
|
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'openssl'
|
2
2
|
require 'base64'
|
3
3
|
require 'hashie/mash'
|
4
|
-
require '
|
4
|
+
require 'yaml'
|
5
|
+
require 'chamber/errors/decryption_failure'
|
5
6
|
|
6
7
|
module Chamber
|
7
8
|
module Filters
|
8
9
|
class DecryptionFilter
|
9
|
-
SECURE_KEY_TOKEN =
|
10
|
-
BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9
|
10
|
+
SECURE_KEY_TOKEN = /\A_secure_/
|
11
|
+
BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9\+/]{342}==\z}
|
11
12
|
|
12
13
|
def initialize(options = {})
|
13
14
|
self.decryption_key = options.fetch(:decryption_key, nil)
|
@@ -15,7 +16,7 @@ class DecryptionFilter
|
|
15
16
|
end
|
16
17
|
|
17
18
|
def self.execute(options = {})
|
18
|
-
|
19
|
+
new(options).send(:execute)
|
19
20
|
end
|
20
21
|
|
21
22
|
protected
|
@@ -27,27 +28,13 @@ class DecryptionFilter
|
|
27
28
|
settings = Hashie::Mash.new
|
28
29
|
|
29
30
|
raw_data.each_pair do |key, value|
|
30
|
-
if value.respond_to? :each_pair
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
decoded_string = Base64.strict_decode64(value)
|
38
|
-
decryption_key.private_decrypt(decoded_string)
|
39
|
-
end
|
40
|
-
else
|
41
|
-
warn 'WARNING: It appears that you would like to keep your ' \
|
42
|
-
"information for #{key} secure, however the value for that " \
|
43
|
-
'setting does not appear to be encrypted. Make sure you run ' \
|
44
|
-
"'chamber secure' before committing."
|
45
|
-
|
46
|
-
value
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
settings[key] = value
|
31
|
+
settings[key] = if value.respond_to? :each_pair
|
32
|
+
execute(value)
|
33
|
+
elsif key.match(SECURE_KEY_TOKEN) && value.respond_to?(:match)
|
34
|
+
read_or_decrypt(key, value)
|
35
|
+
else
|
36
|
+
value
|
37
|
+
end
|
51
38
|
end
|
52
39
|
|
53
40
|
settings
|
@@ -64,6 +51,36 @@ class DecryptionFilter
|
|
64
51
|
|
65
52
|
@decryption_key = OpenSSL::PKey::RSA.new(key_content)
|
66
53
|
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def read_or_decrypt(key, value)
|
58
|
+
if value.match(BASE64_STRING_PATTERN)
|
59
|
+
decrypt(value)
|
60
|
+
else
|
61
|
+
warn 'WARNING: It appears that you would like to keep your ' \
|
62
|
+
"information for #{key} secure, however the value for that " \
|
63
|
+
'setting does not appear to be encrypted. Make sure you run ' \
|
64
|
+
"'chamber secure' before committing."
|
65
|
+
|
66
|
+
value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def decrypt(value)
|
71
|
+
if decryption_key.nil?
|
72
|
+
value
|
73
|
+
else
|
74
|
+
decoded_string = Base64.strict_decode64(value)
|
75
|
+
unencrypted_value = decryption_key.private_decrypt(decoded_string)
|
76
|
+
|
77
|
+
begin
|
78
|
+
_unserialized_value = YAML.load(unencrypted_value)
|
79
|
+
rescue TypeError
|
80
|
+
unencrypted_value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
67
84
|
end
|
68
85
|
end
|
69
86
|
end
|