chef 15.2.20-universal-mingw32 → 15.3.14-universal-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/chef.gemspec +3 -2
  4. data/lib/chef/application.rb +1 -1
  5. data/lib/chef/application/base.rb +7 -0
  6. data/lib/chef/application/client.rb +6 -2
  7. data/lib/chef/application/solo.rb +7 -1
  8. data/lib/chef/cookbook/gem_installer.rb +7 -2
  9. data/lib/chef/exceptions.rb +12 -0
  10. data/lib/chef/knife/bootstrap.rb +8 -1
  11. data/lib/chef/knife/bootstrap/templates/chef-full.erb +1 -1
  12. data/lib/chef/knife/bootstrap/train_connector.rb +3 -3
  13. data/lib/chef/knife/cookbook_metadata_from_file.rb +1 -1
  14. data/lib/chef/node.rb +0 -2
  15. data/lib/chef/policy_builder/expand_node_object.rb +1 -1
  16. data/lib/chef/policy_builder/policyfile.rb +4 -3
  17. data/lib/chef/provider.rb +4 -2
  18. data/lib/chef/provider/ifconfig.rb +5 -3
  19. data/lib/chef/provider/package/chocolatey.rb +12 -22
  20. data/lib/chef/provider/user.rb +1 -1
  21. data/lib/chef/provider/user/dscl.rb +2 -2
  22. data/lib/chef/provider/user/mac.rb +628 -0
  23. data/lib/chef/providers.rb +1 -0
  24. data/lib/chef/resource.rb +28 -20
  25. data/lib/chef/resource/chocolatey_feature.rb +1 -1
  26. data/lib/chef/resource/chocolatey_package.rb +2 -2
  27. data/lib/chef/resource/cron_d.rb +1 -1
  28. data/lib/chef/resource/ohai.rb +1 -1
  29. data/lib/chef/resource/resource_notification.rb +17 -13
  30. data/lib/chef/resource/ruby_block.rb +1 -1
  31. data/lib/chef/resource/service.rb +1 -1
  32. data/lib/chef/resource/user.rb +1 -0
  33. data/lib/chef/resource/user/dscl_user.rb +1 -1
  34. data/lib/chef/resource/user/mac_user.rb +119 -0
  35. data/lib/chef/resource/windows_ad_join.rb +1 -1
  36. data/lib/chef/resource_collection.rb +6 -0
  37. data/lib/chef/resources.rb +1 -0
  38. data/lib/chef/run_context.rb +61 -27
  39. data/lib/chef/runner.rb +50 -12
  40. data/lib/chef/version.rb +1 -1
  41. data/spec/functional/resource/chocolatey_package_spec.rb +19 -1
  42. data/spec/functional/resource/user/mac_user_spec.rb +207 -0
  43. data/spec/integration/client/client_spec.rb +22 -0
  44. data/spec/integration/knife/raw_spec.rb +39 -19
  45. data/spec/integration/knife/redirection_spec.rb +22 -13
  46. data/spec/integration/knife/serve_spec.rb +1 -2
  47. data/spec/integration/recipes/unified_mode_spec.rb +876 -0
  48. data/spec/spec_helper.rb +1 -0
  49. data/spec/support/platform_helpers.rb +10 -0
  50. data/spec/support/shared/integration/integration_helper.rb +1 -2
  51. data/spec/unit/application/client_spec.rb +5 -6
  52. data/spec/unit/application/solo_spec.rb +3 -8
  53. data/spec/unit/application_spec.rb +1 -1
  54. data/spec/unit/cookbook/gem_installer_spec.rb +22 -1
  55. data/spec/unit/knife/bootstrap/train_connector_spec.rb +20 -7
  56. data/spec/unit/knife/bootstrap_spec.rb +13 -5
  57. data/spec/unit/provider/ifconfig_spec.rb +11 -0
  58. data/spec/unit/provider/package/chocolatey_spec.rb +34 -30
  59. data/spec/unit/provider/user/dscl_spec.rb +1 -0
  60. data/spec/unit/provider/user/mac_spec.rb +38 -0
  61. data/spec/unit/provider/user_spec.rb +38 -22
  62. data/tasks/docs.rb +14 -10
  63. metadata +25 -13
  64. data/spec/support/shared/integration/app_server_support.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 594e166bed32da754bf206f64e2109271ddf1469412338557db108e06261e1de
4
- data.tar.gz: d0f5d5ba5ffe1d1956b9e5ce0fecb7e0b7e97dd27a7932a1f9e496420b31293a
3
+ metadata.gz: d697d8e4f6cda468d0586b03e8c1eddda7a72ab33338c4012fc35ceca6b6f389
4
+ data.tar.gz: 8c6f5f5797b7ca69023b0b29db9b76e76f60b8c6c5469603fbc82815037a991f
5
5
  SHA512:
6
- metadata.gz: 22da301445b2bdf05d08430e5c76a09f87f1e41a2107d101adbd095bd3037286ebf8626aa83e2200ac658ec8d31b94ab113cf1a77dbb9d57d80d6b9f176d52f5
7
- data.tar.gz: 2db2c1bab933e7a5e53fd91ef2a86e53952b6e9669a9f92395c88c5106989dc2e7a6c96bf8c62619228599e4bdeb2145c1042311e54308cfcc21bef7e2d1883e
6
+ metadata.gz: 45977ec05519331f10644ed4ac549554d2330078dc4ea587c48c4dbb22575caaf4efc96e05f2df2108f4f578484b7e0e041d119cf15d6e1f6ee1111e6e0dcc4a
7
+ data.tar.gz: e640ff48fd1aac419652fba2c8fbe3a613a15bb1e88d09b24ef2e2549a2815ebddc8838ef05f234c56b01f3081c83a8256d5883352f0267ee4a4cb53fd6e2ea6
data/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # Chef Infra
2
2
  [![Code Climate](https://codeclimate.com/github/chef/chef.svg)](https://codeclimate.com/github/chef/chef)
3
3
  [![Build Status](https://badge.buildkite.com/c82093430ceec7d27af05febb9dcafe3aa331fff9d74c0ab9d.svg?branch=master)](https://buildkite.com/chef-oss/chef-chef-master-verify)
4
- [![Build Status Master](https://ci.appveyor.com/api/projects/status/github/chef/chef?branch=master&svg=true&passingText=master%20-%20Ok&pendingText=master%20-%20Pending&failingText=master%20-%20Failing)](https://ci.appveyor.com/project/Chef/chef/branch/master)
5
4
  [![Gem Version](https://badge.fury.io/rb/chef.svg)](https://badge.fury.io/rb/chef)
6
- [![](https://img.shields.io/badge/Release%20Policy-Cadence%20Release-brightgreen.svg)](https://github.com/chef/chef-rfc/blob/master/rfc086-chef-oss-project-policies.md#cadence-release)
5
+ [![](https://img.shields.io/badge/Release%20Policy-Cadence%20Release-brightgreen.svg)](https://github.com/chef/chef/blob/v15.2.21/docs/dev/design_documents/client_release_cadence.md)
7
6
 
8
7
  **Umbrella Project**: [Chef Infra](https://github.com/chef/chef-oss-practices/blob/master/projects/chef-infra.md)
9
8
 
@@ -16,13 +16,14 @@ Gem::Specification.new do |s|
16
16
  s.required_ruby_version = ">= 2.5.0"
17
17
 
18
18
  s.add_dependency "chef-config", "= #{Chef::VERSION}"
19
- s.add_dependency "train-core", "~> 2.0", ">= 2.0.12"
19
+ s.add_dependency "train-core", "~> 3.0"
20
+ s.add_dependency "train-winrm"
20
21
 
21
22
  s.add_dependency "license-acceptance", "~> 1.0", ">= 1.0.5"
22
23
  s.add_dependency "mixlib-cli", ">= 2.1.1", "< 3.0"
23
24
  s.add_dependency "mixlib-log", ">= 2.0.3", "< 4.0"
24
25
  s.add_dependency "mixlib-authentication", "~> 2.1"
25
- s.add_dependency "mixlib-shellout", ">= 2.4", "< 4.0"
26
+ s.add_dependency "mixlib-shellout", ">= 3.0.3", "< 4.0"
26
27
  s.add_dependency "mixlib-archive", ">= 0.4", "< 2.0"
27
28
  s.add_dependency "ohai", "~> 15.0"
28
29
 
@@ -163,7 +163,7 @@ class Chef
163
163
  chef_config[:specific_recipes] =
164
164
  cli_arguments.map { |file| File.expand_path(file) }
165
165
  else
166
- Chef::Application.fatal!("Invalid arguments are not supported by the chef-client: \"" +
166
+ Chef::Application.fatal!("Invalid argument; could not find the following recipe files: \"" +
167
167
  cli_arguments.select { |file| !File.file?(file) }.join('", "') + '"')
168
168
  end
169
169
  end
@@ -340,6 +340,13 @@ class Chef::Application::Base < Chef::Application
340
340
 
341
341
  private
342
342
 
343
+ def windows_interval_error_message
344
+ "Windows #{Chef::Dist::PRODUCT} interval runs are not supported in #{Chef::Dist::PRODUCT} 15 and later." +
345
+ "\nConfiguration settings:" +
346
+ ("\n interval = #{Chef::Config[:interval]} seconds" if Chef::Config[:interval]).to_s +
347
+ "\nPlease manage #{Chef::Dist::PRODUCT} as a scheduled task instead."
348
+ end
349
+
343
350
  def unforked_interval_error_message
344
351
  "Unforked #{Chef::Dist::PRODUCT} interval runs are disabled by default." +
345
352
  "\nConfiguration settings:" +
@@ -128,8 +128,12 @@ class Chef::Application::Client < Chef::Application::Base
128
128
  Chef::Config[:client_fork] = !!Chef::Config[:interval]
129
129
  end
130
130
 
131
- if !Chef::Config[:client_fork] && Chef::Config[:interval] && !Chef::Platform.windows?
132
- Chef::Application.fatal!(unforked_interval_error_message)
131
+ if Chef::Config[:interval]
132
+ if Chef::Platform.windows?
133
+ Chef::Application.fatal!(windows_interval_error_message)
134
+ elsif !Chef::Config[:client_fork]
135
+ Chef::Application.fatal!(unforked_interval_error_message)
136
+ end
133
137
  end
134
138
 
135
139
  if Chef::Config[:json_attribs]
@@ -102,7 +102,13 @@ class Chef::Application::Solo < Chef::Application::Base
102
102
  Chef::Config[:client_fork] = !!Chef::Config[:interval]
103
103
  end
104
104
 
105
- Chef::Application.fatal!(unforked_interval_error_message) if !Chef::Config[:client_fork] && Chef::Config[:interval]
105
+ if Chef::Config[:interval]
106
+ if Chef::Platform.windows?
107
+ Chef::Application.fatal!(windows_interval_error_message)
108
+ elsif !Chef::Config[:client_fork]
109
+ Chef::Application.fatal!(unforked_interval_error_message)
110
+ end
111
+ end
106
112
 
107
113
  if Chef::Config[:recipe_url]
108
114
  cookbooks_path = Array(Chef::Config[:cookbook_path]).detect { |e| Pathname.new(e).cleanpath.to_s =~ %r{/cookbooks/*$} }
@@ -66,8 +66,13 @@ class Chef
66
66
  tf.close
67
67
  Chef::Log.trace("generated Gemfile contents:")
68
68
  Chef::Log.trace(IO.read(tf.path))
69
- so = shell_out!("bundle install", cwd: dir, env: { "PATH" => path_with_prepended_ruby_bin })
70
- Chef::Log.info(so.stdout)
69
+ # Skip installation only if Chef::Config[:skip_gem_metadata_installation] option is true
70
+ unless Chef::Config[:skip_gem_metadata_installation]
71
+ # Add additional options to bundle install
72
+ cmd = [ "bundle", "install", Chef::Config[:gem_installer_bundler_options] ]
73
+ so = shell_out!(cmd, cwd: dir, env: { "PATH" => path_with_prepended_ruby_bin })
74
+ Chef::Log.info(so.stdout)
75
+ end
71
76
  end
72
77
  end
73
78
  Gem.clear_paths
@@ -509,5 +509,17 @@ class Chef
509
509
  super "Conflicting requirements for gem '#{gem_name}': Both #{value1.inspect} and #{value2.inspect} given for option #{option.inspect}"
510
510
  end
511
511
  end
512
+
513
+ class UnifiedModeImmediateSubscriptionEarlierResource < RuntimeError
514
+ def initialize(notification)
515
+ super "immediate subscription from #{notification.resource} resource cannot be setup to #{notification.notifying_resource} resource, which has already fired while in unified mode"
516
+ end
517
+ end
518
+
519
+ class UnifiedModeBeforeSubscriptionEarlierResource < RuntimeError
520
+ def initialize(notification)
521
+ super "before subscription from #{notification.resource} resource cannot be setup to #{notification.notifying_resource} resource, which has already fired while in unified mode"
522
+ end
523
+ end
512
524
  end
513
525
  end
@@ -672,6 +672,14 @@ class Chef
672
672
  def do_connect(conn_options)
673
673
  @connection = TrainConnector.new(host_descriptor, connection_protocol, conn_options)
674
674
  connection.connect!
675
+ rescue Train::UserError => e
676
+ if !conn_options.key?(:pty) && e.reason == :sudo_no_tty
677
+ ui.warn("#{e.message} - trying with pty request")
678
+ conn_options[:pty] = true # ensure we can talk to systems with requiretty set true in sshd config
679
+ retry
680
+ else
681
+ raise
682
+ end
675
683
  end
676
684
 
677
685
  # Fail if both first_boot_attributes and first_boot_attributes_from_file
@@ -895,7 +903,6 @@ class Chef
895
903
  opts = {}
896
904
  return opts if winrm?
897
905
 
898
- opts[:pty] = true # ensure we can talk to systems with requiretty set true in sshd config
899
906
  opts[:non_interactive] = true # Prevent password prompts from underlying net/ssh
900
907
  opts[:forward_agent] = (config_value(:ssh_forward_agent) === true)
901
908
  opts[:connection_timeout] = session_timeout
@@ -173,7 +173,7 @@ do_download() {
173
173
  <%= knife_config[:bootstrap_install_command] %>
174
174
  <% else %>
175
175
  install_sh="<%= knife_config[:bootstrap_url] ? knife_config[:bootstrap_url] : "https://omnitruck.chef.io/chef/install.sh" %>"
176
- if test -f /usr/bin/<%= Chef::Dist::CLIENT %>}; then
176
+ if test -f /usr/bin/<%= Chef::Dist::CLIENT %>; then
177
177
  echo "-----> Existing <%= Chef::Dist::PRODUCT %> installation detected"
178
178
  else
179
179
  echo "-----> Installing Chef Omnibus (<%= @config[:channel] %>/<%= version_to_install %>)"
@@ -123,13 +123,13 @@ class Chef
123
123
  # eg. /tmp/chef_XXXXXX.
124
124
  # Use mkdir to create TEMP dir to get rid of mktemp
125
125
  dir = "#{DEFAULT_REMOTE_TEMP}/chef_#{SecureRandom.alphanumeric(6)}"
126
- cmd = "mkdir -p %s" % dir
126
+ run_command!("mkdir -p '#{dir}'")
127
127
  # Ensure that dir has the correct owner. We are possibly
128
128
  # running with sudo right now - so this directory would be owned by root.
129
129
  # File upload is performed over SCP as the current logged-in user,
130
130
  # so we'll set ownership to ensure that works.
131
- cmd += " && sudo chown #{config[:user]} '#{dir}'"
132
- run_command!(cmd)
131
+ run_command!("chown #{config[:user]} '#{dir}'") if config[:sudo]
132
+
133
133
  dir
134
134
  end
135
135
  end
@@ -28,7 +28,7 @@ class Chef
28
28
  require_relative "../cookbook/metadata"
29
29
  end
30
30
 
31
- banner "knife cookbook metadata from FILE (options)"
31
+ banner "knife cookbook metadata from file FILE (options)"
32
32
 
33
33
  def run
34
34
  file = @name_args[0]
@@ -87,8 +87,6 @@ class Chef
87
87
  # after the run_context has been set on the node, go through the cookbook_collection
88
88
  # and setup the node[:cookbooks] attribute so that it is published in the node object
89
89
  def set_cookbook_attribute
90
- return unless run_context.cookbook_collection
91
-
92
90
  run_context.cookbook_collection.each do |cookbook_name, cookbook|
93
91
  automatic_attrs[:cookbooks][cookbook_name][:version] = cookbook.version
94
92
  end
@@ -75,7 +75,6 @@ class Chef
75
75
  #
76
76
  def setup_run_context(specific_recipes = nil, run_context = nil)
77
77
  run_context ||= Chef::RunContext.new
78
-
79
78
  run_context.events = events
80
79
  run_context.node = node
81
80
 
@@ -93,6 +92,7 @@ class Chef
93
92
 
94
93
  cookbook_collection.validate!
95
94
  cookbook_collection.install_gems(events)
95
+
96
96
  run_context.cookbook_collection = cookbook_collection
97
97
 
98
98
  # TODO: move this into the cookbook_compilation_start hook
@@ -177,16 +177,17 @@ class Chef
177
177
  #
178
178
  # @return [Chef::RunContext]
179
179
  def setup_run_context(specific_recipes = nil, run_context = nil)
180
+ run_context ||= Chef::RunContext.new
181
+ run_context.node = node
182
+ run_context.events = events
183
+
180
184
  Chef::Cookbook::FileVendor.fetch_from_remote(api_service)
181
185
  sync_cookbooks
182
186
  cookbook_collection = Chef::CookbookCollection.new(cookbooks_to_sync)
183
187
  cookbook_collection.validate!
184
188
  cookbook_collection.install_gems(events)
185
189
 
186
- run_context ||= Chef::RunContext.new
187
- run_context.node = node
188
190
  run_context.cookbook_collection = cookbook_collection
189
- run_context.events = events
190
191
 
191
192
  setup_chef_class(run_context)
192
193
 
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # Author:: Adam Jacob (<adam@chef.io>)
3
3
  # Author:: Christopher Walters (<cw@chef.io>)
4
- # Copyright:: Copyright 2008-2016, 2009-2018, Chef Software Inc.
4
+ # Copyright:: Copyright 2008-2016, 2009-2019, Chef Software Inc.
5
5
  # License:: Apache License, Version 2.0
6
6
  #
7
7
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -238,8 +238,10 @@ class Chef
238
238
  def compile_and_converge_action(&block)
239
239
  old_run_context = run_context
240
240
  @run_context = run_context.create_child
241
+ @run_context.resource_collection.unified_mode = new_resource.class.unified_mode
242
+ runner = Chef::Runner.new(@run_context)
241
243
  return_value = instance_eval(&block)
242
- Chef::Runner.new(run_context).converge
244
+ runner.converge
243
245
  return_value
244
246
  ensure
245
247
  if run_context.resource_collection.any?(&:updated?)
@@ -109,18 +109,20 @@ class Chef
109
109
  # RX errors 0 dropped 0 overruns 0 frame 0
110
110
  # TX packets 1244218 bytes 977339327 (932.0 MiB)
111
111
  # TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
112
+ #
113
+ # Permalink for addr_regex : https://rubular.com/r/JrykUpfjRnYeQD
112
114
  @status = shell_out("ifconfig")
113
115
  @status.stdout.each_line do |line|
114
- addr_regex = /^(\w+):?(\d*):?\ .+$/
116
+ addr_regex = /^((\w|-)+):?(\d*):?\ .+$/
115
117
  if line =~ addr_regex
116
118
  if line.match(addr_regex).nil?
117
119
  @int_name = "nil"
118
- elsif line.match(addr_regex)[2] == ""
120
+ elsif line.match(addr_regex)[3] == ""
119
121
  @int_name = line.match(addr_regex)[1]
120
122
  @interfaces[@int_name] = {}
121
123
  @interfaces[@int_name]["mtu"] = (line =~ /mtu (\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /mtu/ && @interfaces[@int_name]["mtu"].nil?
122
124
  else
123
- @int_name = "#{line.match(addr_regex)[1]}:#{line.match(addr_regex)[2]}"
125
+ @int_name = "#{line.match(addr_regex)[1]}:#{line.match(addr_regex)[3]}"
124
126
  @interfaces[@int_name] = {}
125
127
  @interfaces[@int_name]["mtu"] = (line =~ /mtu (\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /mtu/ && @interfaces[@int_name]["mtu"].nil?
126
128
  end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright:: Copyright 2015-2016, Chef Software, Inc.
2
+ # Copyright:: Copyright 2015-2019, Chef Software Inc.
3
3
  # License:: Apache License, Version 2.0
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -84,13 +84,13 @@ class Chef
84
84
 
85
85
  # choco does not support installing multiple packages with version pins
86
86
  name_has_versions.each do |name, version|
87
- choco_command("install -y --version", version, cmd_args, name)
87
+ choco_command("install", "-y", "--version", version, cmd_args, name)
88
88
  end
89
89
 
90
90
  # but we can do all the ones without version pins at once
91
91
  unless name_nil_versions.empty?
92
92
  cmd_names = name_nil_versions.keys
93
- choco_command("install -y", cmd_args, *cmd_names)
93
+ choco_command("install", "-y", cmd_args, *cmd_names)
94
94
  end
95
95
  end
96
96
 
@@ -106,13 +106,13 @@ class Chef
106
106
 
107
107
  # choco does not support installing multiple packages with version pins
108
108
  name_has_versions.each do |name, version|
109
- choco_command("upgrade -y --version", version, cmd_args, name)
109
+ choco_command("upgrade", "-y", "--version", version, cmd_args, name)
110
110
  end
111
111
 
112
112
  # but we can do all the ones without version pins at once
113
113
  unless name_nil_versions.empty?
114
114
  cmd_names = name_nil_versions.keys
115
- choco_command("upgrade -y", cmd_args, *cmd_names)
115
+ choco_command("upgrade", "-y", cmd_args, *cmd_names)
116
116
  end
117
117
  end
118
118
 
@@ -121,7 +121,7 @@ class Chef
121
121
  # @param names [Array<String>] array of package names to install
122
122
  # @param versions [Array<String>] array of versions to install
123
123
  def remove_package(names, versions)
124
- choco_command("uninstall -y", cmd_args(include_source: false), *names)
124
+ choco_command("uninstall", "-y", cmd_args(include_source: false), *names)
125
125
  end
126
126
 
127
127
  # Choco does not have dpkg's distinction between purge and remove
@@ -172,7 +172,7 @@ class Chef
172
172
  # @param args [String] variable number of string arguments
173
173
  # @return [Mixlib::ShellOut] object returned from shell_out!
174
174
  def choco_command(*args)
175
- shell_out!(args_to_string(choco_exe, *args), returns: new_resource.returns)
175
+ shell_out!(choco_exe, *args, returns: new_resource.returns)
176
176
  end
177
177
 
178
178
  # Use the available_packages Hash helper to create an array suitable for
@@ -210,18 +210,8 @@ class Chef
210
210
  # @return [String] options from new_resource or empty string
211
211
  def cmd_args(include_source: true)
212
212
  cmd_args = [ new_resource.options ]
213
- cmd_args.push( "-source #{new_resource.source}" ) if new_resource.source && include_source
214
- args_to_string(*cmd_args)
215
- end
216
-
217
- # Helper to nicely convert variable string args into a single command line. It
218
- # will compact nulls or empty strings and join arguments with single spaces, without
219
- # introducing any double-spaces for missing args.
220
- #
221
- # @param args [String] variable number of string arguments
222
- # @return [String] nicely concatenated string or empty string
223
- def args_to_string(*args)
224
- args.reject { |i| i.nil? || i == "" }.join(" ")
213
+ cmd_args.push([ "-source", new_resource.source ]) if new_resource.source && include_source
214
+ cmd_args
225
215
  end
226
216
 
227
217
  # Available packages in chocolatey as a Hash of names mapped to versions
@@ -236,8 +226,8 @@ class Chef
236
226
  package_name_array.each do |pkg|
237
227
  available_versions =
238
228
  begin
239
- cmd = [ "list -r #{pkg}" ]
240
- cmd.push( "-source #{new_resource.source}" ) if new_resource.source
229
+ cmd = [ "list", "-r", pkg ]
230
+ cmd.push( [ "-source", new_resource.source ] ) if new_resource.source
241
231
  cmd.push( new_resource.options ) if new_resource.options
242
232
 
243
233
  raw = parse_list_output(*cmd)
@@ -255,7 +245,7 @@ class Chef
255
245
  #
256
246
  # @return [Hash] name-to-version mapping of installed packages
257
247
  def installed_packages
258
- @installed_packages ||= Hash[*parse_list_output("list -l -r").flatten]
248
+ @installed_packages ||= Hash[*parse_list_output("list", "-l", "-r").flatten]
259
249
  @installed_packages
260
250
  end
261
251
 
@@ -34,7 +34,7 @@ class Chef
34
34
  end
35
35
 
36
36
  def convert_group_name
37
- if new_resource.gid.is_a? String
37
+ if new_resource.gid.is_a?(String) && new_resource.gid.to_i == 0
38
38
  new_resource.gid(Etc.getgrnam(new_resource.gid).gid)
39
39
  end
40
40
  rescue ArgumentError
@@ -42,7 +42,7 @@ class Chef
42
42
  # => shadow binary length 128 bytes
43
43
  # => Salt / Iterations are stored separately in the same file
44
44
  #
45
- # This provider only supports Mac OSX versions 10.7 and above
45
+ # This provider only supports macOS versions 10.7 to 10.13
46
46
  class Dscl < Chef::Provider::User
47
47
 
48
48
  attr_accessor :user_info
@@ -50,7 +50,7 @@ class Chef
50
50
  attr_accessor :password_shadow_conversion_algorithm
51
51
 
52
52
  provides :dscl_user
53
- provides :user, os: "darwin"
53
+ provides :user, os: "darwin", platform_version: "<= 10.13"
54
54
 
55
55
  # Just-in-case a recipe calls the user dscl provider without specifying
56
56
  # a gid property. Avoids chown issues in move_home when the manage_home
@@ -0,0 +1,628 @@
1
+ #
2
+ # Author:: Ryan Cragun (<ryan@chef.io>)
3
+ # Copyright:: Copyright (c) 2019, Chef Software Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require_relative "../../resource"
20
+ require_relative "../../dsl/declare_resource"
21
+ require_relative "../../mixin/shell_out"
22
+ require_relative "../../mixin/which"
23
+ require_relative "../user"
24
+ require_relative "../../resource/user/mac_user"
25
+
26
+ class Chef
27
+ class Provider
28
+ class User
29
+ # A macOS user provider that is compatible with default TCC restrictions
30
+ # in macOS 10.14. See resource/user/mac_user.rb for complete description
31
+ # of the mac_user resource and how it differs from the dscl resource used
32
+ # on previous platforms.
33
+ class MacUser < Chef::Provider::User
34
+ include Chef::Mixin::Which
35
+
36
+ provides :mac_user
37
+ provides :user, os: "darwin", platform_version: ">= 10.14"
38
+
39
+ attr_reader :user_plist, :admin_group_plist
40
+
41
+ def load_current_resource
42
+ @current_resource = Chef::Resource::User::MacUser.new(new_resource.username)
43
+ current_resource.username(new_resource.username)
44
+
45
+ reload_admin_group_plist
46
+ reload_user_plist
47
+
48
+ if user_plist
49
+ current_resource.uid(user_plist[:uid][0])
50
+ current_resource.gid(user_plist[:gid][0])
51
+ current_resource.home(user_plist[:home][0])
52
+ current_resource.shell(user_plist[:shell][0])
53
+ current_resource.comment(user_plist[:comment][0])
54
+
55
+ shadow_hash = user_plist[:shadow_hash]
56
+ if shadow_hash
57
+ current_resource.password(shadow_hash[0]["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack("H*")[0])
58
+ current_resource.salt(shadow_hash[0]["SALTED-SHA512-PBKDF2"]["salt"].string.unpack("H*")[0])
59
+ current_resource.iterations(shadow_hash[0]["SALTED-SHA512-PBKDF2"]["iterations"].to_i)
60
+ end
61
+
62
+ current_resource.secure_token(secure_token_enabled?)
63
+ current_resource.admin(admin_user?)
64
+ else
65
+ @user_exists = false
66
+ logger.trace("#{new_resource} user does not exist")
67
+ end
68
+
69
+ current_resource
70
+ end
71
+
72
+ def reload_admin_group_plist
73
+ @admin_group_plist = nil
74
+
75
+ admin_group_xml = run_dscl("read", "/Groups/admin")
76
+ return nil unless admin_group_xml && admin_group_xml != ""
77
+
78
+ @admin_group_plist = Plist.new(::Plist.parse_xml(admin_group_xml))
79
+ end
80
+
81
+ def reload_user_plist
82
+ @user_plist = nil
83
+
84
+ # Load the user information.
85
+ begin
86
+ user_xml = run_dscl("read", "/Users/#{new_resource.username}")
87
+ rescue Chef::Exceptions::DsclCommandFailed
88
+ return nil
89
+ end
90
+
91
+ return nil if user_xml.nil? || user_xml == ""
92
+
93
+ @user_plist = Plist.new(::Plist.parse_xml(user_xml))
94
+
95
+ shadow_hash_hex = user_plist[:shadow_hash][0]
96
+ return unless shadow_hash_hex && shadow_hash_hex != ""
97
+
98
+ # The password infomation is stored in the ShadowHashData key in the
99
+ # plist. However, parsing it is a bit tricky as the value is itself
100
+ # another encoded binary plist. We have to extract the encoded plist,
101
+ # decode it from hex to a binary plist and then convert the binary
102
+ # into XML plist. From there we can extract the hash data.
103
+ #
104
+ # NOTE: `dscl -read` and `plutil -convert` return different values for
105
+ # ShadowHashData.
106
+ #
107
+ # `dscl` returns the value encoded as a hex string and stored as a <string>
108
+ # `plutil` returns the value encoded as a base64 string stored as <data>
109
+ #
110
+ # eg:
111
+ #
112
+ # <array>
113
+ # <string>77687920 63616e27 74206170 706c6520 6275696c 6420636f 6e736973 74656e74 20746f6f 6c696e67</string>
114
+ # </array>
115
+ #
116
+ # vs
117
+ #
118
+ # <array>
119
+ # <data>AADKAAAKAA4LAA0MAAAAAAAAAAA=</data>
120
+ # </array>
121
+ #
122
+ begin
123
+ shadow_binary_plist = [shadow_hash_hex.delete(" ")].pack("H*")
124
+ shadow_xml_plist = shell_out("plutil", "-convert", "xml1", "-o", "-", "-", input: shadow_binary_plist).stdout
125
+ user_plist[:shadow_hash] = ::Plist.parse_xml(shadow_xml_plist)
126
+ rescue Chef::Exceptions::PlistUtilCommandFailed, Chef::Exceptions::DsclCommandFailed
127
+ nil
128
+ end
129
+ end
130
+
131
+ #
132
+ # User Provider Callbacks
133
+ #
134
+
135
+ def create_user
136
+ cmd = [-"-addUser", new_resource.username]
137
+ cmd += ["-fullName", new_resource.comment] if prop_is_set?(:comment)
138
+ cmd += ["-UID", new_resource.uid] if prop_is_set?(:uid)
139
+ cmd += ["-shell", new_resource.shell]
140
+ cmd += ["-home", new_resource.home]
141
+ cmd += ["-admin"] if new_resource.admin
142
+
143
+ # We can technically create a new user without the admin credentials
144
+ # but without them the user cannot enable SecureToken, thus they cannot
145
+ # create other secure users or enable FileVault full disk encryption.
146
+ if prop_is_set?(:admin_username) && prop_is_set?(:admin_password)
147
+ cmd += ["-adminUser", new_resource.admin_username]
148
+ cmd += ["-adminPassword", new_resource.admin_password]
149
+ end
150
+
151
+ converge_by "create user" do
152
+ # sysadminctl doesn't exit with a non-zero exit code if it encounters
153
+ # a problem. We'll check stderr and make sure we see that it finished
154
+ # correctly.
155
+ res = run_sysadminctl(cmd)
156
+ unless res.downcase =~ /creating user/
157
+ raise Chef::Exceptions::User, "error when creating user: #{res}"
158
+ end
159
+ end
160
+
161
+ # Wait for the user to show up in the ds cache
162
+ wait_for_user
163
+
164
+ # Reload with up-to-date user information
165
+ reload_user_plist
166
+ reload_admin_group_plist
167
+
168
+ if prop_is_set?(:password)
169
+ converge_by("set password") { set_password }
170
+ end
171
+
172
+ if new_resource.manage_home
173
+ # "sydadminctl -addUser" will create the home directory if it's
174
+ # the default /Users/<username>, otherwise it sets it in plist
175
+ # but does not create it. Here we'll ensure that it gets created
176
+ # if we've been given a directory that is not the default.
177
+ unless ::File.directory?(new_resource.home) && ::File.exist?(new_resource.home)
178
+ converge_by("create home directory") do
179
+ shell_out!("createhomedir -c -u #{new_resource.username}")
180
+ end
181
+ end
182
+ end
183
+
184
+ if prop_is_set?(:gid)
185
+ # NOTE: Here we're managing the primary group of the user which is
186
+ # a departure from previous behavior. We could just set the
187
+ # PrimaryGroupID for the user and move on if we decide that actual
188
+ # group magement should be done outside of the core resource.
189
+ group_name, group_id, group_action = user_group_info
190
+
191
+ declare_resource(:group, group_name) do
192
+ members new_resource.username
193
+ gid group_id if group_id
194
+ action :nothing
195
+ append true
196
+ end.run_action(group_action)
197
+
198
+ converge_by("create primary group ID") do
199
+ run_dscl("create", "/Users/#{new_resource.username}", "PrimaryGroupID", new_resource.gid)
200
+ end
201
+ end
202
+
203
+ if diverged?(:secure_token)
204
+ converge_by("alter SecureToken") { toggle_secure_token }
205
+ end
206
+
207
+ reload_user_plist
208
+ end
209
+
210
+ def compare_user
211
+ %i{comment shell uid gid salt password admin secure_token}.any? { |m| diverged?(m) }
212
+ end
213
+
214
+ def manage_user
215
+ %i{uid home}.each do |prop|
216
+ raise Chef::Exceptions::User, "cannot modify #{prop} on macOS >= 10.14" if diverged?(prop)
217
+ end
218
+
219
+ if diverged?(:password)
220
+ converge_by("alter password") { set_password }
221
+ end
222
+
223
+ if diverged?(:comment)
224
+ converge_by("alter comment") do
225
+ run_dscl("create", "/Users/#{new_resource.username}", "RealName", new_resource.comment)
226
+ end
227
+ end
228
+
229
+ if diverged?(:shell)
230
+ converge_by("alter shell") do
231
+ run_dscl("create", "/Users/#{new_resource.username}", "UserShell", new_resource.shell)
232
+ end
233
+ end
234
+
235
+ if diverged?(:secure_token)
236
+ converge_by("alter SecureToken") { toggle_secure_token }
237
+ end
238
+
239
+ if diverged?(:admin)
240
+ converge_by("alter admin group membership") do
241
+ declare_resource(:group, "admin") do
242
+ if new_resource.admin
243
+ members new_resource.username
244
+ else
245
+ excluded_members new_resource.username
246
+ end
247
+
248
+ action :nothing
249
+ append true
250
+ end.run_action(:create)
251
+
252
+ admins = admin_group_plist[:group_members]
253
+ if new_resource.admin
254
+ admins << user_plist[:guid][0]
255
+ else
256
+ admins.reject! { |m| m == user_plist[:guid][0] }
257
+ end
258
+
259
+ run_dscl("create", "/Groups/admin", "GroupMembers", admins)
260
+ end
261
+
262
+ reload_admin_group_plist
263
+ end
264
+
265
+ group_name, group_id, group_action = user_group_info
266
+ declare_resource(:group, group_name) do
267
+ gid group_id if group_id
268
+ members new_resource.username
269
+ action :nothing
270
+ append true
271
+ end.run_action(group_action)
272
+
273
+ if diverged?(:gid)
274
+ converge_by("alter group membership") do
275
+ run_dscl("create", "/Users/#{new_resource.username}", "PrimaryGroupID", new_resource.gid)
276
+ end
277
+ end
278
+
279
+ reload_user_plist
280
+ end
281
+
282
+ def remove_user
283
+ cmd = ["-deleteUser", new_resource.username]
284
+ cmd << new_resource.manage_home ? "-secure" : "-keepHome"
285
+ if %i{admin_username admin_password}.all? { |p| prop_is_set?(p) }
286
+ cmd += ["-adminUser", new_resource.admin_username]
287
+ cmd += ["-adminPassword", new_resource.admin_password]
288
+ end
289
+
290
+ # sysadminctl doesn't exit with a non-zero exit code if it encounters
291
+ # a problem. We'll check stderr and make sure we see that it finished
292
+ converge_by "remove user" do
293
+ res = run_sysadminctl(cmd)
294
+ unless res.downcase =~ /deleting record|not found/
295
+ raise Chef::Exceptions::User, "error deleting user: #{res}"
296
+ end
297
+ end
298
+
299
+ reload_user_plist
300
+ @user_exists = false
301
+ end
302
+
303
+ def lock_user
304
+ converge_by "lock user" do
305
+ run_dscl("append", "/Users/#{new_resource.username}", "AuthenticationAuthority", ";DisabledUser;")
306
+ end
307
+
308
+ reload_user_plist
309
+ end
310
+
311
+ def unlock_user
312
+ auth_string = user_plist[:auth_authority].reject! { |tag| tag == ";DisabledUser;" }.join.strip
313
+ converge_by "unlock user" do
314
+ run_dscl("create", "/Users/#{new_resource.username}", "AuthenticationAuthority", auth_string)
315
+ end
316
+
317
+ reload_user_plist
318
+ end
319
+
320
+ def locked?
321
+ user_plist[:auth_authority].any? { |tag| tag == ";DisabledUser;" }
322
+ rescue
323
+ false
324
+ end
325
+
326
+ def check_lock
327
+ @locked = locked?
328
+ end
329
+
330
+ #
331
+ # Methods
332
+ #
333
+
334
+ def diverged?(prop)
335
+ prop = prop.to_sym
336
+
337
+ case prop
338
+ when :password
339
+ password_diverged?
340
+ when :gid
341
+ user_group_diverged?
342
+ when :secure_token
343
+ secure_token_diverged?
344
+ else
345
+ # Other fields are have been set on current resource so just compare
346
+ # them.
347
+ !new_resource.send(prop).nil? && (new_resource.send(prop) != current_resource.send(prop))
348
+ end
349
+ end
350
+
351
+ # Attempt to resolve the group name, gid, and the action required for
352
+ # associated group resource. If a group exists we'll modify it, otherwise
353
+ # create it.
354
+ def user_group_info
355
+ @user_group_info ||= begin
356
+ if new_resource.gid.is_a?(String)
357
+ begin
358
+ g = Etc.getgrnam(new_resource.gid)
359
+ [g.name, g.gid.to_s, :modify]
360
+ rescue
361
+ [new_resource.gid, nil, :create]
362
+ end
363
+ else
364
+ begin
365
+ g = Etc.getgrgid(new_resource.gid)
366
+ [g.name, g.gid.to_s, :modify]
367
+ rescue
368
+ [g.username, nil, :create]
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+ def secure_token_enabled?
375
+ user_plist[:auth_authority].any? { |tag| tag == ";SecureToken;" }
376
+ rescue
377
+ false
378
+ end
379
+
380
+ def secure_token_diverged?
381
+ new_resource.secure_token ? !secure_token_enabled? : secure_token_enabled?
382
+ end
383
+
384
+ def toggle_secure_token
385
+ # Check for this lazily as we only need to validate for these credentials
386
+ # if we're toggling secure token.
387
+ unless %i{admin_username admin_password secure_token_password}.all? { |p| prop_is_set?(p) }
388
+ raise Chef::Exceptions::User, "secure_token_password, admin_username and admin_password properties are required to modify SecureToken"
389
+ end
390
+
391
+ cmd = (new_resource.secure_token ? %w{-secureTokenOn} : %w{-secureTokenOff})
392
+ cmd += [new_resource.username, "-password", new_resource.secure_token_password]
393
+ cmd += ["-adminUser", new_resource.admin_username]
394
+ cmd += ["-adminPassword", new_resource.admin_password]
395
+
396
+ # sysadminctl doesn't exit with a non-zero exit code if it encounters
397
+ # a problem. We'll check stderr and make sure we see that it finished
398
+ res = run_sysadminctl(cmd)
399
+ unless res.downcase =~ /done/
400
+ raise Chef::Exceptions::User, "error when modifying SecureToken: #{res}"
401
+ end
402
+
403
+ # HACK: When SecureToken is enabled or disabled it requires the user
404
+ # password in plaintext, which it verifies and uses as a key. It also
405
+ # takes the liberty of _rehashing_ the password with a random salt and
406
+ # iterations count and saves it back into the user ShadowHashData.
407
+ #
408
+ # Therefore, if we're configuring a user based upon existing shadow
409
+ # hash data we'll have to set the password again so that future runs
410
+ # of the client don't show password drift.
411
+ set_password if prop_is_set?(:salt)
412
+ end
413
+
414
+ def user_group_diverged?
415
+ return false unless prop_is_set?(:gid)
416
+
417
+ group_name, group_id = user_group_info
418
+
419
+ if current_resource.gid.is_a?(String)
420
+ current_resource.gid != group_name
421
+ else
422
+ current_resource.gid != group_id.to_i
423
+ end
424
+ end
425
+
426
+ def password_diverged?
427
+ # There are three options for configuring the password:
428
+ # * ShadowHashData which includes the hash data as:
429
+ # * hashed entropy as the "password"
430
+ # * salt
431
+ # * iterations
432
+ # * Plaintext password
433
+ # * Not configuring it
434
+
435
+ # Check for no desired password configuration
436
+ return false unless prop_is_set?(:password)
437
+
438
+ # Check for ShadowHashData divergence by comparing the entropy,
439
+ # salt, and iterations.
440
+ if prop_is_set?(:salt)
441
+ return true if %i{salt iterations}.any? { |prop| diverged?(prop) }
442
+
443
+ return new_resource.password != current_resource.password
444
+ end
445
+
446
+ # Check for plaintext password divergence. We don't actually know
447
+ # what the stored password is but we can hash the given password with
448
+ # stored salt and iterations, and compare the resulting entropy with
449
+ # the saved entropy.
450
+ OpenSSL::PKCS5.pbkdf2_hmac(
451
+ new_resource.password,
452
+ convert_to_binary(current_resource.salt),
453
+ current_resource.iterations.to_i,
454
+ 128,
455
+ OpenSSL::Digest::SHA512.new
456
+ ).unpack("H*")[0] != current_resource.password
457
+ end
458
+
459
+ def admin_user?
460
+ admin_group_plist[:group_members].any? { |mem| mem == user_plist[:guid][0] }
461
+ rescue
462
+ false
463
+ end
464
+
465
+ def convert_to_binary(string)
466
+ string.unpack("a2" * (string.size / 2)).collect { |i| i.hex.chr }.join
467
+ end
468
+
469
+ def set_password
470
+ if prop_is_set?(:salt)
471
+ entropy = StringIO.new(convert_to_binary(new_resource.password))
472
+ salt = StringIO.new(convert_to_binary(new_resource.salt))
473
+ else
474
+ salt = StringIO.new(OpenSSL::Random.random_bytes(32))
475
+ entropy = StringIO.new(
476
+ OpenSSL::PKCS5.pbkdf2_hmac(
477
+ new_resource.password,
478
+ salt.string,
479
+ new_resource.iterations,
480
+ 128,
481
+ OpenSSL::Digest::SHA512.new
482
+ )
483
+ )
484
+ end
485
+
486
+ shadow_hash = user_plist[:shadow_hash][0]
487
+ shadow_hash["SALTED-SHA512-PBKDF2"] = {
488
+ "entropy" => entropy,
489
+ "salt" => salt,
490
+ "iterations" => new_resource.iterations,
491
+ }
492
+
493
+ shadow_hash_binary = StringIO.new
494
+ shell_out("plutil", "-convert", "binary1", "-o", "-", "-",
495
+ input: shadow_hash.to_plist,
496
+ live_stream: shadow_hash_binary)
497
+
498
+ # Apple seem to have killed their dsimport documentation about the
499
+ # dsimport record format. Perhaps that means our days of being able to
500
+ # use dsimport without an admin password or perhaps at all could be
501
+ # numbered. Here is the record format for posterity:
502
+ #
503
+ # End of record character
504
+ # Escape character
505
+ # Field separator
506
+ # Value separator
507
+ # Record type (Users, Groups, Computers, ComputerGroups, ComputerLists)
508
+ # Number of properties
509
+ # Property 1
510
+ # ...
511
+ # Property N
512
+ #
513
+ # The user password shadow data format breaks down as:
514
+ #
515
+ # 0x0A End of record denoted by \n
516
+ # 0x5C Escaping is denoted by \
517
+ # 0x3A Fields are separated by :
518
+ # 0x2C Values are seperated by ,
519
+ # dsRecTypeStandard:Users The record type we're configuring
520
+ # 2 How many properties we're going to set
521
+ # dsAttrTypeStandard:RecordName Property 1: our users record name
522
+ # base64:dsAttrTypeNative:ShadowHashData Property 2: our shadow hash data
523
+
524
+ import_file = ::File.join(Chef::Config["file_cache_path"], "#{new_resource.username}_password_dsimport")
525
+ ::File.open(import_file, "w+", 0600) do |f|
526
+ f.write <<~DSIMPORT
527
+ 0x0A 0x5C 0x3A 0x2C dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName base64:dsAttrTypeNative:ShadowHashData
528
+ #{new_resource.username}:#{::Base64.strict_encode64(shadow_hash_binary.string)}
529
+ DSIMPORT
530
+ end
531
+
532
+ run_dscl("delete", "/Users/#{new_resource.username}", "ShadowHashData")
533
+ run_dsimport(import_file, "/Local/Default", "M")
534
+ run_dscl("create", "/Users/#{new_resource.username}", "Password", "********")
535
+ ensure
536
+ ::File.delete(import_file) if defined?(import_file) && ::File.exist?(import_file)
537
+ end
538
+
539
+ def wait_for_user
540
+ timeout = Time.now + 5
541
+
542
+ loop do
543
+ begin
544
+ run_dscl("read", "/Users/#{new_resource.username}", "ShadowHashData")
545
+ break
546
+ rescue Chef::Exceptions::DsclCommandFailed => e
547
+ if Time.now < timeout
548
+ sleep 0.1
549
+ else
550
+ raise Chef::Exceptions::User, e.message
551
+ end
552
+ end
553
+ end
554
+ end
555
+
556
+ def run_dsimport(*args)
557
+ shell_out!("dsimport", args)
558
+ end
559
+
560
+ def run_sysadminctl(args)
561
+ # sysadminctl doesn't exit with a non-zero code when errors are encountered
562
+ # and ouputs everything to STDERR instead of STDOUT and STDERR. Therefore we'll
563
+ # return the STDERR and let the caller handle it.
564
+ shell_out!("sysadminctl", args).stderr
565
+ end
566
+
567
+ def run_dscl(*args)
568
+ result = shell_out("dscl", "-plist", ".", "-#{args[0]}", args[1..-1])
569
+ return "" if ( args.first =~ /^delete/ ) && ( result.exitstatus != 0 )
570
+ raise(Chef::Exceptions::DsclCommandFailed, "dscl error: #{result.inspect}") unless result.exitstatus == 0
571
+ raise(Chef::Exceptions::DsclCommandFailed, "dscl error: #{result.inspect}") if result.stdout =~ /No such key: /
572
+
573
+ result.stdout
574
+ end
575
+
576
+ def run_plutil(*args)
577
+ result = shell_out("plutil", "-#{args[0]}", args[1..-1])
578
+ raise(Chef::Exceptions::PlistUtilCommandFailed, "plutil error: #{result.inspect}") unless result.exitstatus == 0
579
+
580
+ result.stdout
581
+ end
582
+
583
+ def prop_is_set?(prop)
584
+ v = new_resource.send(prop.to_sym)
585
+
586
+ !v.nil? && v != ""
587
+ end
588
+
589
+ class Plist
590
+ DSCL_PROPERTY_MAP = {
591
+ uid: "dsAttrTypeStandard:UniqueID",
592
+ guid: "dsAttrTypeStandard:GeneratedUID",
593
+ gid: "dsAttrTypeStandard:PrimaryGroupID",
594
+ home: "dsAttrTypeStandard:NFSHomeDirectory",
595
+ shell: "dsAttrTypeStandard:UserShell",
596
+ comment: "dsAttrTypeStandard:RealName",
597
+ password: "dsAttrTypeStandard:Password",
598
+ auth_authority: "dsAttrTypeStandard:AuthenticationAuthority",
599
+ shadow_hash: "dsAttrTypeNative:ShadowHashData",
600
+ group_members: "dsAttrTypeStandard:GroupMembers",
601
+ }.freeze
602
+
603
+ attr_accessor :plist_hash, :property_map
604
+
605
+ def initialize(plist_hash = {}, property_map = DSCL_PROPERTY_MAP)
606
+ @plist_hash = plist_hash
607
+ @property_map = property_map
608
+ end
609
+
610
+ def get(key)
611
+ return nil unless property_map.key?(key)
612
+
613
+ plist_hash[property_map[key]]
614
+ end
615
+ alias_method :[], :get
616
+
617
+ def set(key, value)
618
+ return nil unless property_map.key?(key)
619
+
620
+ plist_hash[property_map[key]] = [ value ]
621
+ end
622
+ alias_method :[]=, :set
623
+
624
+ end
625
+ end
626
+ end
627
+ end
628
+ end