cheffish 1.5.0 → 1.6.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -0
  3. data/LICENSE +201 -201
  4. data/README.md +120 -120
  5. data/Rakefile +23 -23
  6. data/cheffish.gemspec +26 -0
  7. data/lib/chef/provider/chef_acl.rb +446 -439
  8. data/lib/chef/provider/chef_client.rb +53 -53
  9. data/lib/chef/provider/chef_container.rb +55 -55
  10. data/lib/chef/provider/chef_data_bag.rb +55 -55
  11. data/lib/chef/provider/chef_data_bag_item.rb +278 -278
  12. data/lib/chef/provider/chef_environment.rb +83 -83
  13. data/lib/chef/provider/chef_group.rb +83 -83
  14. data/lib/chef/provider/chef_mirror.rb +169 -169
  15. data/lib/chef/provider/chef_node.rb +87 -87
  16. data/lib/chef/provider/chef_organization.rb +155 -155
  17. data/lib/chef/provider/chef_resolved_cookbooks.rb +46 -46
  18. data/lib/chef/provider/chef_role.rb +84 -84
  19. data/lib/chef/provider/chef_user.rb +59 -59
  20. data/lib/chef/provider/private_key.rb +225 -225
  21. data/lib/chef/provider/public_key.rb +88 -88
  22. data/lib/chef/resource/chef_acl.rb +69 -69
  23. data/lib/chef/resource/chef_client.rb +48 -48
  24. data/lib/chef/resource/chef_container.rb +22 -22
  25. data/lib/chef/resource/chef_data_bag.rb +22 -22
  26. data/lib/chef/resource/chef_data_bag_item.rb +121 -121
  27. data/lib/chef/resource/chef_environment.rb +77 -77
  28. data/lib/chef/resource/chef_group.rb +53 -53
  29. data/lib/chef/resource/chef_mirror.rb +52 -52
  30. data/lib/chef/resource/chef_node.rb +22 -22
  31. data/lib/chef/resource/chef_organization.rb +69 -69
  32. data/lib/chef/resource/chef_resolved_cookbooks.rb +35 -35
  33. data/lib/chef/resource/chef_role.rb +110 -110
  34. data/lib/chef/resource/chef_user.rb +56 -56
  35. data/lib/chef/resource/private_key.rb +48 -48
  36. data/lib/chef/resource/public_key.rb +25 -25
  37. data/lib/cheffish.rb +235 -235
  38. data/lib/cheffish/actor_provider_base.rb +131 -131
  39. data/lib/cheffish/basic_chef_client.rb +184 -184
  40. data/lib/cheffish/chef_provider_base.rb +246 -246
  41. data/lib/cheffish/chef_run.rb +162 -162
  42. data/lib/cheffish/chef_run_data.rb +19 -19
  43. data/lib/cheffish/chef_run_listener.rb +30 -30
  44. data/lib/cheffish/key_formatter.rb +113 -113
  45. data/lib/cheffish/merged_config.rb +98 -94
  46. data/lib/cheffish/recipe_dsl.rb +157 -157
  47. data/lib/cheffish/rspec.rb +8 -8
  48. data/lib/cheffish/rspec/chef_run_support.rb +83 -83
  49. data/lib/cheffish/rspec/matchers.rb +4 -4
  50. data/lib/cheffish/rspec/matchers/be_idempotent.rb +16 -16
  51. data/lib/cheffish/rspec/matchers/emit_no_warnings_or_errors.rb +15 -15
  52. data/lib/cheffish/rspec/matchers/have_updated.rb +37 -37
  53. data/lib/cheffish/rspec/matchers/partially_match.rb +63 -63
  54. data/lib/cheffish/rspec/recipe_run_wrapper.rb +78 -78
  55. data/lib/cheffish/rspec/repository_support.rb +108 -108
  56. data/lib/cheffish/server_api.rb +52 -52
  57. data/lib/cheffish/version.rb +3 -3
  58. data/lib/cheffish/with_pattern.rb +21 -21
  59. data/spec/functional/fingerprint_spec.rb +64 -64
  60. data/spec/functional/merged_config_spec.rb +19 -19
  61. data/spec/functional/server_api_spec.rb +13 -13
  62. data/spec/integration/chef_acl_spec.rb +892 -879
  63. data/spec/integration/chef_client_spec.rb +105 -105
  64. data/spec/integration/chef_container_spec.rb +33 -33
  65. data/spec/integration/chef_group_spec.rb +309 -309
  66. data/spec/integration/chef_mirror_spec.rb +491 -491
  67. data/spec/integration/chef_node_spec.rb +786 -786
  68. data/spec/integration/chef_organization_spec.rb +226 -226
  69. data/spec/integration/chef_role_spec.rb +78 -78
  70. data/spec/integration/chef_user_spec.rb +85 -85
  71. data/spec/integration/private_key_spec.rb +399 -399
  72. data/spec/integration/recipe_dsl_spec.rb +28 -28
  73. data/spec/integration/rspec/converge_spec.rb +183 -183
  74. data/spec/support/key_support.rb +29 -29
  75. data/spec/support/spec_support.rb +15 -15
  76. data/spec/unit/get_private_key_spec.rb +131 -131
  77. data/spec/unit/recipe_run_wrapper_spec.rb +37 -37
  78. metadata +7 -5
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
data/cheffish.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/lib')
2
+ require 'cheffish/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'cheffish'
6
+ s.version = Cheffish::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.extra_rdoc_files = [ 'README.md', 'LICENSE' ]
9
+ s.summary = 'A library to manipulate Chef in Chef.'
10
+ s.description = s.summary
11
+ s.author = 'John Keiser'
12
+ s.email = 'jkeiser@chef.io'
13
+ s.homepage = 'http://github.com/chef/cheffish'
14
+
15
+ s.add_dependency 'chef-zero', '~> 4.3'
16
+
17
+ s.add_development_dependency 'chef', '~> 12.2'
18
+ s.add_development_dependency 'rake'
19
+ s.add_development_dependency 'rspec', '~> 3.0'
20
+ s.bindir = "bin"
21
+ s.executables = %w( )
22
+
23
+ s.require_path = 'lib'
24
+ s.files = %w(Gemfile Rakefile LICENSE README.md) + Dir.glob("*.gemspec") +
25
+ Dir.glob("{distro,lib,tasks,spec}/**/*", File::FNM_DOTMATCH).reject {|f| File.directory?(f) }
26
+ end
@@ -1,439 +1,446 @@
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(sort_values(current_json[permission]), sort_values(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 sort_values(json)
148
+ json.each do |key, value|
149
+ json[key] = value.sort if value.is_a?(Array)
150
+ end
151
+ json
152
+ end
153
+
154
+ def add_rights(acl_path, json)
155
+ if new_resource.rights
156
+ new_resource.rights.each do |rights|
157
+ if rights[:permissions].delete(:all)
158
+ rights[:permissions] |= current_acl(acl_path).keys
159
+ end
160
+
161
+ Array(rights[:permissions]).each do |permission|
162
+ ace = json[permission.to_s] ||= {}
163
+ # WTF, no distinction between users and clients? The Chef API doesn't
164
+ # let us distinguish, so we have no choice :/ This means that:
165
+ # 1. If you specify :users => 'foo', and client 'foo' exists, it will
166
+ # pick that (whether user 'foo' exists or not)
167
+ # 2. If you specify :clients => 'foo', and user 'foo' exists but
168
+ # client 'foo' does not, it will pick user 'foo' and put it in the
169
+ # ACL
170
+ # 3. If an existing item has user 'foo' on it and you specify :clients
171
+ # => 'foo' instead, idempotence will not notice that anything needs
172
+ # to be updated and nothing will happen.
173
+ if rights[:users]
174
+ ace['actors'] ||= []
175
+ ace['actors'] |= Array(rights[:users])
176
+ end
177
+ if rights[:clients]
178
+ ace['actors'] ||= []
179
+ ace['actors'] |= Array(rights[:clients])
180
+ end
181
+ if rights[:groups]
182
+ ace['groups'] ||= []
183
+ ace['groups'] |= Array(rights[:groups])
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ def remove_rights(json)
191
+ if new_resource.remove_rights
192
+ new_resource.remove_rights.each do |rights|
193
+ rights[:permissions].each do |permission|
194
+ if permission == :all
195
+ json.each_key do |key|
196
+ ace = json[key] = json[key.dup]
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
+ else
202
+ ace = json[permission.to_s] = json[permission.to_s].dup
203
+ if ace
204
+ ace['actors'] = ace['actors'] - Array(rights[:users]) if rights[:users] && ace['actors']
205
+ ace['actors'] = ace['actors'] - Array(rights[:clients]) if rights[:clients] && ace['actors']
206
+ ace['groups'] = ace['groups'] - Array(rights[:groups]) if rights[:groups] && ace['groups']
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ def load_current_resource
215
+ end
216
+
217
+ #
218
+ # Matches chef_acl paths like nodes, nodes/*.
219
+ #
220
+ # == Examples
221
+ # match_paths('nodes'): [ 'nodes' ]
222
+ # match_paths('nodes/*'): [ 'nodes/x', 'nodes/y', 'nodes/z' ]
223
+ # match_paths('*'): [ 'clients', 'environments', 'nodes', 'roles', ... ]
224
+ # match_paths('/'): [ '/' ]
225
+ # match_paths(''): [ '' ]
226
+ # match_paths('/*'): [ '/organizations', '/users' ]
227
+ # match_paths('/organizations/*/*'): [ '/organizations/foo/clients', '/organizations/foo/environments', ..., '/organizations/bar/clients', '/organizations/bar/environments', ... ]
228
+ #
229
+ def match_paths(path)
230
+ # Turn multiple slashes into one
231
+ # nodes//x -> nodes/x
232
+ path = path.gsub(/[\/]+/, '/')
233
+ # If it's absolute, start the matching with /. If it's relative, start with '' (relative root).
234
+ if path[0] == '/'
235
+ matches = [ '/' ]
236
+ else
237
+ matches = [ '' ]
238
+ end
239
+
240
+ # Split the path, and get rid of the empty path at the beginning and end
241
+ # (/a/b/c/ -> [ 'a', 'b', 'c' ])
242
+ parts = path.split('/').select { |x| x != '' }.to_a
243
+
244
+ # Descend until we find the matches:
245
+ # path = 'a/b/c'
246
+ # parts = [ 'a', 'b', 'c' ]
247
+ # Starting matches = [ '' ]
248
+ parts.each_with_index do |part, index|
249
+ # For each match, list <match>/<part> and set matches to that.
250
+ #
251
+ # Example: /*/foo
252
+ # 1. To start,
253
+ # matches = [ '/' ], part = '*'.
254
+ # list('/', '*') = [ '/organizations, '/users' ]
255
+ # 2. matches = [ '/organizations', '/users' ], part = 'foo'
256
+ # list('/organizations', 'foo') = [ '/organizations/foo' ]
257
+ # list('/users', 'foo') = [ '/users/foo' ]
258
+ #
259
+ # Result: /*/foo = [ '/organizations/foo', '/users/foo' ]
260
+ #
261
+ matches = Chef::ChefFS::Parallelizer.parallelize(matches) do |path|
262
+ found, error = list(path, part)
263
+ if error
264
+ if parts[0..index-1].all? { |p| p != '*' }
265
+ raise error
266
+ end
267
+ []
268
+ else
269
+ found
270
+ end
271
+ end.flatten(1).to_a
272
+ end
273
+
274
+ matches
275
+ end
276
+
277
+ #
278
+ # Takes a normal path and finds the Chef path to get / set its ACL.
279
+ #
280
+ # nodes/x -> nodes/x/_acl
281
+ # nodes -> containers/nodes/_acl
282
+ # '' -> organizations/_acl (the org acl)
283
+ # /organizations/foo -> /organizations/foo/organizations/_acl
284
+ # /users/foo -> /users/foo/_acl
285
+ # /organizations/foo/nodes/x -> /organizations/foo/nodes/x/_acl
286
+ #
287
+ def acl_path(path)
288
+ parts = path.split('/').select { |x| x != '' }.to_a
289
+ prefix = (path[0] == '/') ? '/' : ''
290
+
291
+ case parts.size
292
+ when 0
293
+ # /, empty (relative root)
294
+ # The root of the server has no publicly visible ACLs. Only nodes/*, etc.
295
+ if prefix == ''
296
+ ::File.join('organizations', '_acl')
297
+ end
298
+
299
+ when 1
300
+ # nodes, roles, etc.
301
+ # The top level organizations and users containers have no publicly
302
+ # visible ACLs. Only nodes/*, etc.
303
+ if prefix == ''
304
+ ::File.join('containers', path, '_acl')
305
+ end
306
+
307
+ when 2
308
+ # /organizations/NAME, /users/NAME, nodes/NAME, roles/NAME, etc.
309
+ if prefix == '/' && parts[0] == 'organizations'
310
+ ::File.join(path, 'organizations', '_acl')
311
+ else
312
+ ::File.join(path, '_acl')
313
+ end
314
+
315
+ when 3
316
+ # /organizations/NAME/nodes, cookbooks/NAME/VERSION, etc.
317
+ if prefix == '/'
318
+ ::File.join('/', parts[0], parts[1], 'containers', parts[2], '_acl')
319
+ else
320
+ ::File.join(parts[0], parts[1], '_acl')
321
+ end
322
+
323
+ when 4
324
+ # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION/BLAH
325
+ # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION, etc.
326
+ if prefix == '/'
327
+ ::File.join(path, '_acl')
328
+ else
329
+ ::File.join(parts[0], parts[1], '_acl')
330
+ end
331
+
332
+ else
333
+ # /organizations/NAME/cookbooks/NAME/VERSION/..., cookbooks/NAME/VERSION/A/B/...
334
+ if prefix == '/'
335
+ ::File.join('/', parts[0], parts[1], parts[2], parts[3], '_acl')
336
+ else
337
+ ::File.join(parts[0], parts[1], '_acl')
338
+ end
339
+ end
340
+ end
341
+
342
+ #
343
+ # Lists the securable children under a path (the ones that either have ACLs
344
+ # or have children with ACLs).
345
+ #
346
+ # list('nodes', 'x') -> [ 'nodes/x' ]
347
+ # list('nodes', '*') -> [ 'nodes/x', 'nodes/y', 'nodes/z' ]
348
+ # list('', '*') -> [ 'clients', 'environments', 'nodes', 'roles', ... ]
349
+ # list('/', '*') -> [ '/organizations']
350
+ # list('cookbooks', 'x') -> [ 'cookbooks/x' ]
351
+ # list('cookbooks/x', '*') -> [ ] # Individual cookbook versions do not have their own ACLs
352
+ # list('/organizations/foo/nodes', '*') -> [ '/organizations/foo/nodes/x', '/organizations/foo/nodes/y' ]
353
+ #
354
+ # The list of children of an organization is == the list of containers. If new
355
+ # containers are added, the list of children will grow. This allows the system
356
+ # to extend to new types of objects and allow cheffish to work with them.
357
+ #
358
+ def list(path, child)
359
+ # TODO make ChefFS understand top level organizations and stop doing this altogether.
360
+ parts = path.split('/').select { |x| x != '' }.to_a
361
+ absolute = (path[0] == '/')
362
+ if absolute && parts[0] == 'organizations'
363
+ return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 3
364
+ else
365
+ return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 1
366
+ end
367
+
368
+ error = nil
369
+
370
+ if child == '*'
371
+ case parts.size
372
+ when 0
373
+ # /*, *
374
+ if absolute
375
+ results = [ "/organizations", "/users" ]
376
+ else
377
+ results, error = rest_list("containers")
378
+ end
379
+
380
+ when 1
381
+ # /organizations/*, /users/*, roles/*, nodes/*, etc.
382
+ results, error = rest_list(path)
383
+ if !error
384
+ results = results.map { |result| ::File.join(path, result) }
385
+ end
386
+
387
+ when 2
388
+ # /organizations/NAME/*
389
+ results, error = rest_list(::File.join(path, 'containers'))
390
+ if !error
391
+ results = results.map { |result| ::File.join(path, result) }
392
+ end
393
+
394
+ when 3
395
+ # /organizations/NAME/TYPE/*
396
+ results, error = rest_list(path)
397
+ if !error
398
+ results = results.map { |result| ::File.join(path, result) }
399
+ end
400
+ end
401
+
402
+ else
403
+ if child == 'data_bags' &&
404
+ (parts.size == 0 || (parts.size == 2 && parts[0] == 'organizations'))
405
+ child = 'data'
406
+ end
407
+
408
+ if absolute
409
+ # /<child>, /users/<child>, /organizations/<child>, /organizations/foo/<child>, /organizations/foo/nodes/<child> ...
410
+ results = [ ::File.join('/', parts[0..2], child) ]
411
+ elsif parts.size == 0
412
+ # <child> (nodes, roles, etc.)
413
+ results = [ child ]
414
+ else
415
+ # nodes/<child>, roles/<child>, etc.
416
+ results = [ ::File.join(parts[0], child) ]
417
+ end
418
+ end
419
+
420
+ [ results, error ]
421
+ end
422
+
423
+ def rest_url(path)
424
+ path[0] == '/' ? URI.join(rest.url, path) : path
425
+ end
426
+
427
+ def rest_list(path)
428
+ begin
429
+ # All our rest lists are hashes where the keys are the names
430
+ [ rest.get(rest_url(path)).keys, nil ]
431
+ rescue Net::HTTPServerException => e
432
+ if e.response.code == '405' || e.response.code == '404'
433
+ parts = path.split('/').select { |p| p != '' }.to_a
434
+
435
+ # We KNOW we expect these to exist. Other containers may or may not.
436
+ unless (parts.size == 1 || (parts.size == 3 && parts[0] == 'organizations')) &&
437
+ %w(clients containers cookbooks data environments groups nodes roles).include?(parts[-1])
438
+ return [ [], "Cannot get list of #{path}: HTTP response code #{e.response.code}" ]
439
+ end
440
+ end
441
+ raise
442
+ end
443
+ end
444
+ end
445
+ end
446
+ end