chef-vault 1.2.5 → 2.0.1.pre

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.
@@ -17,19 +17,23 @@
17
17
  #
18
18
 
19
19
  require 'chef'
20
+ require 'chef/user'
21
+ require 'chef-vault/version'
22
+ require 'chef-vault/exceptions'
23
+ require 'chef-vault/item'
24
+ require 'chef-vault/item_keys'
20
25
  require 'chef-vault/user'
21
26
  require 'chef-vault/certificate'
22
- require 'chef-vault/version'
23
- require 'chef-vault/chef/offline'
27
+ require 'chef-vault/chef_patch/api_client'
28
+ require 'chef-vault/chef_patch/user'
24
29
 
25
30
  class ChefVault
26
31
 
27
- attr_accessor :data_bag
28
- attr_accessor :chef_config_file
32
+ attr_accessor :vault
29
33
 
30
- def initialize(data_bag, chef_config_file=nil)
31
- @data_bag = data_bag
32
- @chef_config_file = chef_config_file
34
+ def initialize(vault, chef_config_file=nil)
35
+ @vault = vault
36
+ ChefVault.load_config(chef_config_file) if chef_config_file
33
37
  end
34
38
 
35
39
  def version
@@ -37,10 +41,14 @@ class ChefVault
37
41
  end
38
42
 
39
43
  def user(username)
40
- ChefVault::User.new(data_bag, username, chef_config_file)
44
+ ChefVault::User.new(vault, username)
41
45
  end
42
46
 
43
47
  def certificate(name)
44
- ChefVault::Certificate.new(data_bag, name, chef_config_file)
48
+ ChefVault::Certificate.new(vault, name)
49
+ end
50
+
51
+ def self.load_config(chef_config_file)
52
+ Chef::Config.from_file(chef_config_file)
45
53
  end
46
54
  end
@@ -15,38 +15,17 @@
15
15
 
16
16
  class ChefVault
17
17
  class Certificate
18
- attr_accessor :name
19
-
20
- def initialize(data_bag, name, chef_config_file)
21
- @name = name
22
- @data_bag = data_bag
18
+ def initialize(vault, name)
19
+ @item = ChefVault::Item.load(vault, name)
20
+ end
23
21
 
24
- if chef_config_file
25
- chef = ChefVault::ChefOffline.new(chef_config_file)
26
- chef.connect
27
- end
22
+ def [](key)
23
+ @item[key]
28
24
  end
29
25
 
30
26
  def decrypt_contents
31
- # use the private client_key file to create a decryptor
32
- private_key = open(Chef::Config[:client_key]).read
33
- private_key = OpenSSL::PKey::RSA.new(private_key)
34
-
35
- begin
36
- keys = Chef::DataBagItem.load(@data_bag, "#{name}_keys")
37
- rescue
38
- throw "Could not find data bag item #{name}_keys in data bag #{@data_bag}"
39
- end
40
-
41
- unless keys[Chef::Config[:node_name]]
42
- throw "#{name} is not encrypted for you! Rebuild the certificate data bag"
43
- end
44
-
45
- node_key = Base64.decode64(keys[Chef::Config[:node_name]])
46
- shared_secret = private_key.private_decrypt(node_key)
47
- certificate = Chef::EncryptedDataBagItem.load(@data_bag, @name, shared_secret)
48
-
49
- certificate["contents"]
27
+ puts "WARNING: This method is deprecated, please switch to item['value'] calls"
28
+ @item["contents"]
50
29
  end
51
30
  end
52
31
  end
@@ -0,0 +1,40 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault
18
+ module ChefPatch
19
+ class ApiClient < Chef::ApiClient
20
+ # Fix an issue where core Chef::ApiClient does not load
21
+ # the private key for Chef 10
22
+ def self.load(name)
23
+ response = http_api.get("clients/#{name}")
24
+ if response.kind_of?(Chef::ApiClient)
25
+ response
26
+ else
27
+ client = Chef::ApiClient.new
28
+ client.name(response['clientname'])
29
+
30
+ if response['certificate']
31
+ der = OpenSSL::X509::Certificate.new response['certificate']
32
+ client.public_key der.public_key.to_s
33
+ end
34
+
35
+ client
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault
18
+ module ChefPatch
19
+ class User < Chef::User
20
+ # def from_hash for our implementation because name is not being
21
+ # set correctly for Chef 10 server
22
+ def superclass.from_hash(user_hash)
23
+ user = Chef::User.new
24
+ user.name user_hash['username'] ? user_hash['username'] : user_hash['name']
25
+ user.private_key user_hash['private_key'] if user_hash.key?('private_key')
26
+ user.password user_hash['password'] if user_hash.key?('password')
27
+ user.public_key user_hash['public_key']
28
+ user.admin user_hash['admin']
29
+ user
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault::Exceptions
18
+ class SecretDecryption < RuntimeError; end
19
+ class NoKeysDefined < RuntimeError; end
20
+ class ItemNotEncrypted < RuntimeError; end
21
+ class KeysActionNotValue < RuntimeError; end
22
+ class AdminNotFound < RuntimeError; end
23
+ class ClientNotFound < RuntimeError; end
24
+ class KeysNotFound < RuntimeError; end
25
+ class ItemNotFound < RuntimeError; end
26
+ class ItemAlreadyExists < RuntimeError; end
27
+ end
@@ -0,0 +1,243 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault::Item < Chef::DataBagItem
18
+ attr_accessor :keys
19
+ attr_accessor :encrypted_data_bag_item
20
+
21
+ def initialize(vault, name)
22
+ super() # Don't pass parameters
23
+ @data_bag = vault
24
+ @raw_data["id"] = name
25
+ @keys = ChefVault::ItemKeys.new(vault, "#{name}_keys")
26
+ @secret = generate_secret
27
+ @encrypted = false
28
+ end
29
+
30
+ def load_keys(vault, keys)
31
+ @keys = ChefVault::ItemKeys.load(vault, keys)
32
+ @secret = secret
33
+ end
34
+
35
+ def clients(search=nil, action=:add)
36
+ if search
37
+ results_returned = false
38
+
39
+ query = Chef::Search::Query.new
40
+ query.search(:node, search)[0].each do |node|
41
+ results_returned = true
42
+
43
+ case action
44
+ when :add
45
+ begin
46
+ keys.add(ChefVault::ChefPatch::ApiClient.load(node.name), @secret, "clients")
47
+ rescue Net::HTTPServerException => http_error
48
+ if http_error.response.code == "404"
49
+ raise ChefVault::Exceptions::ClientNotFound,
50
+ "#{node.name} is not a valid chef client and/or node"
51
+ else
52
+ raise http_error
53
+ end
54
+ end
55
+ when :delete
56
+ keys.delete(node.name, "clients")
57
+ else
58
+ raise ChefVault::Exceptions::KeysActionNotValid,
59
+ "#{action} is not a valid action"
60
+ end
61
+ end
62
+
63
+ unless results_returned
64
+ puts "WARNING: No clients were returned from search, you may not have "\
65
+ "got what you expected!!"
66
+ end
67
+ else
68
+ keys.clients
69
+ end
70
+ end
71
+
72
+ def admins(admins=nil, action=:add)
73
+ if admins
74
+ admins.split(",").each do |admin|
75
+ admin.strip!
76
+ case action
77
+ when :add
78
+ begin
79
+ keys.add(ChefVault::ChefPatch::User.load(admin), @secret, "admins")
80
+ rescue Net::HTTPServerException => http_error
81
+ if http_error.response.code == "404"
82
+ raise ChefVault::Exceptions::AdminNotFound,
83
+ "#{admin} is not a valid chef admin"
84
+ else
85
+ raise http_error
86
+ end
87
+ end
88
+ when :delete
89
+ keys.delete(admin, "admins")
90
+ else
91
+ raise ChefVault::Exceptions::KeysActionNotValid,
92
+ "#{action} is not a valid action"
93
+ end
94
+ end
95
+ else
96
+ keys.admins
97
+ end
98
+ end
99
+
100
+ def remove(key)
101
+ @raw_data.delete(key)
102
+ end
103
+
104
+ def secret
105
+ if @keys.include?(Chef::Config[:node_name])
106
+ private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
107
+ private_key.private_decrypt(Base64.decode64(@keys[Chef::Config[:node_name]]))
108
+ else
109
+ raise ChefVault::Exceptions::SecretDecryption,
110
+ "#{data_bag}/#{id} is not encrypted with your public key. "\
111
+ "Contact an administrator of the vault item to encrypt for you!"
112
+ end
113
+ end
114
+
115
+ def rotate_keys!
116
+ @secret = generate_secret
117
+
118
+ unless clients.empty?
119
+ clients.each do |client|
120
+ clients("name:#{client}")
121
+ end
122
+ end
123
+
124
+ unless admins.empty?
125
+ admins.each do |admin|
126
+ admins(admin)
127
+ end
128
+ end
129
+
130
+ save
131
+ reload_raw_data
132
+ end
133
+
134
+ def generate_secret
135
+ OpenSSL::PKey::RSA.new(245).to_pem.lines.to_a[1..-2].join
136
+ end
137
+
138
+ def []=(key, value)
139
+ reload_raw_data if @encrypted
140
+ super
141
+ end
142
+
143
+ def [](key)
144
+ reload_raw_data if @encrypted
145
+ super
146
+ end
147
+
148
+ def save(item_id=@raw_data['id'])
149
+ # save the keys first, raising an error if no keys were defined
150
+ if keys.admins.empty? && keys.clients.empty?
151
+ raise ChefVault::Exceptions::NoKeysDefined,
152
+ "No keys defined for #{item_id}"
153
+ end
154
+
155
+ keys.save
156
+
157
+ # Make sure the item is encrypted before saving
158
+ encrypt! unless @encrypted
159
+
160
+ # Now save the encrypted data
161
+ if Chef::Config[:solo]
162
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
163
+ data_bag)
164
+ data_bag_item_path = File.join(data_bag_path, item_id)
165
+
166
+ FileUtils.mkdir(data_bag_path) unless File.exists?(data_bag_path)
167
+ File.open("#{data_bag_item_path}.json",'w') do |file|
168
+ file.write(JSON.pretty_generate(self))
169
+ end
170
+
171
+ self
172
+ else
173
+ begin
174
+ chef_data_bag = Chef::DataBag.load(data_bag)
175
+ rescue Net::HTTPServerException => http_error
176
+ if http_error.response.code == "404"
177
+ chef_data_bag = Chef::DataBag.new
178
+ chef_data_bag.name data_bag
179
+ chef_data_bag.create
180
+ end
181
+ end
182
+
183
+ super
184
+ end
185
+ end
186
+
187
+ def to_json(*a)
188
+ json = super
189
+ json.gsub(self.class.name, self.class.superclass.name)
190
+ end
191
+
192
+ def destroy
193
+ keys.destroy
194
+
195
+ if Chef::Config[:solo]
196
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
197
+ data_bag)
198
+ data_bag_item_path = File.join(data_bag_path, @raw_data["id"])
199
+
200
+ FileUtils.rm("#{data_bag_item_path}.json")
201
+
202
+ nil
203
+ else
204
+ super(data_bag, id)
205
+ end
206
+ end
207
+
208
+ def self.load(vault, name)
209
+ item = new(vault, name)
210
+ item.load_keys(vault, "#{name}_keys")
211
+
212
+ begin
213
+ item.raw_data =
214
+ Chef::EncryptedDataBagItem.load(vault, name, item.secret).to_hash
215
+ rescue Net::HTTPServerException => http_error
216
+ if http_error.response.code == "404"
217
+ raise ChefVault::Exceptions::ItemNotFound,
218
+ "#{vault}/#{name} could not be found"
219
+ else
220
+ raise http_error
221
+ end
222
+ rescue Chef::Exceptions::ValidationFailed
223
+ raise ChefVault::Exceptions::ItemNotFound,
224
+ "#{vault}/#{name} could not be found"
225
+ end
226
+
227
+ item
228
+ end
229
+
230
+ private
231
+ def encrypt!
232
+ @raw_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(self, @secret)
233
+ @encrypted = true
234
+ end
235
+
236
+ def reload_raw_data
237
+ @raw_data =
238
+ Chef::EncryptedDataBagItem.load(@data_bag, @raw_data["id"], secret).to_hash
239
+ @encrypted = false
240
+
241
+ @raw_data
242
+ end
243
+ end
@@ -0,0 +1,121 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault::ItemKeys < Chef::DataBagItem
18
+ def initialize(vault, name)
19
+ super() # parenthesis required to strip off parameters
20
+ @data_bag = vault
21
+ @raw_data["id"] = name
22
+ @raw_data["admins"] = []
23
+ @raw_data["clients"] = []
24
+ end
25
+
26
+ def include?(key)
27
+ @raw_data.keys.include?(key)
28
+ end
29
+
30
+ def add(chef_client, data_bag_shared_secret, type)
31
+ public_key = OpenSSL::PKey::RSA.new chef_client.public_key
32
+ self[chef_client.name] =
33
+ Base64.encode64(public_key.public_encrypt(data_bag_shared_secret))
34
+
35
+ @raw_data[type] << chef_client.name unless @raw_data[type].include?(chef_client.name)
36
+ @raw_data[type]
37
+ end
38
+
39
+ def delete(chef_client, type)
40
+ raw_data.delete(chef_client)
41
+ raw_data[type].delete(chef_client)
42
+ end
43
+
44
+ def clients
45
+ @raw_data["clients"]
46
+ end
47
+
48
+ def admins
49
+ @raw_data["admins"]
50
+ end
51
+
52
+ def save(item_id=@raw_data['id'])
53
+ if Chef::Config[:solo]
54
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
55
+ data_bag)
56
+ data_bag_item_path = File.join(data_bag_path, item_id)
57
+
58
+ FileUtils.mkdir(data_bag_path) unless File.exists?(data_bag_path)
59
+ File.open("#{data_bag_item_path}.json",'w') do |file|
60
+ file.write(JSON.pretty_generate(self))
61
+ end
62
+
63
+ self
64
+ else
65
+ begin
66
+ chef_data_bag = Chef::DataBag.load(data_bag)
67
+ rescue Net::HTTPServerException => http_error
68
+ if http_error.response.code == "404"
69
+ chef_data_bag = Chef::DataBag.new
70
+ chef_data_bag.name data_bag
71
+ chef_data_bag.create
72
+ end
73
+ end
74
+
75
+ super
76
+ end
77
+ end
78
+
79
+ def destroy
80
+ if Chef::Config[:solo]
81
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
82
+ data_bag)
83
+ data_bag_item_path = File.join(data_bag_path, @raw_data["id"])
84
+
85
+ FileUtils.rm("#{data_bag_item_path}.json")
86
+
87
+ nil
88
+ else
89
+ super(data_bag, id)
90
+ end
91
+ end
92
+
93
+ def to_json(*a)
94
+ json = super
95
+ json.gsub(self.class.name, self.class.superclass.name)
96
+ end
97
+
98
+ def self.from_data_bag_item(data_bag_item)
99
+ item = new(data_bag_item.data_bag, data_bag_item.name)
100
+ item.raw_data = data_bag_item.raw_data
101
+ item
102
+ end
103
+
104
+ def self.load(vault, name)
105
+ begin
106
+ data_bag_item = Chef::DataBagItem.load(vault, name)
107
+ rescue Net::HTTPServerException => http_error
108
+ if http_error.response.code == "404"
109
+ raise ChefVault::Exceptions::KeysNotFound,
110
+ "#{vault}/#{name} could not be found"
111
+ else
112
+ raise http_error
113
+ end
114
+ rescue Chef::Exceptions::ValidationFailed
115
+ raise ChefVault::Exceptions::KeysNotFound,
116
+ "#{vault}/#{name} could not be found"
117
+ end
118
+
119
+ from_data_bag_item(data_bag_item)
120
+ end
121
+ end