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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -930
  3. data/Rakefile +6 -0
  4. data/lib/chamber.rb +11 -7
  5. data/lib/chamber/binary/heroku.rb +45 -25
  6. data/lib/chamber/binary/runner.rb +82 -44
  7. data/lib/chamber/binary/travis.rb +14 -8
  8. data/lib/chamber/commands/base.rb +1 -2
  9. data/lib/chamber/commands/comparable.rb +0 -1
  10. data/lib/chamber/commands/compare.rb +1 -1
  11. data/lib/chamber/commands/files.rb +0 -1
  12. data/lib/chamber/commands/heroku.rb +2 -3
  13. data/lib/chamber/commands/heroku/push.rb +1 -1
  14. data/lib/chamber/commands/initialize.rb +69 -12
  15. data/lib/chamber/commands/securable.rb +9 -4
  16. data/lib/chamber/commands/secure.rb +1 -1
  17. data/lib/chamber/commands/show.rb +20 -4
  18. data/lib/chamber/commands/travis.rb +0 -1
  19. data/lib/chamber/configuration.rb +5 -5
  20. data/lib/chamber/context_resolver.rb +12 -12
  21. data/lib/chamber/decryption_key.rb +51 -0
  22. data/lib/chamber/environmentable.rb +4 -1
  23. data/lib/chamber/errors/decryption_failure.rb +6 -0
  24. data/lib/chamber/file.rb +7 -8
  25. data/lib/chamber/file_set.rb +23 -22
  26. data/lib/chamber/filters/boolean_conversion_filter.rb +1 -2
  27. data/lib/chamber/filters/decryption_filter.rb +42 -25
  28. data/lib/chamber/filters/encryption_filter.rb +7 -5
  29. data/lib/chamber/filters/environment_filter.rb +7 -7
  30. data/lib/chamber/filters/failed_decryption_filter.rb +41 -0
  31. data/lib/chamber/filters/namespace_filter.rb +1 -1
  32. data/lib/chamber/filters/secure_filter.rb +3 -5
  33. data/lib/chamber/filters/translate_secure_keys_filter.rb +5 -24
  34. data/lib/chamber/namespace_set.rb +6 -6
  35. data/lib/chamber/rails.rb +1 -3
  36. data/lib/chamber/rails/railtie.rb +6 -3
  37. data/lib/chamber/settings.rb +34 -32
  38. data/lib/chamber/version.rb +1 -1
  39. data/spec/fixtures/settings.yml +1 -0
  40. data/spec/lib/chamber/commands/files_spec.rb +4 -2
  41. data/spec/lib/chamber/commands/secure_spec.rb +8 -5
  42. data/spec/lib/chamber/commands/show_spec.rb +18 -3
  43. data/spec/lib/chamber/context_resolver_spec.rb +38 -18
  44. data/spec/lib/chamber/file_set_spec.rb +73 -52
  45. data/spec/lib/chamber/file_spec.rb +37 -23
  46. data/spec/lib/chamber/filters/boolean_conversion_filter_spec.rb +35 -33
  47. data/spec/lib/chamber/filters/decryption_filter_spec.rb +142 -21
  48. data/spec/lib/chamber/filters/encryption_filter_spec.rb +51 -19
  49. data/spec/lib/chamber/filters/environment_filter_spec.rb +12 -6
  50. data/spec/lib/chamber/filters/failed_decryption_filter_spec.rb +53 -0
  51. data/spec/lib/chamber/filters/insecure_filter_spec.rb +38 -18
  52. data/spec/lib/chamber/filters/namespace_filter_spec.rb +38 -38
  53. data/spec/lib/chamber/filters/secure_filter_spec.rb +10 -10
  54. data/spec/lib/chamber/filters/translate_secure_keys_filter_spec.rb +9 -6
  55. data/spec/lib/chamber/namespace_set_spec.rb +7 -5
  56. data/spec/lib/chamber/settings_spec.rb +168 -79
  57. data/spec/lib/chamber_spec.rb +72 -71
  58. metadata +22 -21
  59. data/lib/chamber/errors/undecryptable_value_error.rb +0 -6
  60. 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, v| k == 'basepath' }
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 { |filename| Shellwords.escape(filename) }
49
+ shell_escaped_chamber_filenames = chamber.filenames.map do |filename|
50
+ Shellwords.escape(filename)
51
+ end
51
52
 
52
- `git ls-files --other --ignored --exclude-from=.gitignore | sed -e "s|^|#{Shellwords.escape(rootpath.to_s)}/|" | grep --colour=never -E '#{shell_escaped_chamber_filenames.join('|')}'`.split("\n")
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
@@ -11,7 +11,7 @@ class Secure < Chamber::Commands::Base
11
11
 
12
12
  def call
13
13
  disable_warnings do
14
- insecure_environment_variables.each do |key, value|
14
+ insecure_environment_variables.each do |key, _value|
15
15
  if dry_run
16
16
  shell.say_status 'encrypt', key, :blue
17
17
  else
@@ -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 = options[: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 ? chamber.to_s(pair_separator: "\n") : PP.pp(chamber.to_hash, StringIO.new, 60).string.chomp
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
@@ -3,7 +3,6 @@ require 'bundler'
3
3
  module Chamber
4
4
  module Commands
5
5
  module Travis
6
-
7
6
  protected
8
7
 
9
8
  def travis_encrypt(command)
@@ -20,11 +20,11 @@ class Configuration
20
20
 
21
21
  def to_hash
22
22
  {
23
- basepath: self.basepath,
24
- decryption_key: self.decryption_key,
25
- encryption_key: self.encryption_key,
26
- files: self.files,
27
- namespaces: self.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] = [::Rails.env]
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] ||= [ options[:basepath] + 'credentials*.yml',
34
- options[:basepath] + 'settings*.yml',
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
- self.new(options).resolve
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
- key ||= options[:rootpath] + '.chamber.pem'
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
- environment_keys = parent_keys.dup.push(key)
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))
@@ -0,0 +1,6 @@
1
+ module Chamber
2
+ module Errors
3
+ class DecryptionFailure < RuntimeError
4
+ end
5
+ end
6
+ end
@@ -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: file_contents_hash,
68
- namespaces: namespaces,
69
- decryption_key: decryption_key,
70
- encryption_key: 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 = self.read
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
- self.write(file_contents)
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 = self.read
101
+ file_contents = read
103
102
  erb_result = ERB.new(file_contents).result
104
103
 
105
104
  YAML.load(erb_result) || {}
@@ -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), and whether
10
- # their namespace matches one of the namespaces passed to the FileSet (text
11
- # after a dash '-' but before the extension is considered the namespace for 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 ||= -> do
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! { |file| File.new( path: file,
219
- namespaces: namespaces,
220
- decryption_key: decryption_key,
221
- encryption_key: encryption_key) }
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
- if path.directory?
247
- path + '*.yml'
248
- else
249
- path
250
- end
251
- end
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
- file.basename.fnmatch? '*-*'
257
- end
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
- file.basename.fnmatch? "*-#{namespace}.???"
266
- end
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
- self.new(options).send(:execute)
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 'chamber/errors/undecryptable_value_error'
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 = %r{\A_secure_}
10
- BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9\+\/]{342}==\z}
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
- self.new(options).send(:execute)
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
- value = execute(value)
32
- elsif key.match(SECURE_KEY_TOKEN) && value.respond_to?(:match)
33
- value = if value.match(BASE64_STRING_PATTERN)
34
- if decryption_key.nil?
35
- value
36
- else
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