cheffish 1.4.0 → 1.4.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +201 -201
  3. data/README.md +120 -120
  4. data/Rakefile +23 -23
  5. data/lib/chef/provider/chef_acl.rb +439 -439
  6. data/lib/chef/provider/chef_client.rb +53 -53
  7. data/lib/chef/provider/chef_container.rb +55 -55
  8. data/lib/chef/provider/chef_data_bag.rb +55 -55
  9. data/lib/chef/provider/chef_data_bag_item.rb +278 -278
  10. data/lib/chef/provider/chef_environment.rb +83 -83
  11. data/lib/chef/provider/chef_group.rb +83 -83
  12. data/lib/chef/provider/chef_mirror.rb +169 -169
  13. data/lib/chef/provider/chef_node.rb +87 -87
  14. data/lib/chef/provider/chef_organization.rb +155 -155
  15. data/lib/chef/provider/chef_resolved_cookbooks.rb +46 -46
  16. data/lib/chef/provider/chef_role.rb +84 -84
  17. data/lib/chef/provider/chef_user.rb +59 -59
  18. data/lib/chef/provider/private_key.rb +225 -225
  19. data/lib/chef/provider/public_key.rb +88 -88
  20. data/lib/chef/resource/chef_acl.rb +69 -69
  21. data/lib/chef/resource/chef_client.rb +48 -48
  22. data/lib/chef/resource/chef_container.rb +22 -22
  23. data/lib/chef/resource/chef_data_bag.rb +22 -22
  24. data/lib/chef/resource/chef_data_bag_item.rb +121 -121
  25. data/lib/chef/resource/chef_environment.rb +77 -77
  26. data/lib/chef/resource/chef_group.rb +53 -53
  27. data/lib/chef/resource/chef_mirror.rb +52 -52
  28. data/lib/chef/resource/chef_node.rb +22 -22
  29. data/lib/chef/resource/chef_organization.rb +69 -69
  30. data/lib/chef/resource/chef_resolved_cookbooks.rb +35 -35
  31. data/lib/chef/resource/chef_role.rb +110 -110
  32. data/lib/chef/resource/chef_user.rb +56 -56
  33. data/lib/chef/resource/private_key.rb +48 -48
  34. data/lib/chef/resource/public_key.rb +25 -25
  35. data/lib/cheffish.rb +235 -235
  36. data/lib/cheffish/actor_provider_base.rb +131 -131
  37. data/lib/cheffish/basic_chef_client.rb +184 -184
  38. data/lib/cheffish/chef_provider_base.rb +246 -246
  39. data/lib/cheffish/chef_run.rb +162 -162
  40. data/lib/cheffish/chef_run_data.rb +19 -19
  41. data/lib/cheffish/chef_run_listener.rb +30 -30
  42. data/lib/cheffish/key_formatter.rb +113 -113
  43. data/lib/cheffish/merged_config.rb +94 -94
  44. data/lib/cheffish/recipe_dsl.rb +157 -157
  45. data/lib/cheffish/rspec.rb +8 -8
  46. data/lib/cheffish/rspec/chef_run_support.rb +83 -83
  47. data/lib/cheffish/rspec/matchers.rb +4 -4
  48. data/lib/cheffish/rspec/matchers/be_idempotent.rb +16 -16
  49. data/lib/cheffish/rspec/matchers/emit_no_warnings_or_errors.rb +15 -15
  50. data/lib/cheffish/rspec/matchers/have_updated.rb +37 -37
  51. data/lib/cheffish/rspec/matchers/partially_match.rb +63 -63
  52. data/lib/cheffish/rspec/recipe_run_wrapper.rb +59 -59
  53. data/lib/cheffish/rspec/repository_support.rb +108 -108
  54. data/lib/cheffish/server_api.rb +52 -52
  55. data/lib/cheffish/version.rb +3 -3
  56. data/lib/cheffish/with_pattern.rb +21 -21
  57. data/spec/functional/fingerprint_spec.rb +64 -64
  58. data/spec/functional/merged_config_spec.rb +19 -19
  59. data/spec/functional/server_api_spec.rb +13 -13
  60. data/spec/integration/chef_acl_spec.rb +879 -879
  61. data/spec/integration/chef_client_spec.rb +105 -105
  62. data/spec/integration/chef_container_spec.rb +33 -33
  63. data/spec/integration/chef_group_spec.rb +309 -309
  64. data/spec/integration/chef_mirror_spec.rb +491 -491
  65. data/spec/integration/chef_node_spec.rb +786 -786
  66. data/spec/integration/chef_organization_spec.rb +226 -226
  67. data/spec/integration/chef_role_spec.rb +78 -78
  68. data/spec/integration/chef_user_spec.rb +85 -85
  69. data/spec/integration/private_key_spec.rb +399 -399
  70. data/spec/integration/recipe_dsl_spec.rb +28 -28
  71. data/spec/integration/rspec/converge_spec.rb +183 -183
  72. data/spec/support/key_support.rb +29 -29
  73. data/spec/support/spec_support.rb +15 -15
  74. data/spec/unit/get_private_key_spec.rb +131 -131
  75. data/spec/unit/recipe_run_wrapper_spec.rb +37 -37
  76. metadata +3 -4
data/Rakefile CHANGED
@@ -1,23 +1,23 @@
1
- require 'bundler'
2
- require 'rubygems'
3
- require 'rubygems/package_task'
4
- require 'rdoc/task'
5
- require 'rspec/core/rake_task'
6
-
7
- Bundler::GemHelper.install_tasks
8
-
9
- task :default => :spec
10
-
11
- desc "Run specs"
12
- RSpec::Core::RakeTask.new(:spec) do |spec|
13
- spec.pattern = 'spec/**/*_spec.rb'
14
- end
15
-
16
- gem_spec = eval(File.read("cheffish.gemspec"))
17
-
18
- RDoc::Task.new do |rdoc|
19
- rdoc.rdoc_dir = 'rdoc'
20
- rdoc.title = "cheffish #{gem_spec.version}"
21
- rdoc.rdoc_files.include('README*')
22
- rdoc.rdoc_files.include('lib/**/*.rb')
23
- end
1
+ require 'bundler'
2
+ require 'rubygems'
3
+ require 'rubygems/package_task'
4
+ require 'rdoc/task'
5
+ require 'rspec/core/rake_task'
6
+
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ task :default => :spec
10
+
11
+ desc "Run specs"
12
+ RSpec::Core::RakeTask.new(:spec) do |spec|
13
+ spec.pattern = 'spec/**/*_spec.rb'
14
+ end
15
+
16
+ gem_spec = eval(File.read("cheffish.gemspec"))
17
+
18
+ RDoc::Task.new do |rdoc|
19
+ rdoc.rdoc_dir = 'rdoc'
20
+ rdoc.title = "cheffish #{gem_spec.version}"
21
+ rdoc.rdoc_files.include('README*')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
@@ -1,439 +1,439 @@
1
- require 'cheffish/chef_provider_base'
2
- require 'chef/resource/chef_acl'
3
- require 'chef/chef_fs/data_handler/acl_data_handler'
4
- require 'chef/chef_fs/parallelizer'
5
- require 'uri'
6
-
7
- class Chef
8
- class Provider
9
- class ChefAcl < Cheffish::ChefProviderBase
10
- provides :chef_acl
11
-
12
- def whyrun_supported?
13
- true
14
- end
15
-
16
- action :create do
17
- if new_resource.remove_rights && new_resource.complete
18
- Chef::Log.warn("'remove_rights' is redundant when 'complete' is specified: all rights not specified in a 'rights' declaration will be removed.")
19
- end
20
- # Verify that we're not destroying all hope of ACL recovery here
21
- if new_resource.complete && (!new_resource.rights || !new_resource.rights.any? { |r| r[:permissions].include?(:all) || r[:permissions].include?(:grant) })
22
- # NOTE: if superusers exist, this should turn into a warning.
23
- raise "'complete' specified on chef_acl resource, but no GRANT permissions were granted. I'm sorry Dave, I can't let you remove all access to an object with no hope of recovery."
24
- end
25
-
26
- # Find all matching paths so we can update them (resolve * and **)
27
- paths = match_paths(new_resource.path)
28
- if paths.size == 0 && !new_resource.path.split('/').any? { |p| p == '*' }
29
- raise "Path #{new_resource.path} cannot have an ACL set on it!"
30
- end
31
-
32
- # Go through the matches and update the ACLs for them
33
- paths.each do |path|
34
- create_acl(path)
35
- end
36
- end
37
-
38
- # Update the ACL if necessary.
39
- def create_acl(path)
40
- changed = false
41
- # There may not be an ACL path for some valid paths (/ and /organizations,
42
- # for example). We want to recurse into these, but we don't want to try to
43
- # update nonexistent ACLs for them.
44
- acl = acl_path(path)
45
- if acl
46
- # It's possible to make a custom container
47
- current_json = current_acl(acl)
48
- if current_json
49
-
50
- # Compare the desired and current json for the ACL, and update if different.
51
- modify = {}
52
- desired_acl(acl).each do |permission, desired_json|
53
- differences = json_differences(current_json[permission], desired_json)
54
-
55
- if differences.size > 0
56
- # Verify we aren't trying to destroy grant permissions
57
- if permission == 'grant' && desired_json['actors'] == [] && desired_json['groups'] == []
58
- # NOTE: if superusers exist, this should turn into a warning.
59
- raise "chef_acl attempted to remove all actors from GRANT! I'm sorry Dave, I can't let you remove access to an object with no hope of recovery."
60
- end
61
- modify[differences] ||= {}
62
- modify[differences][permission] = desired_json
63
- end
64
- end
65
-
66
- if modify.size > 0
67
- changed = true
68
- description = [ "update acl #{path} at #{rest_url(path)}" ] + modify.map do |diffs, permissions|
69
- diffs.map { |diff| " #{permissions.keys.join(', ')}:#{diff}" }
70
- end.flatten(1)
71
- converge_by description do
72
- modify.values.each do |permissions|
73
- permissions.each do |permission, desired_json|
74
- rest.put(rest_url("#{acl}/#{permission}"), { permission => desired_json })
75
- end
76
- end
77
- end
78
- end
79
- end
80
- end
81
-
82
- # If we have been asked to recurse, do so.
83
- # If recurse is on_change, then we will recurse if there is no ACL, or if
84
- # the ACL has changed.
85
- if new_resource.recursive == true || (new_resource.recursive == :on_change && (!acl || changed))
86
- children, error = list(path, '*')
87
- Chef::ChefFS::Parallelizer.parallel_do(children) do |child|
88
- next if child.split('/')[-1] == 'containers'
89
- create_acl(child)
90
- end
91
- # containers mess up our descent, so we do them last
92
- Chef::ChefFS::Parallelizer.parallel_do(children) do |child|
93
- next if child.split('/')[-1] != 'containers'
94
- create_acl(child)
95
- end
96
-
97
- end
98
- end
99
-
100
- # Get the current ACL for the given path
101
- def current_acl(acl_path)
102
- @current_acls ||= {}
103
- if !@current_acls.has_key?(acl_path)
104
- @current_acls[acl_path] = begin
105
- rest.get(rest_url(acl_path))
106
- rescue Net::HTTPServerException => e
107
- unless e.response.code == '404' && new_resource.path.split('/').any? { |p| p == '*' }
108
- raise
109
- end
110
- end
111
- end
112
- @current_acls[acl_path]
113
- end
114
-
115
- # Get the desired acl for the given acl path
116
- def desired_acl(acl_path)
117
- result = new_resource.raw_json ? new_resource.raw_json.dup : {}
118
-
119
- # Calculate the JSON based on rights
120
- add_rights(acl_path, result)
121
-
122
- if new_resource.complete
123
- result = Chef::ChefFS::DataHandler::AclDataHandler.new.normalize(result, nil)
124
- else
125
- # If resource is incomplete, use current json to fill any holes
126
- current_acl(acl_path).each do |permission, perm_hash|
127
- if !result[permission]
128
- result[permission] = perm_hash.dup
129
- else
130
- result[permission] = result[permission].dup
131
- perm_hash.each do |type, actors|
132
- if !result[permission][type]
133
- result[permission][type] = actors
134
- else
135
- result[permission][type] = result[permission][type].dup
136
- result[permission][type] |= actors
137
- end
138
- end
139
- end
140
- end
141
-
142
- remove_rights(result)
143
- end
144
- result
145
- end
146
-
147
- def add_rights(acl_path, json)
148
- if new_resource.rights
149
- new_resource.rights.each do |rights|
150
- if rights[:permissions].delete(:all)
151
- rights[:permissions] |= current_acl(acl_path).keys
152
- end
153
-
154
- Array(rights[:permissions]).each do |permission|
155
- ace = json[permission.to_s] ||= {}
156
- # WTF, no distinction between users and clients? The Chef API doesn't
157
- # let us distinguish, so we have no choice :/ This means that:
158
- # 1. If you specify :users => 'foo', and client 'foo' exists, it will
159
- # pick that (whether user 'foo' exists or not)
160
- # 2. If you specify :clients => 'foo', and user 'foo' exists but
161
- # client 'foo' does not, it will pick user 'foo' and put it in the
162
- # ACL
163
- # 3. If an existing item has user 'foo' on it and you specify :clients
164
- # => 'foo' instead, idempotence will not notice that anything needs
165
- # to be updated and nothing will happen.
166
- if rights[:users]
167
- ace['actors'] ||= []
168
- ace['actors'] |= Array(rights[:users])
169
- end
170
- if rights[:clients]
171
- ace['actors'] ||= []
172
- ace['actors'] |= Array(rights[:clients])
173
- end
174
- if rights[:groups]
175
- ace['groups'] ||= []
176
- ace['groups'] |= Array(rights[:groups])
177
- end
178
- end
179
- end
180
- end
181
- end
182
-
183
- def remove_rights(json)
184
- if new_resource.remove_rights
185
- new_resource.remove_rights.each do |rights|
186
- rights[:permissions].each do |permission|
187
- if permission == :all
188
- json.each_key do |key|
189
- ace = json[key] = json[key.dup]
190
- ace['actors'] = ace['actors'] - Array(rights[:users]) if rights[:users] && ace['actors']
191
- ace['actors'] = ace['actors'] - Array(rights[:clients]) if rights[:clients] && ace['actors']
192
- ace['groups'] = ace['groups'] - Array(rights[:groups]) if rights[:groups] && ace['groups']
193
- end
194
- else
195
- ace = json[permission.to_s] = json[permission.to_s].dup
196
- if ace
197
- ace['actors'] = ace['actors'] - Array(rights[:users]) if rights[:users] && ace['actors']
198
- ace['actors'] = ace['actors'] - Array(rights[:clients]) if rights[:clients] && ace['actors']
199
- ace['groups'] = ace['groups'] - Array(rights[:groups]) if rights[:groups] && ace['groups']
200
- end
201
- end
202
- end
203
- end
204
- end
205
- end
206
-
207
- def load_current_resource
208
- end
209
-
210
- #
211
- # Matches chef_acl paths like nodes, nodes/*.
212
- #
213
- # == Examples
214
- # match_paths('nodes'): [ 'nodes' ]
215
- # match_paths('nodes/*'): [ 'nodes/x', 'nodes/y', 'nodes/z' ]
216
- # match_paths('*'): [ 'clients', 'environments', 'nodes', 'roles', ... ]
217
- # match_paths('/'): [ '/' ]
218
- # match_paths(''): [ '' ]
219
- # match_paths('/*'): [ '/organizations', '/users' ]
220
- # match_paths('/organizations/*/*'): [ '/organizations/foo/clients', '/organizations/foo/environments', ..., '/organizations/bar/clients', '/organizations/bar/environments', ... ]
221
- #
222
- def match_paths(path)
223
- # Turn multiple slashes into one
224
- # nodes//x -> nodes/x
225
- path = path.gsub(/[\/]+/, '/')
226
- # If it's absolute, start the matching with /. If it's relative, start with '' (relative root).
227
- if path[0] == '/'
228
- matches = [ '/' ]
229
- else
230
- matches = [ '' ]
231
- end
232
-
233
- # Split the path, and get rid of the empty path at the beginning and end
234
- # (/a/b/c/ -> [ 'a', 'b', 'c' ])
235
- parts = path.split('/').select { |x| x != '' }.to_a
236
-
237
- # Descend until we find the matches:
238
- # path = 'a/b/c'
239
- # parts = [ 'a', 'b', 'c' ]
240
- # Starting matches = [ '' ]
241
- parts.each_with_index do |part, index|
242
- # For each match, list <match>/<part> and set matches to that.
243
- #
244
- # Example: /*/foo
245
- # 1. To start,
246
- # matches = [ '/' ], part = '*'.
247
- # list('/', '*') = [ '/organizations, '/users' ]
248
- # 2. matches = [ '/organizations', '/users' ], part = 'foo'
249
- # list('/organizations', 'foo') = [ '/organizations/foo' ]
250
- # list('/users', 'foo') = [ '/users/foo' ]
251
- #
252
- # Result: /*/foo = [ '/organizations/foo', '/users/foo' ]
253
- #
254
- matches = Chef::ChefFS::Parallelizer.parallelize(matches) do |path|
255
- found, error = list(path, part)
256
- if error
257
- if parts[0..index-1].all? { |p| p != '*' }
258
- raise error
259
- end
260
- []
261
- else
262
- found
263
- end
264
- end.flatten(1).to_a
265
- end
266
-
267
- matches
268
- end
269
-
270
- #
271
- # Takes a normal path and finds the Chef path to get / set its ACL.
272
- #
273
- # nodes/x -> nodes/x/_acl
274
- # nodes -> containers/nodes/_acl
275
- # '' -> organizations/_acl (the org acl)
276
- # /organizations/foo -> /organizations/foo/organizations/_acl
277
- # /users/foo -> /users/foo/_acl
278
- # /organizations/foo/nodes/x -> /organizations/foo/nodes/x/_acl
279
- #
280
- def acl_path(path)
281
- parts = path.split('/').select { |x| x != '' }.to_a
282
- prefix = (path[0] == '/') ? '/' : ''
283
-
284
- case parts.size
285
- when 0
286
- # /, empty (relative root)
287
- # The root of the server has no publicly visible ACLs. Only nodes/*, etc.
288
- if prefix == ''
289
- ::File.join('organizations', '_acl')
290
- end
291
-
292
- when 1
293
- # nodes, roles, etc.
294
- # The top level organizations and users containers have no publicly
295
- # visible ACLs. Only nodes/*, etc.
296
- if prefix == ''
297
- ::File.join('containers', path, '_acl')
298
- end
299
-
300
- when 2
301
- # /organizations/NAME, /users/NAME, nodes/NAME, roles/NAME, etc.
302
- if prefix == '/' && parts[0] == 'organizations'
303
- ::File.join(path, 'organizations', '_acl')
304
- else
305
- ::File.join(path, '_acl')
306
- end
307
-
308
- when 3
309
- # /organizations/NAME/nodes, cookbooks/NAME/VERSION, etc.
310
- if prefix == '/'
311
- ::File.join('/', parts[0], parts[1], 'containers', parts[2], '_acl')
312
- else
313
- ::File.join(parts[0], parts[1], '_acl')
314
- end
315
-
316
- when 4
317
- # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION/BLAH
318
- # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION, etc.
319
- if prefix == '/'
320
- ::File.join(path, '_acl')
321
- else
322
- ::File.join(parts[0], parts[1], '_acl')
323
- end
324
-
325
- else
326
- # /organizations/NAME/cookbooks/NAME/VERSION/..., cookbooks/NAME/VERSION/A/B/...
327
- if prefix == '/'
328
- ::File.join('/', parts[0], parts[1], parts[2], parts[3], '_acl')
329
- else
330
- ::File.join(parts[0], parts[1], '_acl')
331
- end
332
- end
333
- end
334
-
335
- #
336
- # Lists the securable children under a path (the ones that either have ACLs
337
- # or have children with ACLs).
338
- #
339
- # list('nodes', 'x') -> [ 'nodes/x' ]
340
- # list('nodes', '*') -> [ 'nodes/x', 'nodes/y', 'nodes/z' ]
341
- # list('', '*') -> [ 'clients', 'environments', 'nodes', 'roles', ... ]
342
- # list('/', '*') -> [ '/organizations']
343
- # list('cookbooks', 'x') -> [ 'cookbooks/x' ]
344
- # list('cookbooks/x', '*') -> [ ] # Individual cookbook versions do not have their own ACLs
345
- # list('/organizations/foo/nodes', '*') -> [ '/organizations/foo/nodes/x', '/organizations/foo/nodes/y' ]
346
- #
347
- # The list of children of an organization is == the list of containers. If new
348
- # containers are added, the list of children will grow. This allows the system
349
- # to extend to new types of objects and allow cheffish to work with them.
350
- #
351
- def list(path, child)
352
- # TODO make ChefFS understand top level organizations and stop doing this altogether.
353
- parts = path.split('/').select { |x| x != '' }.to_a
354
- absolute = (path[0] == '/')
355
- if absolute && parts[0] == 'organizations'
356
- return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 3
357
- else
358
- return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 1
359
- end
360
-
361
- error = nil
362
-
363
- if child == '*'
364
- case parts.size
365
- when 0
366
- # /*, *
367
- if absolute
368
- results = [ "/organizations", "/users" ]
369
- else
370
- results, error = rest_list("containers")
371
- end
372
-
373
- when 1
374
- # /organizations/*, /users/*, roles/*, nodes/*, etc.
375
- results, error = rest_list(path)
376
- if !error
377
- results = results.map { |result| ::File.join(path, result) }
378
- end
379
-
380
- when 2
381
- # /organizations/NAME/*
382
- results, error = rest_list(::File.join(path, 'containers'))
383
- if !error
384
- results = results.map { |result| ::File.join(path, result) }
385
- end
386
-
387
- when 3
388
- # /organizations/NAME/TYPE/*
389
- results, error = rest_list(path)
390
- if !error
391
- results = results.map { |result| ::File.join(path, result) }
392
- end
393
- end
394
-
395
- else
396
- if child == 'data_bags' &&
397
- (parts.size == 0 || (parts.size == 2 && parts[0] == 'organizations'))
398
- child = 'data'
399
- end
400
-
401
- if absolute
402
- # /<child>, /users/<child>, /organizations/<child>, /organizations/foo/<child>, /organizations/foo/nodes/<child> ...
403
- results = [ ::File.join('/', parts[0..2], child) ]
404
- elsif parts.size == 0
405
- # <child> (nodes, roles, etc.)
406
- results = [ child ]
407
- else
408
- # nodes/<child>, roles/<child>, etc.
409
- results = [ ::File.join(parts[0], child) ]
410
- end
411
- end
412
-
413
- [ results, error ]
414
- end
415
-
416
- def rest_url(path)
417
- path[0] == '/' ? URI.join(rest.url, path) : path
418
- end
419
-
420
- def rest_list(path)
421
- begin
422
- # All our rest lists are hashes where the keys are the names
423
- [ rest.get(rest_url(path)).keys, nil ]
424
- rescue Net::HTTPServerException => e
425
- if e.response.code == '405' || e.response.code == '404'
426
- parts = path.split('/').select { |p| p != '' }.to_a
427
-
428
- # We KNOW we expect these to exist. Other containers may or may not.
429
- unless (parts.size == 1 || (parts.size == 3 && parts[0] == 'organizations')) &&
430
- %w(clients containers cookbooks data environments groups nodes roles).include?(parts[-1])
431
- return [ [], "Cannot get list of #{path}: HTTP response code #{e.response.code}" ]
432
- end
433
- end
434
- raise
435
- end
436
- end
437
- end
438
- end
439
- end
1
+ require 'cheffish/chef_provider_base'
2
+ require 'chef/resource/chef_acl'
3
+ require 'chef/chef_fs/data_handler/acl_data_handler'
4
+ require 'chef/chef_fs/parallelizer'
5
+ require 'uri'
6
+
7
+ class Chef
8
+ class Provider
9
+ class ChefAcl < Cheffish::ChefProviderBase
10
+ provides :chef_acl
11
+
12
+ def whyrun_supported?
13
+ true
14
+ end
15
+
16
+ action :create do
17
+ if new_resource.remove_rights && new_resource.complete
18
+ Chef::Log.warn("'remove_rights' is redundant when 'complete' is specified: all rights not specified in a 'rights' declaration will be removed.")
19
+ end
20
+ # Verify that we're not destroying all hope of ACL recovery here
21
+ if new_resource.complete && (!new_resource.rights || !new_resource.rights.any? { |r| r[:permissions].include?(:all) || r[:permissions].include?(:grant) })
22
+ # NOTE: if superusers exist, this should turn into a warning.
23
+ raise "'complete' specified on chef_acl resource, but no GRANT permissions were granted. I'm sorry Dave, I can't let you remove all access to an object with no hope of recovery."
24
+ end
25
+
26
+ # Find all matching paths so we can update them (resolve * and **)
27
+ paths = match_paths(new_resource.path)
28
+ if paths.size == 0 && !new_resource.path.split('/').any? { |p| p == '*' }
29
+ raise "Path #{new_resource.path} cannot have an ACL set on it!"
30
+ end
31
+
32
+ # Go through the matches and update the ACLs for them
33
+ paths.each do |path|
34
+ create_acl(path)
35
+ end
36
+ end
37
+
38
+ # Update the ACL if necessary.
39
+ def create_acl(path)
40
+ changed = false
41
+ # There may not be an ACL path for some valid paths (/ and /organizations,
42
+ # for example). We want to recurse into these, but we don't want to try to
43
+ # update nonexistent ACLs for them.
44
+ acl = acl_path(path)
45
+ if acl
46
+ # It's possible to make a custom container
47
+ current_json = current_acl(acl)
48
+ if current_json
49
+
50
+ # Compare the desired and current json for the ACL, and update if different.
51
+ modify = {}
52
+ desired_acl(acl).each do |permission, desired_json|
53
+ differences = json_differences(current_json[permission], desired_json)
54
+
55
+ if differences.size > 0
56
+ # Verify we aren't trying to destroy grant permissions
57
+ if permission == 'grant' && desired_json['actors'] == [] && desired_json['groups'] == []
58
+ # NOTE: if superusers exist, this should turn into a warning.
59
+ raise "chef_acl attempted to remove all actors from GRANT! I'm sorry Dave, I can't let you remove access to an object with no hope of recovery."
60
+ end
61
+ modify[differences] ||= {}
62
+ modify[differences][permission] = desired_json
63
+ end
64
+ end
65
+
66
+ if modify.size > 0
67
+ changed = true
68
+ description = [ "update acl #{path} at #{rest_url(path)}" ] + modify.map do |diffs, permissions|
69
+ diffs.map { |diff| " #{permissions.keys.join(', ')}:#{diff}" }
70
+ end.flatten(1)
71
+ converge_by description do
72
+ modify.values.each do |permissions|
73
+ permissions.each do |permission, desired_json|
74
+ rest.put(rest_url("#{acl}/#{permission}"), { permission => desired_json })
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # If we have been asked to recurse, do so.
83
+ # If recurse is on_change, then we will recurse if there is no ACL, or if
84
+ # the ACL has changed.
85
+ if new_resource.recursive == true || (new_resource.recursive == :on_change && (!acl || changed))
86
+ children, error = list(path, '*')
87
+ Chef::ChefFS::Parallelizer.parallel_do(children) do |child|
88
+ next if child.split('/')[-1] == 'containers'
89
+ create_acl(child)
90
+ end
91
+ # containers mess up our descent, so we do them last
92
+ Chef::ChefFS::Parallelizer.parallel_do(children) do |child|
93
+ next if child.split('/')[-1] != 'containers'
94
+ create_acl(child)
95
+ end
96
+
97
+ end
98
+ end
99
+
100
+ # Get the current ACL for the given path
101
+ def current_acl(acl_path)
102
+ @current_acls ||= {}
103
+ if !@current_acls.has_key?(acl_path)
104
+ @current_acls[acl_path] = begin
105
+ rest.get(rest_url(acl_path))
106
+ rescue Net::HTTPServerException => e
107
+ unless e.response.code == '404' && new_resource.path.split('/').any? { |p| p == '*' }
108
+ raise
109
+ end
110
+ end
111
+ end
112
+ @current_acls[acl_path]
113
+ end
114
+
115
+ # Get the desired acl for the given acl path
116
+ def desired_acl(acl_path)
117
+ result = new_resource.raw_json ? new_resource.raw_json.dup : {}
118
+
119
+ # Calculate the JSON based on rights
120
+ add_rights(acl_path, result)
121
+
122
+ if new_resource.complete
123
+ result = Chef::ChefFS::DataHandler::AclDataHandler.new.normalize(result, nil)
124
+ else
125
+ # If resource is incomplete, use current json to fill any holes
126
+ current_acl(acl_path).each do |permission, perm_hash|
127
+ if !result[permission]
128
+ result[permission] = perm_hash.dup
129
+ else
130
+ result[permission] = result[permission].dup
131
+ perm_hash.each do |type, actors|
132
+ if !result[permission][type]
133
+ result[permission][type] = actors
134
+ else
135
+ result[permission][type] = result[permission][type].dup
136
+ result[permission][type] |= actors
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ remove_rights(result)
143
+ end
144
+ result
145
+ end
146
+
147
+ def add_rights(acl_path, json)
148
+ if new_resource.rights
149
+ new_resource.rights.each do |rights|
150
+ if rights[:permissions].delete(:all)
151
+ rights[:permissions] |= current_acl(acl_path).keys
152
+ end
153
+
154
+ Array(rights[:permissions]).each do |permission|
155
+ ace = json[permission.to_s] ||= {}
156
+ # WTF, no distinction between users and clients? The Chef API doesn't
157
+ # let us distinguish, so we have no choice :/ This means that:
158
+ # 1. If you specify :users => 'foo', and client 'foo' exists, it will
159
+ # pick that (whether user 'foo' exists or not)
160
+ # 2. If you specify :clients => 'foo', and user 'foo' exists but
161
+ # client 'foo' does not, it will pick user 'foo' and put it in the
162
+ # ACL
163
+ # 3. If an existing item has user 'foo' on it and you specify :clients
164
+ # => 'foo' instead, idempotence will not notice that anything needs
165
+ # to be updated and nothing will happen.
166
+ if rights[:users]
167
+ ace['actors'] ||= []
168
+ ace['actors'] |= Array(rights[:users])
169
+ end
170
+ if rights[:clients]
171
+ ace['actors'] ||= []
172
+ ace['actors'] |= Array(rights[:clients])
173
+ end
174
+ if rights[:groups]
175
+ ace['groups'] ||= []
176
+ ace['groups'] |= Array(rights[:groups])
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ def remove_rights(json)
184
+ if new_resource.remove_rights
185
+ new_resource.remove_rights.each do |rights|
186
+ rights[:permissions].each do |permission|
187
+ if permission == :all
188
+ json.each_key do |key|
189
+ ace = json[key] = json[key.dup]
190
+ ace['actors'] = ace['actors'] - Array(rights[:users]) if rights[:users] && ace['actors']
191
+ ace['actors'] = ace['actors'] - Array(rights[:clients]) if rights[:clients] && ace['actors']
192
+ ace['groups'] = ace['groups'] - Array(rights[:groups]) if rights[:groups] && ace['groups']
193
+ end
194
+ else
195
+ ace = json[permission.to_s] = json[permission.to_s].dup
196
+ if ace
197
+ ace['actors'] = ace['actors'] - Array(rights[:users]) if rights[:users] && ace['actors']
198
+ ace['actors'] = ace['actors'] - Array(rights[:clients]) if rights[:clients] && ace['actors']
199
+ ace['groups'] = ace['groups'] - Array(rights[:groups]) if rights[:groups] && ace['groups']
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ def load_current_resource
208
+ end
209
+
210
+ #
211
+ # Matches chef_acl paths like nodes, nodes/*.
212
+ #
213
+ # == Examples
214
+ # match_paths('nodes'): [ 'nodes' ]
215
+ # match_paths('nodes/*'): [ 'nodes/x', 'nodes/y', 'nodes/z' ]
216
+ # match_paths('*'): [ 'clients', 'environments', 'nodes', 'roles', ... ]
217
+ # match_paths('/'): [ '/' ]
218
+ # match_paths(''): [ '' ]
219
+ # match_paths('/*'): [ '/organizations', '/users' ]
220
+ # match_paths('/organizations/*/*'): [ '/organizations/foo/clients', '/organizations/foo/environments', ..., '/organizations/bar/clients', '/organizations/bar/environments', ... ]
221
+ #
222
+ def match_paths(path)
223
+ # Turn multiple slashes into one
224
+ # nodes//x -> nodes/x
225
+ path = path.gsub(/[\/]+/, '/')
226
+ # If it's absolute, start the matching with /. If it's relative, start with '' (relative root).
227
+ if path[0] == '/'
228
+ matches = [ '/' ]
229
+ else
230
+ matches = [ '' ]
231
+ end
232
+
233
+ # Split the path, and get rid of the empty path at the beginning and end
234
+ # (/a/b/c/ -> [ 'a', 'b', 'c' ])
235
+ parts = path.split('/').select { |x| x != '' }.to_a
236
+
237
+ # Descend until we find the matches:
238
+ # path = 'a/b/c'
239
+ # parts = [ 'a', 'b', 'c' ]
240
+ # Starting matches = [ '' ]
241
+ parts.each_with_index do |part, index|
242
+ # For each match, list <match>/<part> and set matches to that.
243
+ #
244
+ # Example: /*/foo
245
+ # 1. To start,
246
+ # matches = [ '/' ], part = '*'.
247
+ # list('/', '*') = [ '/organizations, '/users' ]
248
+ # 2. matches = [ '/organizations', '/users' ], part = 'foo'
249
+ # list('/organizations', 'foo') = [ '/organizations/foo' ]
250
+ # list('/users', 'foo') = [ '/users/foo' ]
251
+ #
252
+ # Result: /*/foo = [ '/organizations/foo', '/users/foo' ]
253
+ #
254
+ matches = Chef::ChefFS::Parallelizer.parallelize(matches) do |path|
255
+ found, error = list(path, part)
256
+ if error
257
+ if parts[0..index-1].all? { |p| p != '*' }
258
+ raise error
259
+ end
260
+ []
261
+ else
262
+ found
263
+ end
264
+ end.flatten(1).to_a
265
+ end
266
+
267
+ matches
268
+ end
269
+
270
+ #
271
+ # Takes a normal path and finds the Chef path to get / set its ACL.
272
+ #
273
+ # nodes/x -> nodes/x/_acl
274
+ # nodes -> containers/nodes/_acl
275
+ # '' -> organizations/_acl (the org acl)
276
+ # /organizations/foo -> /organizations/foo/organizations/_acl
277
+ # /users/foo -> /users/foo/_acl
278
+ # /organizations/foo/nodes/x -> /organizations/foo/nodes/x/_acl
279
+ #
280
+ def acl_path(path)
281
+ parts = path.split('/').select { |x| x != '' }.to_a
282
+ prefix = (path[0] == '/') ? '/' : ''
283
+
284
+ case parts.size
285
+ when 0
286
+ # /, empty (relative root)
287
+ # The root of the server has no publicly visible ACLs. Only nodes/*, etc.
288
+ if prefix == ''
289
+ ::File.join('organizations', '_acl')
290
+ end
291
+
292
+ when 1
293
+ # nodes, roles, etc.
294
+ # The top level organizations and users containers have no publicly
295
+ # visible ACLs. Only nodes/*, etc.
296
+ if prefix == ''
297
+ ::File.join('containers', path, '_acl')
298
+ end
299
+
300
+ when 2
301
+ # /organizations/NAME, /users/NAME, nodes/NAME, roles/NAME, etc.
302
+ if prefix == '/' && parts[0] == 'organizations'
303
+ ::File.join(path, 'organizations', '_acl')
304
+ else
305
+ ::File.join(path, '_acl')
306
+ end
307
+
308
+ when 3
309
+ # /organizations/NAME/nodes, cookbooks/NAME/VERSION, etc.
310
+ if prefix == '/'
311
+ ::File.join('/', parts[0], parts[1], 'containers', parts[2], '_acl')
312
+ else
313
+ ::File.join(parts[0], parts[1], '_acl')
314
+ end
315
+
316
+ when 4
317
+ # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION/BLAH
318
+ # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION, etc.
319
+ if prefix == '/'
320
+ ::File.join(path, '_acl')
321
+ else
322
+ ::File.join(parts[0], parts[1], '_acl')
323
+ end
324
+
325
+ else
326
+ # /organizations/NAME/cookbooks/NAME/VERSION/..., cookbooks/NAME/VERSION/A/B/...
327
+ if prefix == '/'
328
+ ::File.join('/', parts[0], parts[1], parts[2], parts[3], '_acl')
329
+ else
330
+ ::File.join(parts[0], parts[1], '_acl')
331
+ end
332
+ end
333
+ end
334
+
335
+ #
336
+ # Lists the securable children under a path (the ones that either have ACLs
337
+ # or have children with ACLs).
338
+ #
339
+ # list('nodes', 'x') -> [ 'nodes/x' ]
340
+ # list('nodes', '*') -> [ 'nodes/x', 'nodes/y', 'nodes/z' ]
341
+ # list('', '*') -> [ 'clients', 'environments', 'nodes', 'roles', ... ]
342
+ # list('/', '*') -> [ '/organizations']
343
+ # list('cookbooks', 'x') -> [ 'cookbooks/x' ]
344
+ # list('cookbooks/x', '*') -> [ ] # Individual cookbook versions do not have their own ACLs
345
+ # list('/organizations/foo/nodes', '*') -> [ '/organizations/foo/nodes/x', '/organizations/foo/nodes/y' ]
346
+ #
347
+ # The list of children of an organization is == the list of containers. If new
348
+ # containers are added, the list of children will grow. This allows the system
349
+ # to extend to new types of objects and allow cheffish to work with them.
350
+ #
351
+ def list(path, child)
352
+ # TODO make ChefFS understand top level organizations and stop doing this altogether.
353
+ parts = path.split('/').select { |x| x != '' }.to_a
354
+ absolute = (path[0] == '/')
355
+ if absolute && parts[0] == 'organizations'
356
+ return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 3
357
+ else
358
+ return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 1
359
+ end
360
+
361
+ error = nil
362
+
363
+ if child == '*'
364
+ case parts.size
365
+ when 0
366
+ # /*, *
367
+ if absolute
368
+ results = [ "/organizations", "/users" ]
369
+ else
370
+ results, error = rest_list("containers")
371
+ end
372
+
373
+ when 1
374
+ # /organizations/*, /users/*, roles/*, nodes/*, etc.
375
+ results, error = rest_list(path)
376
+ if !error
377
+ results = results.map { |result| ::File.join(path, result) }
378
+ end
379
+
380
+ when 2
381
+ # /organizations/NAME/*
382
+ results, error = rest_list(::File.join(path, 'containers'))
383
+ if !error
384
+ results = results.map { |result| ::File.join(path, result) }
385
+ end
386
+
387
+ when 3
388
+ # /organizations/NAME/TYPE/*
389
+ results, error = rest_list(path)
390
+ if !error
391
+ results = results.map { |result| ::File.join(path, result) }
392
+ end
393
+ end
394
+
395
+ else
396
+ if child == 'data_bags' &&
397
+ (parts.size == 0 || (parts.size == 2 && parts[0] == 'organizations'))
398
+ child = 'data'
399
+ end
400
+
401
+ if absolute
402
+ # /<child>, /users/<child>, /organizations/<child>, /organizations/foo/<child>, /organizations/foo/nodes/<child> ...
403
+ results = [ ::File.join('/', parts[0..2], child) ]
404
+ elsif parts.size == 0
405
+ # <child> (nodes, roles, etc.)
406
+ results = [ child ]
407
+ else
408
+ # nodes/<child>, roles/<child>, etc.
409
+ results = [ ::File.join(parts[0], child) ]
410
+ end
411
+ end
412
+
413
+ [ results, error ]
414
+ end
415
+
416
+ def rest_url(path)
417
+ path[0] == '/' ? URI.join(rest.url, path) : path
418
+ end
419
+
420
+ def rest_list(path)
421
+ begin
422
+ # All our rest lists are hashes where the keys are the names
423
+ [ rest.get(rest_url(path)).keys, nil ]
424
+ rescue Net::HTTPServerException => e
425
+ if e.response.code == '405' || e.response.code == '404'
426
+ parts = path.split('/').select { |p| p != '' }.to_a
427
+
428
+ # We KNOW we expect these to exist. Other containers may or may not.
429
+ unless (parts.size == 1 || (parts.size == 3 && parts[0] == 'organizations')) &&
430
+ %w(clients containers cookbooks data environments groups nodes roles).include?(parts[-1])
431
+ return [ [], "Cannot get list of #{path}: HTTP response code #{e.response.code}" ]
432
+ end
433
+ end
434
+ raise
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end