chef-vault 2.5.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +5 -1
  3. data/.travis.yml +5 -1
  4. data/Changelog.md +36 -3
  5. data/KNIFE_EXAMPLES.md +49 -74
  6. data/README.md +166 -104
  7. data/THEORY.md +4 -2
  8. data/bin/chef-vault +2 -2
  9. data/chef-vault.gemspec +2 -2
  10. data/features/clean_on_refresh.feature +28 -0
  11. data/features/isvault.feature +24 -0
  12. data/features/itemtype.feature +25 -0
  13. data/features/step_definitions/chef-databag.rb +4 -0
  14. data/features/step_definitions/chef-vault.rb +21 -0
  15. data/features/step_definitions/chef_databagitem.rb +9 -0
  16. data/features/vault_show_vaultname.feature +22 -0
  17. data/features/vault_update.feature +1 -1
  18. data/features/verify_id_matches.feature +11 -0
  19. data/lib/chef-vault/certificate.rb +1 -1
  20. data/lib/chef-vault/exceptions.rb +3 -0
  21. data/lib/chef-vault/item.rb +161 -18
  22. data/lib/chef-vault/user.rb +1 -1
  23. data/lib/chef-vault/version.rb +1 -1
  24. data/lib/chef/knife/decrypt.rb +1 -1
  25. data/lib/chef/knife/encrypt_create.rb +1 -1
  26. data/lib/chef/knife/encrypt_delete.rb +1 -1
  27. data/lib/chef/knife/encrypt_remove.rb +1 -1
  28. data/lib/chef/knife/encrypt_rotate_keys.rb +1 -1
  29. data/lib/chef/knife/encrypt_update.rb +1 -1
  30. data/lib/chef/knife/vault_base.rb +22 -0
  31. data/lib/chef/knife/vault_create.rb +0 -1
  32. data/lib/chef/knife/vault_decrypt.rb +1 -1
  33. data/lib/chef/knife/vault_isvault.rb +43 -0
  34. data/lib/chef/knife/vault_itemtype.rb +43 -0
  35. data/lib/chef/knife/vault_list.rb +7 -18
  36. data/lib/chef/knife/vault_refresh.rb +6 -12
  37. data/lib/chef/knife/vault_rotate_all_keys.rb +1 -1
  38. data/lib/chef/knife/vault_show.rb +15 -1
  39. data/lib/chef/knife/vault_update.rb +1 -1
  40. data/spec/chef-vault/certificate_spec.rb +9 -2
  41. data/spec/chef-vault/item_spec.rb +164 -3
  42. data/spec/chef-vault/user_spec.rb +9 -2
  43. metadata +14 -6
data/THEORY.md CHANGED
@@ -195,7 +195,7 @@ the vault create throws an error.
195
195
  The data bag 'foo' is then fetched (or created if it does
196
196
  not already exist). An encrypted data bag item named 'bar'
197
197
  is created, encrypted with the shared secret. The data bag
198
- item is save to the Chef server.
198
+ item is saved to the Chef server.
199
199
 
200
200
  ### Decrypting a Vault using knife
201
201
 
@@ -231,7 +231,9 @@ the server 'one'.
231
231
 
232
232
  Assuming that the recipe contains code such as this:
233
233
 
234
- chef_gem 'chef-vault'
234
+ chef_gem 'chef-vault' do
235
+ compile_time true if respond_to?(:compile_time)
236
+ end
235
237
  require 'chef-vault'
236
238
  vaultitem = ChefVault::Item.load('foo', 'bar')
237
239
 
@@ -89,9 +89,9 @@ require 'chef-vault'
89
89
  ChefVault.load_config(options[:chef])
90
90
  item = ChefVault::Item.load(options[:vault], options[:item])
91
91
 
92
- puts "#{options[:vault]}/#{options[:item]}"
92
+ $stdout.puts "#{options[:vault]}/#{options[:item]}"
93
93
 
94
94
  options[:values].split(",").each do |value|
95
95
  value.strip! # remove white space
96
- puts("\t#{value}: #{item[value]}")
96
+ $stdout.puts("\t#{value}: #{item[value]}")
97
97
  end
@@ -35,12 +35,12 @@ Gem::Specification.new do |s|
35
35
  s.executables = %w( chef-vault )
36
36
 
37
37
  s.add_development_dependency 'rake', '~> 10.4'
38
- s.add_development_dependency 'rspec', '~> 3.1'
38
+ s.add_development_dependency 'rspec', '~> 3.2'
39
39
  s.add_development_dependency 'rspec-its', '~> 1.1'
40
40
  s.add_development_dependency 'aruba', '~> 0.6'
41
41
  s.add_development_dependency 'simplecov', '~> 0.9'
42
42
  s.add_development_dependency 'simplecov-console', '~> 0.2'
43
- s.add_development_dependency 'rubocop', '~> 0.29'
43
+ s.add_development_dependency 'rubocop', '~> 0.30'
44
44
  # Chef 12 and higher pull in Ohai 8, which needs Ruby v2
45
45
  # so only in the case of a CI build on ruby v1, we constrain
46
46
  # chef to 11 or lower so that we can maintain CI test coverage
@@ -0,0 +1,28 @@
1
+ Feature: clean unknown clients on vault refresh
2
+
3
+ When refreshing a vault, new clients may be added if they match
4
+ the search query or client list, but old clients that no longer
5
+ exist will never be removed. The --clean-unknown-clients switch
6
+ will cause cause any unknown clients to be removed from the vault
7
+ item's access list as well
8
+
9
+ Scenario: Refresh without clean option
10
+ Given a local mode chef repo with nodes 'one,two,three'
11
+ And I create a vault item 'test/item' containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three'
12
+ Then the vault item 'test/item' should be encrypted for 'one,two,three'
13
+ And I delete client 'one' from the Chef server
14
+ And I refresh the vault item 'test/item'
15
+ And the vault item 'test/item' should be encrypted for 'one,two,three'
16
+ And 'one,two,three' should be a client for the vault item 'test/item'
17
+
18
+ Scenario: Refresh with clean option
19
+ Given a local mode chef repo with nodes 'one,two,three'
20
+ And I create a vault item 'test/item' containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three'
21
+ Then the vault item 'test/item' should be encrypted for 'one,two,three'
22
+ And I delete client 'one' from the Chef server
23
+ And I refresh the vault item 'test/item' with the 'clean-unknown-clients' option
24
+ Then the output should contain "Removing unknown client 'one'"
25
+ And the vault item 'test/item' should be encrypted for 'two,three'
26
+ And the vault item 'test/item' should not be encrypted for 'one'
27
+ And 'two,three' should be a client for the vault item 'test/item'
28
+ And 'one' should not be a client for the vault item 'test/item'
@@ -0,0 +1,24 @@
1
+ Feature: determine if a data bag item is a vault
2
+
3
+ If a data bag item is a vault, 'knife vault isvault VAULTNAME ITEMNAME'
4
+ should exit 0. Otherwise it should exit 1.
5
+
6
+ Scenario: detect vault item
7
+ Given a local mode chef repo with nodes 'one,two,three'
8
+ And I create a vault item 'test/item' containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three'
9
+ And I check if the data bag item 'test/item' is a vault
10
+ Then the exit status should be 0
11
+
12
+ Scenario: detect non-vault item (encrypted data bag)
13
+ Given a local mode chef repo with nodes 'one,two,three'
14
+ And I create an empty data bag 'test'
15
+ And I create an encrypted data bag item 'test/item' containing the JSON '{"id": "item", "foo": "bar"}' with the secret 'sekrit'
16
+ And I check if the data bag item 'test/item' is a vault
17
+ Then the exit status should not be 0
18
+
19
+ Scenario: detect non-vault item (normal data bag)
20
+ Given a local mode chef repo with nodes 'one,two,three'
21
+ And I create an empty data bag 'test'
22
+ And I create a data bag item 'test/item' containing the JSON '{"id": "item", "foo": "bar"}'
23
+ And I check if the data bag item 'test/item' is a vault
24
+ Then the exit status should not be 0
@@ -0,0 +1,25 @@
1
+ Feature: determine the type of a data bag item
2
+
3
+ 'knife vault itemtype VAULTNAME ITEMNAME' should output one of
4
+ 'normal', 'encrypted', or 'vault' depending on what type of item
5
+ it detects
6
+
7
+ Scenario: detect vault item
8
+ Given a local mode chef repo with nodes 'one,two,three'
9
+ And I create a vault item 'test/item' containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three'
10
+ And I check the type of the data bag item 'test/item'
11
+ Then the output should match /^vault$/
12
+
13
+ Scenario: detect non-vault item (encrypted data bag)
14
+ Given a local mode chef repo with nodes 'one,two,three'
15
+ And I create an empty data bag 'test'
16
+ And I create an encrypted data bag item 'test/item' containing the JSON '{"id": "item", "foo": "bar"}' with the secret 'sekrit'
17
+ And I check the type of the data bag item 'test/item'
18
+ Then the output should match /^encrypted$/
19
+
20
+ Scenario: detect non-vault item (normal data bag)
21
+ Given a local mode chef repo with nodes 'one,two,three'
22
+ And I create an empty data bag 'test'
23
+ And I create a data bag item 'test/item' containing the JSON '{"id": "item", "foo": "bar"}'
24
+ And I check the type of the data bag item 'test/item'
25
+ Then the output should match /^normal$/
@@ -3,3 +3,7 @@ When /^I create a data bag '(.+)' containing the JSON '(.+)'$/ do |bag, json|
3
3
  run_simple "knife data bag create #{bag} -z -c knife.rb -d"
4
4
  run_simple "knife data bag from_file #{bag} -z -c knife.rb item.json"
5
5
  end
6
+
7
+ Given(/^I create an empty data bag '(.+)'$/) do |databag|
8
+ run_simple "knife data bag create #{databag} -z -c knife.rb", false
9
+ end
@@ -28,6 +28,15 @@ Given(/^I rotate all keys with the '(.+)' options?$/) do |optionlist|
28
28
  run_simple "knife vault rotate all keys -z -c knife.rb #{options}"
29
29
  end
30
30
 
31
+ Given(/^I refresh the vault item '(.+)\/(.+)'$/) do |vault, item|
32
+ run_simple "knife vault refresh #{vault} #{item} -c knife.rb -z"
33
+ end
34
+
35
+ Given(/^I refresh the vault item '(.+)\/(.+)' with the '(.+)' options?$/) do |vault, item, optionlist|
36
+ options = optionlist.split(/,/).map{|o| "--#{o}"}.join(' ')
37
+ run_simple "knife vault refresh #{vault} #{item} -c knife.rb -z #{options}"
38
+ end
39
+
31
40
  Given(/^I try to decrypt the vault item '(.+)\/(.+)' as '(.+)'$/) do |vault, item, node|
32
41
  run_simple "knife vault show #{vault} #{item} -z -c knife.rb -u #{node} -k #{node}.pem", false
33
42
  end
@@ -93,3 +102,15 @@ end
93
102
  Given(/^I add '(.+)' as an admin for the vault item '(.+)\/(.+)'$/) do |newadmin, vault, item|
94
103
  run_simple "knife vault update #{vault} #{item} -c knife.rb -z -A #{newadmin}"
95
104
  end
105
+
106
+ Given(/^I show the keys of the vault '(.+)'$/) do |vault|
107
+ run_simple "knife vault show #{vault} -c knife.rb -z"
108
+ end
109
+
110
+ Given(/^I check if the data bag item '(.+)\/(.+)' is a vault$/) do |vault, item|
111
+ run_simple "knife vault isvault #{vault} #{item} -c knife.rb -z", false
112
+ end
113
+
114
+ Given(/^I check the type of the data bag item '(.+)\/(.+)'$/) do |vault, item|
115
+ run_simple "knife vault itemtype #{vault} #{item} -c knife.rb -z"
116
+ end
@@ -0,0 +1,9 @@
1
+ Given(/^I create a data bag item '(.+)\/(.+)' containing the JSON '(.+)'$/) do |databag, _, json|
2
+ write_file 'item.json', json
3
+ run_simple "knife data bag from file #{databag} item.json -z -c knife.rb", false
4
+ end
5
+
6
+ Given(/^I create an encrypted data bag item '(.+)\/(.+)' containing the JSON '(.+)' with the secret '(.+)'$/) do |databag, _, json, secret|
7
+ write_file 'item.json', json
8
+ run_simple "knife data bag from file #{databag} item.json -s #{secret} -z -c knife.rb", false
9
+ end
@@ -0,0 +1,22 @@
1
+ Feature: knife vault show [VAULTNAME]
2
+
3
+ 'knife vault show [VAULTNAME]' displays the keys of a vault
4
+ (i.e. the items that are not suffixed with _keys)
5
+
6
+ Scenario: show keys of a vault
7
+ Given a local mode chef repo with nodes 'one,two,three'
8
+ And I create a vault item 'test/item' containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three'
9
+ And I create a vault item 'test/item2' containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three'
10
+ Then the vault item 'test/item' should be encrypted for 'one,two,three'
11
+ And 'one,two,three' should be a client for the vault item 'test/item'
12
+ And I show the keys of the vault 'test'
13
+ Then the output should match /(?m:^item$)/
14
+ And the output should match /(?m:^item2$)/
15
+ And the output should not match /(?m:^item_keys$)/
16
+ And the output should not match /(?m:^item2_keys$)/
17
+
18
+ Scenario: show keys of a data bag that is not a vault
19
+ Given a local mode chef repo with nodes 'one,two,three'
20
+ And I create a data bag 'notavault' containing the JSON '{"id": "item", "foo": "bar"}'
21
+ And I show the keys of the vault 'notavault'
22
+ Then the output should match /data bag notavault is not a chef-vault/
@@ -1,4 +1,4 @@
1
- Feature: knife vault create
1
+ Feature: knife vault update
2
2
 
3
3
  'knife vault update' is used to add clients, or administrators
4
4
  and to re-run the search query
@@ -0,0 +1,11 @@
1
+ Feature: knife vault create with mismatched ID
2
+
3
+ 'knife vault create' creates a vault. A JSON file can be passed
4
+ on the command line. If the vault ID specified on the command line
5
+ does not match the value of the 'id' key in the JSON file, knife
6
+ should throw an error
7
+
8
+ Scenario: create vault from JSON file with mismatched ID
9
+ Given a local mode chef repo with nodes 'one,two,three'
10
+ And I create a vault item 'test/item' containing the JSON '{"id": "eyetem"}' encrypted for 'one,two,three'
11
+ Then the output should match /id mismatch - input JSON has id 'eyetem' but vault item has id 'item'/
@@ -24,7 +24,7 @@ class ChefVault
24
24
  end
25
25
 
26
26
  def decrypt_contents
27
- puts "WARNING: This method is deprecated, please switch to item['value'] calls"
27
+ $stdout.puts "WARNING: This method is deprecated, please switch to item['value'] calls"
28
28
  @item["contents"]
29
29
  end
30
30
  end
@@ -45,5 +45,8 @@ class ChefVault
45
45
 
46
46
  class SearchNotFound < Exceptions
47
47
  end
48
+
49
+ class IdMismatch < Exceptions
50
+ end
48
51
  end
49
52
  end
@@ -18,16 +18,52 @@ require 'securerandom'
18
18
 
19
19
  class ChefVault
20
20
  class Item < Chef::DataBagItem
21
+ # @!attribute [rw] keys
22
+ # @return [ChefVault::ItemKeys] the keys associated with this vault
21
23
  attr_accessor :keys
24
+
25
+ # @!attribute [rw] encrypted_data_bag_item
26
+ # @return [nil] this attribute is not currently used
22
27
  attr_accessor :encrypted_data_bag_item
23
28
 
24
- def initialize(vault, name)
29
+ # @!attribute [rw] node_name
30
+ # @return [String] the node name that is used to decrypt secrets.
31
+ # Defaults to the value of Chef::Config[:node_name]
32
+ attr_accessor :node_name
33
+
34
+ # @!attribute [rw] client_key_path
35
+ # @return [String] the path to the private key that is used to
36
+ # decrypt secrets. Defaults to the value of Chef::Config[:client_key]
37
+ attr_accessor :client_key_path
38
+
39
+ # returns the raw keys of the underlying Chef::DataBagItem. chef-vault v2
40
+ # defined #keys as a public accessor that returns the ChefVault::ItemKeys
41
+ # object for the vault. Ideally, #keys would provide Hash-like behaviour
42
+ # as it does for Chef::DataBagItem, but this would break the API. We will
43
+ # revisit this as part of the 3.x rewrite
44
+ def_delegator :@raw_data, :keys, :raw_keys
45
+
46
+ # constructs a new ChefVault::Item
47
+ # @param vault [String] the name of the data bag that contains the vault
48
+ # @param name [String] the name of the item in the vault
49
+ # @param opts [Hash]
50
+ # @option opts [String] :node_name the name of the node to decrypt secrets
51
+ # as. Defaults to the :node_name value of Chef::Config
52
+ # @option opts [String] :client_key_path the name of the node to decrypt
53
+ # secrets as. Defaults to the :client_key value of Chef::Config
54
+ def initialize(vault, name, opts = {})
25
55
  super() # Don't pass parameters
26
56
  @data_bag = vault
27
57
  @raw_data["id"] = name
28
58
  @keys = ChefVault::ItemKeys.new(vault, "#{name}_keys")
29
59
  @secret = generate_secret
30
60
  @encrypted = false
61
+ opts = {
62
+ :node_name => Chef::Config[:node_name],
63
+ :client_key_path => Chef::Config[:client_key]
64
+ }.merge(opts)
65
+ @node_name = opts[:node_name]
66
+ @client_key_path = opts[:client_key_path]
31
67
  end
32
68
 
33
69
  def load_keys(vault, keys)
@@ -35,23 +71,24 @@ class ChefVault
35
71
  @secret = secret
36
72
  end
37
73
 
38
- def clients(search=nil, action=:add)
39
- if search
74
+ def clients(search_or_client=nil, action=:add)
75
+ if search_or_client.is_a?(Chef::ApiClient)
76
+ handle_client_action(search_or_client, action)
77
+ elsif search_or_client
40
78
  results_returned = false
41
-
42
79
  query = Chef::Search::Query.new
43
- query.search(:node, search)[0].each do |node|
80
+ query.search(:node, search_or_client)[0].each do |node|
44
81
  results_returned = true
45
-
46
82
  case action
47
83
  when :add
48
84
  begin
49
- keys.add(load_client(node.name), @secret, "clients")
85
+ client = load_client(node.name)
86
+ add_client(client)
50
87
  rescue ChefVault::Exceptions::ClientNotFound
51
- $stderr.puts "node '#{node.name}' has no private key; skipping"
88
+ $stdout.puts "node '#{node.name}' has no private key; skipping"
52
89
  end
53
90
  when :delete
54
- keys.delete(node.name, "clients")
91
+ delete_client_or_node(node)
55
92
  else
56
93
  raise ChefVault::Exceptions::KeysActionNotValid,
57
94
  "#{action} is not a valid action"
@@ -59,7 +96,7 @@ class ChefVault
59
96
  end
60
97
 
61
98
  unless results_returned
62
- puts "WARNING: No clients were returned from search, you may not have "\
99
+ $stdout.puts "WARNING: No clients were returned from search, you may not have "\
63
100
  "got what you expected!!"
64
101
  end
65
102
  else
@@ -99,10 +136,10 @@ class ChefVault
99
136
  end
100
137
 
101
138
  def secret
102
- if @keys.include?(Chef::Config[:node_name])
103
- private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
139
+ if @keys.include?(@node_name)
140
+ private_key = OpenSSL::PKey::RSA.new(File.open(@client_key_path).read())
104
141
  begin
105
- private_key.private_decrypt(Base64.decode64(@keys[Chef::Config[:node_name]]))
142
+ private_key.private_decrypt(Base64.decode64(@keys[@node_name]))
106
143
  rescue OpenSSL::PKey::RSAError
107
144
  raise ChefVault::Exceptions::SecretDecryption,
108
145
  "#{data_bag}/#{id} is encrypted for you, but your private key failed to decrypt the contents. "\
@@ -159,6 +196,14 @@ class ChefVault
159
196
  # validate the format of the id before attempting to save
160
197
  validate_id!(item_id)
161
198
 
199
+ # ensure that the ID of the vault hasn't changed since the keys
200
+ # data bag item was created
201
+ keys_id = keys['id'].match(/^(.+)_keys/)[1]
202
+ if keys_id != item_id
203
+ raise ChefVault::Exceptions::IdMismatch,
204
+ "id mismatch - input JSON has id '#{item_id}' but vault item has id '#{keys_id}'"
205
+ end
206
+
162
207
  # save the keys first, raising an error if no keys were defined
163
208
  if keys.admins.empty? && keys.clients.empty?
164
209
  raise ChefVault::Exceptions::NoKeysDefined,
@@ -218,8 +263,11 @@ class ChefVault
218
263
  end
219
264
  end
220
265
 
221
- def self.load(vault, name)
222
- item = new(vault, name)
266
+ # loads an existing vault item
267
+ # @param (see #initialize)
268
+ # @option (see #initialize)
269
+ def self.load(vault, name, opts = {})
270
+ item = new(vault, name, opts)
223
271
  item.load_keys(vault, "#{name}_keys")
224
272
 
225
273
  begin
@@ -240,6 +288,74 @@ class ChefVault
240
288
  item
241
289
  end
242
290
 
291
+ # determines if a data bag item looks like a vault
292
+ # @param vault [String] the name of the data bag
293
+ # @param name [String] the name of the item in the data bag
294
+ # @return [Boolean] true if the data bag item looks like a vault
295
+ def self.vault?(vault, name)
296
+ :vault == data_bag_item_type(vault, name)
297
+ end
298
+
299
+ # determines whether a data bag item is a vault, an encrypted
300
+ # data bag item, or a normal data bag item. An item is a vault
301
+ # if:
302
+ #
303
+ # a) the data bag item contains at least one key whose value is
304
+ # an hash with the key 'encrypted data'
305
+ # b) the data bag that contains the item contains a second item
306
+ # suffixed with _keys
307
+ #
308
+ # if a) is false, the item is a normal data bag
309
+ # if a) and b) are true, the item is a vault
310
+ # if a) is true but b) is false, the item is an encrypted data
311
+ # bag item
312
+ # @param vault [String] the name of the data bag
313
+ # @param name [String] the name of the item in the data bag
314
+ # @return [Symbol] one of :vault, :encrypted or :normal
315
+ def self.data_bag_item_type(vault, name)
316
+ # adapted from https://github.com/opscode-cookbooks/chef-vault/blob/v1.3.0/libraries/chef_vault_item.rb
317
+ # and https://github.com/sensu/sensu-chef/blob/2.9.0/libraries/sensu_helpers.rb
318
+ dbi = Chef::DataBagItem.load(vault, name)
319
+ encrypted = dbi.detect do |_, v|
320
+ v.is_a?(Hash) && v.key?('encrypted_data')
321
+ end
322
+
323
+ # return a symbol describing the type of item we detected
324
+ case
325
+ when encrypted && Chef::DataBag.load(vault).key?("#{name}_keys")
326
+ :vault
327
+ when encrypted
328
+ :encrypted
329
+ else
330
+ :normal
331
+ end
332
+ end
333
+
334
+ # refreshes a vault by re-processing the search query and
335
+ # adding a secret for any nodes found (including new ones)
336
+ # @param clean_unknown_clients [Boolean] remove clients that can
337
+ # no longer be found
338
+ # @return [void]
339
+ def refresh(clean_unknown_clients = false)
340
+ unless search
341
+ raise ChefVault::Exceptions::SearchNotFound,
342
+ "#{vault}/#{item} does not have a stored search_query, "\
343
+ 'probably because it was created with an older version '\
344
+ "of chef-vault. Use 'knife vault update' to update the "\
345
+ 'databag with the search query.'
346
+ end
347
+
348
+ # a bit of a misnomer; this doesn't remove unknown
349
+ # admins, just clients which are nodes
350
+ remove_unknown_nodes if clean_unknown_clients
351
+
352
+ # re-process the search query to add new clients
353
+ clients(search)
354
+
355
+ # save the updated vault
356
+ save
357
+ end
358
+
243
359
  private
244
360
 
245
361
  def encrypt!
@@ -261,7 +377,7 @@ class ChefVault
261
377
  rescue Net::HTTPServerException => http_error
262
378
  if http_error.response.code == "404"
263
379
  begin
264
- puts "WARNING: #{admin} not found in users, trying clients."
380
+ $stdout.puts "WARNING: #{admin} not found in users, trying clients."
265
381
  admin = load_client(admin)
266
382
  rescue ChefVault::Exceptions::ClientNotFound
267
383
  raise ChefVault::Exceptions::AdminNotFound,
@@ -291,7 +407,7 @@ class ChefVault
291
407
  end
292
408
 
293
409
  # removes unknown nodes by performing a node search
294
- # for each of the existing nodclientses. If the search
410
+ # for each of the existing clients. If the search
295
411
  # returns nothing or the client cannot be loaded, then
296
412
  # we remove that client from the vault
297
413
  # @return [void]
@@ -304,7 +420,7 @@ class ChefVault
304
420
  end
305
421
  # now delete any flagged clients from the keys data bag
306
422
  clients_to_remove.each do |client|
307
- puts "Removing unknown client '#{client}'"
423
+ $stdout.puts "Removing unknown client '#{client}'"
308
424
  keys.delete(client, "clients")
309
425
  end
310
426
  end
@@ -340,5 +456,32 @@ class ChefVault
340
456
  end
341
457
  true
342
458
  end
459
+
460
+ # adds or deletes an API client from the vault item keys
461
+ # @param client [Chef::ApiClient] the API client to operate on
462
+ # @param action [Symbol] :add or :delete
463
+ # @return [void]
464
+ def handle_client_action(client, action)
465
+ case action
466
+ when :add
467
+ add_client(client)
468
+ when :delete
469
+ delete_client_or_node(client)
470
+ end
471
+ end
472
+
473
+ # adds a client to the vault item keys
474
+ # @param client [Chef::ApiClient] the API client to add
475
+ # @return [void]
476
+ def add_client(client)
477
+ keys.add(client, @secret, 'clients')
478
+ end
479
+
480
+ # removes a client to the vault item keys
481
+ # @param client_or_node [Chef::ApiClient,Chef::Node] the API client or node to remove
482
+ # @return [void]
483
+ def delete_client_or_node(client_or_node)
484
+ keys.delete(client_or_node.name, 'clients')
485
+ end
343
486
  end
344
487
  end