chamber 2.10.2 → 2.11.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/LICENSE.txt +1 -1
  5. data/lib/chamber.rb +3 -6
  6. data/lib/chamber/binary/heroku.rb +1 -0
  7. data/lib/chamber/binary/runner.rb +29 -13
  8. data/lib/chamber/binary/travis.rb +1 -0
  9. data/lib/chamber/commands/base.rb +10 -9
  10. data/lib/chamber/commands/comparable.rb +1 -0
  11. data/lib/chamber/commands/compare.rb +8 -7
  12. data/lib/chamber/commands/files.rb +1 -0
  13. data/lib/chamber/commands/heroku.rb +1 -0
  14. data/lib/chamber/commands/heroku/clear.rb +2 -1
  15. data/lib/chamber/commands/heroku/compare.rb +1 -0
  16. data/lib/chamber/commands/heroku/pull.rb +3 -4
  17. data/lib/chamber/commands/heroku/push.rb +1 -0
  18. data/lib/chamber/commands/initialize.rb +88 -76
  19. data/lib/chamber/commands/securable.rb +3 -2
  20. data/lib/chamber/commands/secure.rb +3 -1
  21. data/lib/chamber/commands/show.rb +7 -6
  22. data/lib/chamber/commands/travis.rb +1 -0
  23. data/lib/chamber/commands/travis/secure.rb +1 -0
  24. data/lib/chamber/configuration.rb +14 -13
  25. data/lib/chamber/context_resolver.rb +52 -55
  26. data/lib/chamber/encryption_methods/none.rb +4 -2
  27. data/lib/chamber/encryption_methods/public_key.rb +4 -2
  28. data/lib/chamber/encryption_methods/ssl.rb +11 -9
  29. data/lib/chamber/errors/decryption_failure.rb +1 -0
  30. data/lib/chamber/file.rb +27 -18
  31. data/lib/chamber/file_set.rb +14 -13
  32. data/lib/chamber/filters/decryption_filter.rb +48 -18
  33. data/lib/chamber/filters/encryption_filter.rb +32 -22
  34. data/lib/chamber/filters/environment_filter.rb +109 -16
  35. data/lib/chamber/filters/failed_decryption_filter.rb +10 -8
  36. data/lib/chamber/filters/insecure_filter.rb +1 -0
  37. data/lib/chamber/filters/namespace_filter.rb +8 -7
  38. data/lib/chamber/filters/secure_filter.rb +10 -9
  39. data/lib/chamber/filters/translate_secure_keys_filter.rb +10 -9
  40. data/lib/chamber/instance.rb +5 -4
  41. data/lib/chamber/key_pair.rb +82 -0
  42. data/lib/chamber/keys/base.rb +64 -0
  43. data/lib/chamber/keys/decryption.rb +41 -0
  44. data/lib/chamber/keys/encryption.rb +41 -0
  45. data/lib/chamber/namespace_set.rb +10 -9
  46. data/lib/chamber/rails.rb +1 -0
  47. data/lib/chamber/rails/railtie.rb +1 -0
  48. data/lib/chamber/rubinius_fix.rb +1 -0
  49. data/lib/chamber/settings.rb +45 -41
  50. data/lib/chamber/types/secured.rb +14 -12
  51. data/lib/chamber/version.rb +2 -1
  52. metadata +28 -27
  53. metadata.gz.sig +0 -0
  54. data/lib/chamber/decryption_key.rb +0 -52
  55. data/lib/chamber/environmentable.rb +0 -27
  56. data/lib/chamber/filters/boolean_conversion_filter.rb +0 -41
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'pathname'
3
4
  require 'chamber/namespace_set'
4
5
  require 'chamber/file'
@@ -111,11 +112,16 @@ require 'chamber/settings'
111
112
  #
112
113
  module Chamber
113
114
  class FileSet
115
+ attr_reader :namespaces,
116
+ :paths
117
+ attr_accessor :decryption_keys,
118
+ :encryption_keys
119
+
114
120
  def initialize(options = {})
115
- self.namespaces = options[:namespaces] || {}
116
- self.decryption_key = options[:decryption_key]
117
- self.encryption_key = options[:encryption_key]
118
- self.paths = options.fetch(:files)
121
+ self.namespaces = options[:namespaces] || {}
122
+ self.decryption_keys = options[:decryption_keys]
123
+ self.encryption_keys = options[:encryption_keys]
124
+ self.paths = options.fetch(:files)
119
125
  end
120
126
 
121
127
  ###
@@ -177,11 +183,6 @@ class FileSet
177
183
 
178
184
  protected
179
185
 
180
- attr_reader :namespaces,
181
- :paths
182
- attr_accessor :decryption_key,
183
- :encryption_key
184
-
185
186
  ###
186
187
  # Internal: Allows the paths for the FileSet to be set. It can either be an
187
188
  # object that responds to `#each` like an Array or one that doesn't. In which
@@ -216,10 +217,10 @@ class FileSet
216
217
  relevant_glob_files = relevant_files & current_glob_files
217
218
 
218
219
  relevant_glob_files.map! do |file|
219
- File.new(path: file,
220
- namespaces: namespaces,
221
- decryption_key: decryption_key,
222
- encryption_key: encryption_key)
220
+ File.new(path: file,
221
+ namespaces: namespaces,
222
+ decryption_keys: decryption_keys,
223
+ encryption_keys: encryption_keys)
223
224
  end
224
225
 
225
226
  sorted_relevant_files += relevant_glob_files
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'openssl'
3
4
  require 'base64'
4
5
  require 'hashie/mash'
@@ -11,13 +12,31 @@ require 'chamber/errors/decryption_failure'
11
12
  module Chamber
12
13
  module Filters
13
14
  class DecryptionFilter
14
- SECURE_KEY_TOKEN = /\A_secure_/
15
15
  BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9\+/]{342}==\z}
16
- LARGE_DATA_STRING_PATTERN = %r{\A([A-Za-z0-9\+\/#]*\={0,2})#([A-Za-z0-9\+\/#]*\={0,2})#([A-Za-z0-9\+\/#]*\={0,2})\z} # rubocop:disable Metrics/LineLength
16
+ LARGE_DATA_STRING_PATTERN = %r{
17
+ \A # Beginning of String
18
+ (
19
+ [A-Za-z0-9\+\/#]*\={0,2} # Base64 Encoded Key
20
+ )
21
+ \# # Separator
22
+ (
23
+ [A-Za-z0-9\+\/#]*\={0,2} # Base64 Encoded IV
24
+ )
25
+ \# # Separator
26
+ (
27
+ [A-Za-z0-9\+\/#]*\={0,2} # Base64 Encoded Data
28
+ )
29
+ \z # End of String
30
+ }x
31
+
32
+ attr_accessor :data,
33
+ :secure_key_token
34
+ attr_reader :decryption_keys
17
35
 
18
36
  def initialize(options = {})
19
- self.decryption_key = options.fetch(:decryption_key, nil)
20
- self.data = options.fetch(:data).dup
37
+ self.decryption_keys = options.fetch(:decryption_keys, {}) || {}
38
+ self.data = options.fetch(:data).dup
39
+ self.secure_key_token = /\A#{Regexp.escape(options.fetch(:secure_key_prefix))}/
21
40
  end
22
41
 
23
42
  def self.execute(options = {})
@@ -26,17 +45,14 @@ class DecryptionFilter
26
45
 
27
46
  protected
28
47
 
29
- attr_accessor :data
30
- attr_reader :decryption_key
31
-
32
48
  def execute(raw_data = data)
33
49
  settings = Hashie::Mash.new
34
50
 
35
51
  raw_data.each_pair do |key, value|
36
52
  settings[key] = if value.respond_to? :each_pair
37
53
  execute(value)
38
- elsif key.match(SECURE_KEY_TOKEN)
39
- decryption_method(value).decrypt(key, value, decryption_key)
54
+ elsif key.match(secure_key_token)
55
+ decrypt(key, value)
40
56
  else
41
57
  value
42
58
  end
@@ -45,20 +61,34 @@ class DecryptionFilter
45
61
  settings
46
62
  end
47
63
 
48
- def decryption_key=(keyish)
49
- return @decryption_key = nil if keyish.nil?
64
+ def decryption_keys=(other)
65
+ @decryption_keys = other.each_value.map do |keyish|
66
+ content = if ::File.readable?(::File.expand_path(keyish))
67
+ ::File.read(::File.expand_path(keyish))
68
+ else
69
+ keyish
70
+ end
50
71
 
51
- key_content = if ::File.readable?(::File.expand_path(keyish))
52
- ::File.read(::File.expand_path(keyish))
53
- else
54
- keyish
55
- end
56
-
57
- @decryption_key = OpenSSL::PKey::RSA.new(key_content)
72
+ OpenSSL::PKey::RSA.new(content)
73
+ end
58
74
  end
59
75
 
60
76
  private
61
77
 
78
+ def decrypt(key, value)
79
+ method = decryption_method(value)
80
+
81
+ decryption_keys.each do |decryption_key|
82
+ begin
83
+ return method.decrypt(key, value, decryption_key)
84
+ rescue OpenSSL::PKey::RSAError
85
+ next
86
+ end
87
+ end
88
+
89
+ value
90
+ end
91
+
62
92
  def decryption_method(value)
63
93
  if value.respond_to?(:match)
64
94
  if value.match(BASE64_STRING_PATTERN)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'openssl'
3
4
  require 'base64'
4
5
  require 'hashie/mash'
@@ -10,13 +11,17 @@ require 'chamber/encryption_methods/none'
10
11
  module Chamber
11
12
  module Filters
12
13
  class EncryptionFilter
13
- SECURE_KEY_TOKEN = /\A_secure_/
14
14
  BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9\+\/]{342}==\z}
15
15
  LARGE_DATA_STRING_PATTERN = %r{\A([A-Za-z0-9\+\/#]*\={0,2})#([A-Za-z0-9\+\/#]*\={0,2})#([A-Za-z0-9\+\/#]*\={0,2})\z} # rubocop:disable Metrics/LineLength
16
16
 
17
+ attr_accessor :data,
18
+ :secure_key_token
19
+ attr_reader :encryption_keys
20
+
17
21
  def initialize(options = {})
18
- self.encryption_key = options.fetch(:encryption_key, nil)
19
- self.data = options.fetch(:data).dup
22
+ self.encryption_keys = options.fetch(:encryption_keys, {}) || {}
23
+ self.data = options.fetch(:data).dup
24
+ self.secure_key_token = /\A#{Regexp.escape(options.fetch(:secure_key_prefix))}/
20
25
  end
21
26
 
22
27
  def self.execute(options = {})
@@ -25,35 +30,40 @@ class EncryptionFilter
25
30
 
26
31
  protected
27
32
 
28
- attr_accessor :data
29
- attr_reader :encryption_key
30
-
31
- def execute(raw_data = data)
32
- settings = Hashie::Mash.new
33
-
34
- raw_data.each_pair do |key, value|
33
+ def execute(raw_data = data, namespace = nil)
34
+ raw_data.each_with_object(Hashie::Mash.new) do |(key, value), settings|
35
35
  settings[key] = if value.respond_to? :each_pair
36
- execute(value)
37
- elsif key.match(SECURE_KEY_TOKEN)
38
- encryption_method(value).encrypt(key, value, encryption_key)
36
+ execute(value, namespace || key)
37
+ elsif key.match(secure_key_token)
38
+ encrypt(namespace, key, value)
39
39
  else
40
40
  value
41
41
  end
42
42
  end
43
+ end
43
44
 
44
- settings
45
+ def encryption_keys=(other)
46
+ @encryption_keys = other.each_with_object({}) do |(namespace, keyish), memo|
47
+ memo[namespace] = if keyish.is_a?(OpenSSL::PKey::RSA)
48
+ keyish
49
+ elsif ::File.readable?(::File.expand_path(keyish))
50
+ file_contents = ::File.read(::File.expand_path(keyish))
51
+ OpenSSL::PKey::RSA.new(file_contents)
52
+ else
53
+ OpenSSL::PKey::RSA.new(keyish)
54
+ end
55
+ end
45
56
  end
46
57
 
47
- def encryption_key=(keyish)
48
- return @encryption_key = nil if keyish.nil?
58
+ private
49
59
 
50
- key_content = if ::File.readable?(::File.expand_path(keyish))
51
- ::File.read(::File.expand_path(keyish))
52
- else
53
- keyish
54
- end
60
+ def encrypt(namespace, key, value)
61
+ method = encryption_method(value)
62
+ encryption_key = encryption_keys[namespace] || encryption_keys[:__default]
63
+
64
+ return value unless encryption_key
55
65
 
56
- @encryption_key = OpenSSL::PKey::RSA.new(key_content)
66
+ method.encrypt(key, value, encryption_key)
57
67
  end
58
68
 
59
69
  def encryption_method(value)
@@ -1,20 +1,33 @@
1
1
  # frozen_string_literal: true
2
- require 'chamber/environmentable'
2
+
3
+ require 'yaml'
4
+ require 'hashie/mash'
3
5
 
4
6
  module Chamber
5
7
  module Filters
6
8
  class EnvironmentFilter
7
- include Environmentable
8
-
9
- def initialize(options = {})
10
- self.data = options.fetch(:data)
11
- end
12
-
13
9
  ###
14
10
  # Internal: Allows the existing environment to be injected into the passed in
15
11
  # hash. The hash that is passed in is *not* modified, instead a new hash is
16
12
  # returned.
17
13
  #
14
+ # This filter will also do basic value conversions from the environment
15
+ # variable string to the data type defined in the YAML. For example if the
16
+ # YAML value is `true`, then the conversion knows it's a Boolean. If there's
17
+ # an environment varible which should override that value, it will look to see
18
+ # if it is a `String` of 'true', 'false', 't', 'f', 'yes', or 'no' and perform
19
+ # the appropriate conversion of that value into a Boolean.
20
+ #
21
+ # This will work for:
22
+ #
23
+ # * Booleans
24
+ # * Integers
25
+ # * Floats
26
+ # * Arrays
27
+ #
28
+ # For the Arrays, it will convert the environment value by parsing the string
29
+ # as YAML. Whatever the parsed value ends up being, *must* be an Array.
30
+ #
18
31
  # Examples:
19
32
  #
20
33
  # ###
@@ -37,6 +50,25 @@ class EnvironmentFilter
37
50
  # }
38
51
  #
39
52
  # ###
53
+ # # Can do basic value conversions based on the raw data
54
+ # #
55
+ # ENV['LEVEL_ONE_1_LEVEL_TWO_1'] = '1'
56
+ # ENV['LEVEL_ONE_1_LEVEL_TWO_2_LEVEL_THREE_1'] = '[1, 2, 3]'
57
+ #
58
+ # EnvironmentFilter.execute(
59
+ # level_one_1: {
60
+ # level_two_1: 4,
61
+ # level_two_2: {
62
+ # level_three_1: [4, 5, 6] } } )
63
+ #
64
+ # # => {
65
+ # 'level_one_1' => {
66
+ # 'level_two_1' => 1,
67
+ # 'level_two_2' => {
68
+ # 'level_three_1' => [1, 2, 3],
69
+ # }
70
+ #
71
+ # ###
40
72
  # # Can inject environment variables if said variables are prefixed
41
73
  # #
42
74
  # ENV['PREFIX_LEVEL_TWO_1'] = 'env value 1'
@@ -58,19 +90,80 @@ class EnvironmentFilter
58
90
  new(options).__send__(:execute)
59
91
  end
60
92
 
61
- protected
93
+ attr_accessor :data,
94
+ :secure_key_token
62
95
 
63
- attr_accessor :data
96
+ def initialize(options = {})
97
+ self.data = options.fetch(:data)
98
+ self.secure_key_token = /\A#{Regexp.escape(options.fetch(:secure_key_prefix))}/
99
+ end
100
+
101
+ protected
64
102
 
65
103
  def execute(settings = data, parent_keys = [])
66
- with_environment(settings, parent_keys,
67
- lambda do |key, value, environment_keys|
68
- { key => execute(value, environment_keys) }
69
- end,
70
- lambda do |key, value, environment_key|
71
- { key => (ENV[environment_key] || value) }
72
- end)
104
+ with_environment(
105
+ settings,
106
+ parent_keys,
107
+ lambda do |key, value, environment_keys|
108
+ { key => execute(value, environment_keys) }
109
+ end,
110
+ lambda do |key, value, environment_key|
111
+ { key => convert_environment_value(ENV[environment_key], value) }
112
+ end,
113
+ )
114
+ end
115
+
116
+ private
117
+
118
+ def with_environment(settings, parent_keys, hash_block, value_block)
119
+ environment_hash = Hashie::Mash.new
120
+
121
+ settings.each_pair do |key, value|
122
+ environment_key = key.to_s.gsub(secure_key_token, '')
123
+ environment_keys = parent_keys.dup.push(environment_key)
124
+
125
+ if value.respond_to? :each_pair
126
+ environment_hash.merge!(hash_block.call(key, value, environment_keys))
127
+ else
128
+ environment_key = environment_keys.join('_').upcase
129
+
130
+ environment_hash.merge!(value_block.call(key, value, environment_key))
131
+ end
132
+ end
133
+
134
+ environment_hash
135
+ end
136
+
137
+ # rubocop:disable Metrics/CyclomaticComplexity
138
+ def convert_environment_value(environment_value, settings_value)
139
+ return settings_value unless environment_value
140
+ return if %w{___nil___ ___null___}.include?(environment_value)
141
+
142
+ case settings_value.class.name
143
+ when 'TrueClass', 'FalseClass'
144
+ case environment_value.downcase
145
+ when 'false', 'f', 'no', 'off', '0'
146
+ false
147
+ when 'true', 't', 'yes', 'on', '1'
148
+ true
149
+ else
150
+ fail ArgumentError, "Invalid value for Boolean: #{environment_value}"
151
+ end
152
+ when 'Float'
153
+ Float(environment_value)
154
+ when 'Array'
155
+ YAML.safe_load(environment_value).tap do |parsed_value|
156
+ unless parsed_value.is_a?(Array)
157
+ fail ArgumentError, "Invalid value for Array: #{environment_value}"
158
+ end
159
+ end
160
+ when 'Integer'
161
+ Integer(environment_value)
162
+ else
163
+ environment_value
164
+ end
73
165
  end
166
+ # rubocop:enable Metrics/CyclomaticComplexity
74
167
  end
75
168
  end
76
169
  end
@@ -1,23 +1,25 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'chamber/errors/decryption_failure'
3
4
 
4
5
  module Chamber
5
6
  module Filters
6
7
  class FailedDecryptionFilter
7
- SECURE_KEY_TOKEN = /\A_secure_/
8
8
  BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9\+/]{342}==\z}
9
9
 
10
- def initialize(options = {})
11
- self.data = options.fetch(:data).dup
12
- end
13
-
14
10
  def self.execute(options = {})
15
11
  new(options).__send__(:execute)
16
12
  end
17
13
 
18
- protected
14
+ attr_accessor :data,
15
+ :secure_key_token
19
16
 
20
- attr_accessor :data
17
+ def initialize(options = {})
18
+ self.data = options.fetch(:data).dup
19
+ self.secure_key_token = /\A#{Regexp.escape(options.fetch(:secure_key_prefix))}/
20
+ end
21
+
22
+ protected
21
23
 
22
24
  def execute(raw_data = data)
23
25
  settings = raw_data
@@ -25,7 +27,7 @@ class FailedDecryptionFilter
25
27
  raw_data.each_pair do |key, value|
26
28
  if value.respond_to? :each_pair
27
29
  execute(value)
28
- elsif key.match(SECURE_KEY_TOKEN) &&
30
+ elsif key.match(secure_key_token) &&
29
31
  value.respond_to?(:match) &&
30
32
  value.match(BASE64_STRING_PATTERN)
31
33
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'hashie/mash'
3
4
  require 'chamber/filters/secure_filter'
4
5
 
@@ -1,23 +1,24 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'hashie/mash'
3
4
 
4
5
  module Chamber
5
6
  module Filters
6
7
  class NamespaceFilter
7
- def initialize(options = {})
8
- self.data = Hashie::Mash.new(options.fetch(:data))
9
- self.namespaces = options.fetch(:namespaces)
10
- end
11
-
12
8
  def self.execute(options = {})
13
9
  new(options).__send__(:execute)
14
10
  end
15
11
 
16
- protected
17
-
18
12
  attr_accessor :data,
19
13
  :namespaces
20
14
 
15
+ def initialize(options = {})
16
+ self.data = Hashie::Mash.new(options.fetch(:data))
17
+ self.namespaces = options.fetch(:namespaces)
18
+ end
19
+
20
+ protected
21
+
21
22
  def execute
22
23
  if data_is_namespaced?
23
24
  namespaces.each_with_object(Hashie::Mash.new) do |namespace, filtered_data|