chef 15.2.20-universal-mingw32 → 15.3.14-universal-mingw32
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.
- checksums.yaml +4 -4
- data/README.md +1 -2
- data/chef.gemspec +3 -2
- data/lib/chef/application.rb +1 -1
- data/lib/chef/application/base.rb +7 -0
- data/lib/chef/application/client.rb +6 -2
- data/lib/chef/application/solo.rb +7 -1
- data/lib/chef/cookbook/gem_installer.rb +7 -2
- data/lib/chef/exceptions.rb +12 -0
- data/lib/chef/knife/bootstrap.rb +8 -1
- data/lib/chef/knife/bootstrap/templates/chef-full.erb +1 -1
- data/lib/chef/knife/bootstrap/train_connector.rb +3 -3
- data/lib/chef/knife/cookbook_metadata_from_file.rb +1 -1
- data/lib/chef/node.rb +0 -2
- data/lib/chef/policy_builder/expand_node_object.rb +1 -1
- data/lib/chef/policy_builder/policyfile.rb +4 -3
- data/lib/chef/provider.rb +4 -2
- data/lib/chef/provider/ifconfig.rb +5 -3
- data/lib/chef/provider/package/chocolatey.rb +12 -22
- data/lib/chef/provider/user.rb +1 -1
- data/lib/chef/provider/user/dscl.rb +2 -2
- data/lib/chef/provider/user/mac.rb +628 -0
- data/lib/chef/providers.rb +1 -0
- data/lib/chef/resource.rb +28 -20
- data/lib/chef/resource/chocolatey_feature.rb +1 -1
- data/lib/chef/resource/chocolatey_package.rb +2 -2
- data/lib/chef/resource/cron_d.rb +1 -1
- data/lib/chef/resource/ohai.rb +1 -1
- data/lib/chef/resource/resource_notification.rb +17 -13
- data/lib/chef/resource/ruby_block.rb +1 -1
- data/lib/chef/resource/service.rb +1 -1
- data/lib/chef/resource/user.rb +1 -0
- data/lib/chef/resource/user/dscl_user.rb +1 -1
- data/lib/chef/resource/user/mac_user.rb +119 -0
- data/lib/chef/resource/windows_ad_join.rb +1 -1
- data/lib/chef/resource_collection.rb +6 -0
- data/lib/chef/resources.rb +1 -0
- data/lib/chef/run_context.rb +61 -27
- data/lib/chef/runner.rb +50 -12
- data/lib/chef/version.rb +1 -1
- data/spec/functional/resource/chocolatey_package_spec.rb +19 -1
- data/spec/functional/resource/user/mac_user_spec.rb +207 -0
- data/spec/integration/client/client_spec.rb +22 -0
- data/spec/integration/knife/raw_spec.rb +39 -19
- data/spec/integration/knife/redirection_spec.rb +22 -13
- data/spec/integration/knife/serve_spec.rb +1 -2
- data/spec/integration/recipes/unified_mode_spec.rb +876 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/platform_helpers.rb +10 -0
- data/spec/support/shared/integration/integration_helper.rb +1 -2
- data/spec/unit/application/client_spec.rb +5 -6
- data/spec/unit/application/solo_spec.rb +3 -8
- data/spec/unit/application_spec.rb +1 -1
- data/spec/unit/cookbook/gem_installer_spec.rb +22 -1
- data/spec/unit/knife/bootstrap/train_connector_spec.rb +20 -7
- data/spec/unit/knife/bootstrap_spec.rb +13 -5
- data/spec/unit/provider/ifconfig_spec.rb +11 -0
- data/spec/unit/provider/package/chocolatey_spec.rb +34 -30
- data/spec/unit/provider/user/dscl_spec.rb +1 -0
- data/spec/unit/provider/user/mac_spec.rb +38 -0
- data/spec/unit/provider/user_spec.rb +38 -22
- data/tasks/docs.rb +14 -10
- metadata +25 -13
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d697d8e4f6cda468d0586b03e8c1eddda7a72ab33338c4012fc35ceca6b6f389
|
4
|
+
data.tar.gz: 8c6f5f5797b7ca69023b0b29db9b76e76f60b8c6c5469603fbc82815037a991f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45977ec05519331f10644ed4ac549554d2330078dc4ea587c48c4dbb22575caaf4efc96e05f2df2108f4f578484b7e0e041d119cf15d6e1f6ee1111e6e0dcc4a
|
7
|
+
data.tar.gz: e640ff48fd1aac419652fba2c8fbe3a613a15bb1e88d09b24ef2e2549a2815ebddc8838ef05f234c56b01f3081c83a8256d5883352f0267ee4a4cb53fd6e2ea6
|
data/README.md
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
# Chef Infra
|
2
2
|
[](https://codeclimate.com/github/chef/chef)
|
3
3
|
[](https://buildkite.com/chef-oss/chef-chef-master-verify)
|
4
|
-
[](https://ci.appveyor.com/project/Chef/chef/branch/master)
|
5
4
|
[](https://badge.fury.io/rb/chef)
|
6
|
-
[](https://github.com/chef/chef
|
5
|
+
[](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
|
|
data/chef.gemspec
CHANGED
@@ -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", "~>
|
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", ">=
|
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
|
|
data/lib/chef/application.rb
CHANGED
@@ -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
|
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
|
132
|
-
Chef::
|
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
|
-
|
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
|
-
|
70
|
-
Chef::
|
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
|
data/lib/chef/exceptions.rb
CHANGED
@@ -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
|
data/lib/chef/knife/bootstrap.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
132
|
-
|
131
|
+
run_command!("chown #{config[:user]} '#{dir}'") if config[:sudo]
|
132
|
+
|
133
133
|
dir
|
134
134
|
end
|
135
135
|
end
|
data/lib/chef/node.rb
CHANGED
@@ -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
|
|
data/lib/chef/provider.rb
CHANGED
@@ -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-
|
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
|
-
|
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)[
|
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)[
|
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-
|
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!(
|
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
|
214
|
-
|
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
|
240
|
-
cmd.push( "-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
|
|
data/lib/chef/provider/user.rb
CHANGED
@@ -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
|
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
|