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.
- 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
|
[![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
|
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
|
|
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
|