chef 10.32.2-x86-mingw32 → 10.34.0-x86-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.
- data/distro/common/html/chef-client.8.html +4 -4
- data/distro/common/html/chef-expander.8.html +4 -4
- data/distro/common/html/chef-expanderctl.8.html +4 -4
- data/distro/common/html/chef-server-webui.8.html +4 -4
- data/distro/common/html/chef-server.8.html +4 -4
- data/distro/common/html/chef-solo.8.html +4 -4
- data/distro/common/html/chef-solr.8.html +5 -5
- data/distro/common/html/knife-bootstrap.1.html +4 -4
- data/distro/common/html/knife-client.1.html +4 -4
- data/distro/common/html/knife-configure.1.html +4 -4
- data/distro/common/html/knife-cookbook-site.1.html +4 -4
- data/distro/common/html/knife-cookbook.1.html +4 -4
- data/distro/common/html/knife-data-bag.1.html +4 -4
- data/distro/common/html/knife-environment.1.html +4 -4
- data/distro/common/html/knife-exec.1.html +4 -4
- data/distro/common/html/knife-index.1.html +4 -4
- data/distro/common/html/knife-node.1.html +4 -4
- data/distro/common/html/knife-role.1.html +4 -4
- data/distro/common/html/knife-search.1.html +4 -4
- data/distro/common/html/knife-ssh.1.html +4 -4
- data/distro/common/html/knife-status.1.html +4 -4
- data/distro/common/html/knife-tag.1.html +4 -4
- data/distro/common/html/knife.1.html +4 -4
- data/distro/common/html/shef.1.html +4 -4
- data/distro/common/man/man1/knife-bootstrap.1 +1 -1
- data/distro/common/man/man1/knife-client.1 +1 -1
- data/distro/common/man/man1/knife-configure.1 +1 -1
- data/distro/common/man/man1/knife-cookbook-site.1 +1 -1
- data/distro/common/man/man1/knife-cookbook.1 +1 -1
- data/distro/common/man/man1/knife-data-bag.1 +1 -1
- data/distro/common/man/man1/knife-environment.1 +1 -1
- data/distro/common/man/man1/knife-exec.1 +1 -1
- data/distro/common/man/man1/knife-index.1 +1 -1
- data/distro/common/man/man1/knife-node.1 +1 -1
- data/distro/common/man/man1/knife-role.1 +1 -1
- data/distro/common/man/man1/knife-search.1 +1 -1
- data/distro/common/man/man1/knife-ssh.1 +1 -1
- data/distro/common/man/man1/knife-status.1 +1 -1
- data/distro/common/man/man1/knife-tag.1 +1 -1
- data/distro/common/man/man1/knife.1 +1 -1
- data/distro/common/man/man1/shef.1 +1 -1
- data/distro/common/man/man8/chef-client.8 +1 -1
- data/distro/common/man/man8/chef-expander.8 +1 -1
- data/distro/common/man/man8/chef-expanderctl.8 +1 -1
- data/distro/common/man/man8/chef-server-webui.8 +1 -1
- data/distro/common/man/man8/chef-server.8 +1 -1
- data/distro/common/man/man8/chef-solo.8 +1 -1
- data/distro/common/man/man8/chef-solr.8 +1 -1
- data/lib/chef/client.rb +1 -2
- data/lib/chef/exceptions.rb +1 -0
- data/lib/chef/node.rb +1 -1
- data/lib/chef/provider/group/dscl.rb +27 -9
- data/lib/chef/provider/package/rpm.rb +2 -2
- data/lib/chef/provider/service/macosx.rb +5 -1
- data/lib/chef/provider/user/dscl.rb +569 -172
- data/lib/chef/resource/mount.rb +11 -10
- data/lib/chef/resource/user.rb +18 -0
- data/lib/chef/rest/rest_request.rb +1 -0
- data/lib/chef/shef.rb +7 -0
- data/lib/chef/shef/shef_session.rb +4 -4
- data/lib/chef/version.rb +1 -1
- data/spec/data/mac_users/10.7-8.plist.xml +559 -0
- data/spec/data/mac_users/10.7-8.shadow.xml +11 -0
- data/spec/data/mac_users/10.7.plist.xml +559 -0
- data/spec/data/mac_users/10.7.shadow.xml +11 -0
- data/spec/data/mac_users/10.8.plist.xml +559 -0
- data/spec/data/mac_users/10.8.shadow.xml +21 -0
- data/spec/data/mac_users/10.9.plist.xml +560 -0
- data/spec/data/mac_users/10.9.shadow.xml +21 -0
- data/spec/functional/resource/user/dscl_spec.rb +199 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/unit/client_spec.rb +23 -4
- data/spec/unit/provider/group/dscl_spec.rb +35 -0
- data/spec/unit/provider/package/rpm_spec.rb +12 -0
- data/spec/unit/provider/service/macosx_spec.rb +37 -0
- data/spec/unit/provider/user/dscl_spec.rb +705 -284
- data/spec/unit/resource/mount_spec.rb +11 -0
- data/spec/unit/rest/auth_credentials_spec.rb +5 -0
- data/spec/unit/shef/shef_session_spec.rb +44 -2
- metadata +223 -116
- checksums.yaml +0 -7
@@ -60,7 +60,7 @@ class Chef
|
|
60
60
|
status = popen4("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}") do |pid, stdin, stdout, stderr|
|
61
61
|
stdout.each do |line|
|
62
62
|
case line
|
63
|
-
when
|
63
|
+
when /^([\w\d_.-]+)\s([\w\d_.-]+)$/
|
64
64
|
@current_resource.package_name($1)
|
65
65
|
@new_resource.version($2)
|
66
66
|
end
|
@@ -77,7 +77,7 @@ class Chef
|
|
77
77
|
@rpm_status = popen4("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") do |pid, stdin, stdout, stderr|
|
78
78
|
stdout.each do |line|
|
79
79
|
case line
|
80
|
-
when
|
80
|
+
when /^([\w\d_.-]+)\s([\w\d_.-]+)$/
|
81
81
|
Chef::Log.debug("#{@new_resource} current version is #{$2}")
|
82
82
|
@current_resource.version($2)
|
83
83
|
end
|
@@ -53,7 +53,7 @@ class Chef
|
|
53
53
|
a.failure_message Chef::Exceptions::Service, "Several plist files match service name. Please use full service name."
|
54
54
|
end
|
55
55
|
|
56
|
-
requirements.assert(:
|
56
|
+
requirements.assert(:enable, :disable) do |a|
|
57
57
|
a.assertion { !@service_label.to_s.empty? }
|
58
58
|
a.failure_message Chef::Exceptions::Service,
|
59
59
|
"Could not find service's label in plist file '#{@plist}'!"
|
@@ -166,6 +166,10 @@ class Chef
|
|
166
166
|
private
|
167
167
|
|
168
168
|
def find_service_label
|
169
|
+
# CHEF-5223 "you can't glob for a file that hasn't been converged
|
170
|
+
# onto the node yet."
|
171
|
+
return nil if @plist.nil?
|
172
|
+
|
169
173
|
# Most services have the same internal label as the name of the
|
170
174
|
# plist file. However, there is no rule saying that *has* to be
|
171
175
|
# the case, and some core services (notably, ssh) do not follow
|
@@ -6,9 +6,9 @@
|
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
8
8
|
# You may obtain a copy of the License at
|
9
|
-
#
|
9
|
+
#
|
10
10
|
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
-
#
|
11
|
+
#
|
12
12
|
# Unless required by applicable law or agreed to in writing, software
|
13
13
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
14
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
@@ -17,238 +17,284 @@
|
|
17
17
|
#
|
18
18
|
|
19
19
|
require 'chef/mixin/shell_out'
|
20
|
+
require 'mixlib/shellout'
|
20
21
|
require 'chef/provider/user'
|
21
22
|
require 'openssl'
|
23
|
+
require 'plist'
|
22
24
|
|
23
25
|
class Chef
|
24
26
|
class Provider
|
25
27
|
class User
|
28
|
+
include Chef::Mixin::ShellOut
|
29
|
+
|
30
|
+
#
|
31
|
+
# The most tricky bit of this provider is the way it deals with user passwords.
|
32
|
+
# Mac OS X has different password shadow calculations based on the version.
|
33
|
+
# < 10.7 => password shadow calculation format SALTED-SHA1
|
34
|
+
# => stored in: /var/db/shadow/hash/#{guid}
|
35
|
+
# => shadow binary length 68 bytes
|
36
|
+
# => First 4 bytes salt / Next 64 bytes shadow value
|
37
|
+
# = 10.7 => password shadow calculation format SALTED-SHA512
|
38
|
+
# => stored in: /var/db/dslocal/nodes/Default/users/#{name}.plist
|
39
|
+
# => shadow binary length 68 bytes
|
40
|
+
# => First 4 bytes salt / Next 64 bytes shadow value
|
41
|
+
# > 10.7 => password shadow calculation format SALTED-SHA512-PBKDF2
|
42
|
+
# => stored in: /var/db/dslocal/nodes/Default/users/#{name}.plist
|
43
|
+
# => shadow binary length 128 bytes
|
44
|
+
# => Salt / Iterations are stored seperately in the same file
|
45
|
+
#
|
46
|
+
# This provider only supports Mac OSX versions 10.7 and above
|
26
47
|
class Dscl < Chef::Provider::User
|
27
|
-
include Chef::Mixin::ShellOut
|
28
|
-
|
29
|
-
NFS_HOME_DIRECTORY = %r{^NFSHomeDirectory: (.*)$}
|
30
|
-
AUTHENTICATION_AUTHORITY = %r{^AuthenticationAuthority: (.*)$}
|
31
|
-
|
32
|
-
def dscl(*args)
|
33
|
-
shell_out("dscl . -#{args.join(' ')}")
|
34
|
-
end
|
35
48
|
|
36
|
-
def
|
37
|
-
|
38
|
-
return "" if ( args.first =~ /^delete/ ) && ( result.exitstatus != 0 )
|
39
|
-
raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") unless result.exitstatus == 0
|
40
|
-
raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") if result.stdout =~ /No such key: /
|
41
|
-
return result.stdout
|
42
|
-
end
|
49
|
+
def define_resource_requirements
|
50
|
+
super
|
43
51
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
# end
|
52
|
+
requirements.assert(:all_actions) do |a|
|
53
|
+
a.assertion { mac_osx_version_less_than_10_7? == false }
|
54
|
+
a.failure_message(Chef::Exceptions::User, "Chef::Provider::User::Dscl only supports Mac OS X versions 10.7 and above.")
|
55
|
+
end
|
49
56
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
users_uids = safe_dscl("list /Users uid")
|
54
|
-
while(next_uid_guess < search_limit + 200)
|
55
|
-
if users_uids =~ Regexp.new("#{Regexp.escape(next_uid_guess.to_s)}\n")
|
56
|
-
next_uid_guess += 1
|
57
|
-
else
|
58
|
-
uid = next_uid_guess
|
59
|
-
break
|
60
|
-
end
|
57
|
+
requirements.assert(:all_actions) do |a|
|
58
|
+
a.assertion { ::File.exists?("/usr/bin/dscl") }
|
59
|
+
a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/dscl' on the system for #{@new_resource}!")
|
61
60
|
end
|
62
|
-
return uid || raise("uid not found. Exhausted. Searched #{search_limit} times")
|
63
|
-
end
|
64
61
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
62
|
+
requirements.assert(:all_actions) do |a|
|
63
|
+
a.assertion { ::File.exists?("/usr/bin/plutil") }
|
64
|
+
a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/plutil' on the system for #{@new_resource}!")
|
65
|
+
end
|
70
66
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
67
|
+
requirements.assert(:create, :modify, :manage) do |a|
|
68
|
+
a.assertion do
|
69
|
+
if @new_resource.password && mac_osx_version_greater_than_10_7?
|
70
|
+
# SALTED-SHA512 password shadow hashes are not supported on 10.8 and above.
|
71
|
+
!salted_sha512?(@new_resource.password)
|
72
|
+
else
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
a.failure_message(Chef::Exceptions::User, "SALTED-SHA512 passwords are not supported on Mac 10.8 and above. \
|
77
|
+
If you want to set the user password using shadow info make sure you specify a SALTED-SHA512-PBKDF2 shadow hash \
|
78
|
+
in 'password', with the associated 'salt' and 'iterations'.")
|
75
79
|
end
|
76
|
-
safe_dscl("create /Users/#{@new_resource.username} UniqueID #{@new_resource.uid}")
|
77
|
-
end
|
78
80
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
elsif current_home_exists?
|
89
|
-
move_home
|
81
|
+
requirements.assert(:create, :modify, :manage) do |a|
|
82
|
+
a.assertion do
|
83
|
+
if @new_resource.password && mac_osx_version_greater_than_10_7? && salted_sha512_pbkdf2?(@new_resource.password)
|
84
|
+
# salt and iterations should be specified when
|
85
|
+
# SALTED-SHA512-PBKDF2 password shadow hash is given
|
86
|
+
!@new_resource.salt.nil? && !@new_resource.iterations.nil?
|
87
|
+
else
|
88
|
+
true
|
89
|
+
end
|
90
90
|
end
|
91
|
+
a.failure_message(Chef::Exceptions::User, "SALTED-SHA512-PBKDF2 shadow hash is given without associated \
|
92
|
+
'salt' and 'iterations'. Please specify 'salt' and 'iterations' in order to set the user password using shadow hash.")
|
91
93
|
end
|
92
|
-
safe_dscl("create /Users/#{@new_resource.username} NFSHomeDirectory '#{@new_resource.home}'")
|
93
|
-
end
|
94
94
|
|
95
|
-
|
96
|
-
|
97
|
-
|
95
|
+
requirements.assert(:create, :modify, :manage) do |a|
|
96
|
+
a.assertion do
|
97
|
+
if @new_resource.password && !mac_osx_version_greater_than_10_7?
|
98
|
+
# On 10.7 SALTED-SHA512-PBKDF2 is not supported
|
99
|
+
!salted_sha512_pbkdf2?(@new_resource.password)
|
100
|
+
else
|
101
|
+
true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
a.failure_message(Chef::Exceptions::User, "SALTED-SHA512-PBKDF2 shadow hashes are not supported on \
|
105
|
+
Mac OS X version 10.7. Please specify a SALTED-SHA512 shadow hash in 'password' attribute to set the \
|
106
|
+
user password using shadow hash.")
|
107
|
+
end
|
98
108
|
|
99
|
-
def osx_salted_sha1?(string)
|
100
|
-
return !! ( string =~ /^[[:xdigit:]]{48}$/ )
|
101
109
|
end
|
102
110
|
|
103
|
-
def
|
104
|
-
|
105
|
-
|
111
|
+
def load_current_resource
|
112
|
+
@current_resource = Chef::Resource::User.new(@new_resource.username)
|
113
|
+
@current_resource.username(@new_resource.username)
|
106
114
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
+
user_info = read_user_info
|
116
|
+
if user_info
|
117
|
+
@current_resource.uid(dscl_get(user_info, :uid))
|
118
|
+
@current_resource.gid(dscl_get(user_info, :gid))
|
119
|
+
@current_resource.home(dscl_get(user_info, :home))
|
120
|
+
@current_resource.shell(dscl_get(user_info, :shell))
|
121
|
+
@current_resource.comment(dscl_get(user_info, :comment))
|
122
|
+
@authentication_authority = dscl_get(user_info, :auth_authority)
|
115
123
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
shadow_hash =
|
123
|
-
|
124
|
-
if
|
125
|
-
|
124
|
+
if @new_resource.password && dscl_get(user_info, :password) == "********"
|
125
|
+
# A password is set. Let's get the password information from shadow file
|
126
|
+
shadow_hash_binary = dscl_get(user_info, :shadow_hash)
|
127
|
+
|
128
|
+
# Calling shell_out directly since we want to give an input stream
|
129
|
+
shadow_hash_xml = convert_binary_plist_to_xml(shadow_hash_binary.string)
|
130
|
+
shadow_hash = Plist::parse_xml(shadow_hash_xml)
|
131
|
+
|
132
|
+
if shadow_hash["SALTED-SHA512"]
|
133
|
+
# Convert the shadow value from Base64 encoding to hex before consuming them
|
134
|
+
@password_shadow_conversion_algorithm = "SALTED-SHA512"
|
135
|
+
@current_resource.password(shadow_hash["SALTED-SHA512"].string.unpack('H*').first)
|
136
|
+
elsif shadow_hash["SALTED-SHA512-PBKDF2"]
|
137
|
+
@password_shadow_conversion_algorithm = "SALTED-SHA512-PBKDF2"
|
138
|
+
# Convert the entropy from Base64 encoding to hex before consuming them
|
139
|
+
@current_resource.password(shadow_hash["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack('H*').first)
|
140
|
+
@current_resource.iterations(shadow_hash["SALTED-SHA512-PBKDF2"]["iterations"])
|
141
|
+
# Convert the salt from Base64 encoding to hex before consuming them
|
142
|
+
@current_resource.salt(shadow_hash["SALTED-SHA512-PBKDF2"]["salt"].string.unpack('H*').first)
|
126
143
|
else
|
127
|
-
|
128
|
-
OpenSSL::Random.random_bytes(10).each_byte { |b| hex_salt << b.to_i.to_s(16) }
|
129
|
-
hex_salt = hex_salt.slice(0...8)
|
130
|
-
salt = [hex_salt].pack("H*")
|
131
|
-
sha1 = ::OpenSSL::Digest::SHA1.hexdigest(salt+@new_resource.password)
|
132
|
-
salted_sha1 = (hex_salt+sha1).upcase
|
144
|
+
raise(Chef::Exceptions::User,"Unknown shadow_hash format: #{shadow_hash.keys.join(' ')}")
|
133
145
|
end
|
134
|
-
shadow_hash = String.new("00000000"*155)
|
135
|
-
shadow_hash[168] = salted_sha1
|
136
|
-
end
|
137
|
-
|
138
|
-
::File.open("/var/db/shadow/hash/#{guid}",'w',0600) do |output|
|
139
|
-
output.puts shadow_hash
|
140
|
-
end
|
141
|
-
|
142
|
-
unless shadow_hash_set?
|
143
|
-
safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';ShadowHash;'")
|
144
146
|
end
|
147
|
+
|
148
|
+
convert_group_name if @new_resource.gid
|
149
|
+
else
|
150
|
+
@user_exists = false
|
151
|
+
Chef::Log.debug("#{@new_resource} user does not exist")
|
145
152
|
end
|
146
|
-
end
|
147
153
|
|
148
|
-
|
149
|
-
super
|
150
|
-
raise Chef::Exceptions::User, "Could not find binary /usr/bin/dscl for #{@new_resource}" unless ::File.exists?("/usr/bin/dscl")
|
154
|
+
@current_resource
|
151
155
|
end
|
152
156
|
|
157
|
+
#
|
158
|
+
# Provider Actions
|
159
|
+
#
|
160
|
+
|
153
161
|
def create_user
|
154
162
|
dscl_create_user
|
155
163
|
dscl_create_comment
|
156
|
-
|
164
|
+
dscl_set_uid
|
157
165
|
dscl_set_gid
|
158
|
-
|
166
|
+
dscl_set_home
|
159
167
|
dscl_set_shell
|
160
|
-
|
168
|
+
set_password
|
161
169
|
end
|
162
|
-
|
170
|
+
|
163
171
|
def manage_user
|
164
172
|
dscl_create_user if diverged?(:username)
|
165
173
|
dscl_create_comment if diverged?(:comment)
|
166
|
-
|
174
|
+
dscl_set_uid if diverged?(:uid)
|
167
175
|
dscl_set_gid if diverged?(:gid)
|
168
|
-
|
176
|
+
dscl_set_home if diverged?(:home)
|
169
177
|
dscl_set_shell if diverged?(:shell)
|
170
|
-
|
178
|
+
set_password if diverged_password?
|
171
179
|
end
|
172
|
-
|
180
|
+
|
181
|
+
#
|
182
|
+
# Action Helpers
|
183
|
+
#
|
184
|
+
|
185
|
+
#
|
186
|
+
# Create a user using dscl
|
187
|
+
#
|
173
188
|
def dscl_create_user
|
174
|
-
|
189
|
+
run_dscl("create /Users/#{@new_resource.username}")
|
175
190
|
end
|
176
|
-
|
191
|
+
|
192
|
+
#
|
193
|
+
# Saves the specified Chef user `comment` into RealName attribute
|
194
|
+
# of Mac user.
|
195
|
+
#
|
177
196
|
def dscl_create_comment
|
178
|
-
|
179
|
-
end
|
180
|
-
|
181
|
-
def dscl_set_gid
|
182
|
-
safe_dscl("create /Users/#{@new_resource.username} PrimaryGroupID '#{@new_resource.gid}'")
|
197
|
+
run_dscl("create /Users/#{@new_resource.username} RealName '#{@new_resource.comment}'")
|
183
198
|
end
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
199
|
+
|
200
|
+
#
|
201
|
+
# Sets the user id for the user using dscl.
|
202
|
+
# If a `uid` is not specified, it finds the next available one starting
|
203
|
+
# from 200 if `system` is set, 500 otherwise.
|
204
|
+
#
|
205
|
+
def dscl_set_uid
|
206
|
+
@new_resource.uid(get_free_uid) if (@new_resource.uid.nil? || @new_resource.uid == '')
|
207
|
+
|
208
|
+
if uid_used?(@new_resource.uid)
|
209
|
+
raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{@new_resource.uid} is already in use")
|
190
210
|
end
|
211
|
+
|
212
|
+
run_dscl("create /Users/#{@new_resource.username} UniqueID #{@new_resource.uid}")
|
191
213
|
end
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
214
|
+
|
215
|
+
#
|
216
|
+
# Find the next available uid on the system. starting with 200 if `system` is set,
|
217
|
+
# 500 otherwise.
|
218
|
+
#
|
219
|
+
def get_free_uid(search_limit=1000)
|
220
|
+
uid = nil
|
221
|
+
base_uid = @new_resource.system ? 200 : 500
|
222
|
+
next_uid_guess = base_uid
|
223
|
+
users_uids = run_dscl("list /Users uid")
|
224
|
+
while(next_uid_guess < search_limit + base_uid)
|
225
|
+
if users_uids =~ Regexp.new("#{Regexp.escape(next_uid_guess.to_s)}\n")
|
226
|
+
next_uid_guess += 1
|
227
|
+
else
|
228
|
+
uid = next_uid_guess
|
229
|
+
break
|
201
230
|
end
|
202
231
|
end
|
203
|
-
|
204
|
-
groups = []
|
205
|
-
Etc.group do |group|
|
206
|
-
groups << group.name if group.mem.include?(@new_resource.username)
|
207
|
-
end
|
208
|
-
groups.each do |group_name|
|
209
|
-
safe_dscl("delete /Groups/#{group_name} GroupMembership '#{@new_resource.username}'")
|
210
|
-
end
|
211
|
-
# remove user account
|
212
|
-
safe_dscl("delete /Users/#{@new_resource.username}")
|
232
|
+
return uid || raise("uid not found. Exhausted. Searched #{search_limit} times")
|
213
233
|
end
|
214
234
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
end
|
223
|
-
|
224
|
-
def check_lock
|
225
|
-
return @locked = locked?
|
235
|
+
#
|
236
|
+
# Returns true if uid is in use by a different account, false otherwise.
|
237
|
+
#
|
238
|
+
def uid_used?(uid)
|
239
|
+
return false unless uid
|
240
|
+
users_uids = run_dscl("list /Users uid")
|
241
|
+
!! ( users_uids =~ Regexp.new("#{Regexp.escape(uid.to_s)}\n") )
|
226
242
|
end
|
227
243
|
|
228
|
-
|
229
|
-
|
244
|
+
#
|
245
|
+
# Sets the group id for the user using dscl. Fails if a group doesn't
|
246
|
+
# exist on the system with given group id.
|
247
|
+
#
|
248
|
+
def dscl_set_gid
|
249
|
+
unless @new_resource.gid && @new_resource.gid.to_s.match(/^\d+$/)
|
250
|
+
begin
|
251
|
+
possible_gid = run_dscl("read /Groups/#{@new_resource.gid} PrimaryGroupID").split(" ").last
|
252
|
+
rescue Chef::Exceptions::DsclCommandFailed => e
|
253
|
+
raise Chef::Exceptions::GroupIDNotFound.new("Group not found for #{@new_resource.gid} when creating user #{@new_resource.username}")
|
254
|
+
end
|
255
|
+
@new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/)
|
256
|
+
end
|
257
|
+
run_dscl("create /Users/#{@new_resource.username} PrimaryGroupID '#{@new_resource.gid}'")
|
230
258
|
end
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
259
|
+
|
260
|
+
#
|
261
|
+
# Sets the home directory for the user. If `:manage_home` is set home
|
262
|
+
# directory is managed (moved / created) for the user.
|
263
|
+
#
|
264
|
+
def dscl_set_home
|
265
|
+
if @new_resource.home.nil? || @new_resource.home.empty?
|
266
|
+
run_dscl("delete /Users/#{@new_resource.username} NFSHomeDirectory")
|
267
|
+
return
|
268
|
+
end
|
269
|
+
|
270
|
+
if @new_resource.supports[:manage_home]
|
271
|
+
validate_home_dir_specification!
|
272
|
+
|
273
|
+
if (@current_resource.home == @new_resource.home) && !new_home_exists?
|
274
|
+
ditto_home
|
275
|
+
elsif !current_home_exists? && !new_home_exists?
|
276
|
+
ditto_home
|
277
|
+
elsif current_home_exists?
|
278
|
+
move_home
|
279
|
+
end
|
280
|
+
end
|
281
|
+
run_dscl("create /Users/#{@new_resource.username} NFSHomeDirectory '#{@new_resource.home}'")
|
236
282
|
end
|
237
|
-
|
283
|
+
|
238
284
|
def validate_home_dir_specification!
|
239
285
|
unless @new_resource.home =~ /^\//
|
240
|
-
raise(Chef::Exceptions::InvalidHomeDirectory,"invalid path spec for User: '#{@new_resource.username}', home directory: '#{@new_resource.home}'")
|
286
|
+
raise(Chef::Exceptions::InvalidHomeDirectory,"invalid path spec for User: '#{@new_resource.username}', home directory: '#{@new_resource.home}'")
|
241
287
|
end
|
242
288
|
end
|
243
|
-
|
289
|
+
|
244
290
|
def current_home_exists?
|
245
291
|
::File.exist?("#{@current_resource.home}")
|
246
292
|
end
|
247
|
-
|
293
|
+
|
248
294
|
def new_home_exists?
|
249
|
-
::File.exist?("#{@new_resource.home}")
|
295
|
+
::File.exist?("#{@new_resource.home}")
|
250
296
|
end
|
251
|
-
|
297
|
+
|
252
298
|
def ditto_home
|
253
299
|
skel = "/System/Library/User Template/English.lproj"
|
254
300
|
raise(Chef::Exceptions::User,"can't find skel at: #{skel}") unless ::File.exists?(skel)
|
@@ -258,7 +304,7 @@ class Chef
|
|
258
304
|
|
259
305
|
def move_home
|
260
306
|
Chef::Log.debug("#{@new_resource} moving #{self} home from #{@current_resource.home} to #{@new_resource.home}")
|
261
|
-
|
307
|
+
|
262
308
|
src = @current_resource.home
|
263
309
|
FileUtils.mkdir_p(@new_resource.home)
|
264
310
|
files = ::Dir.glob("#{src}/*", ::File::FNM_DOTMATCH) - ["#{src}/.","#{src}/.."]
|
@@ -266,14 +312,365 @@ class Chef
|
|
266
312
|
::FileUtils.rmdir(src)
|
267
313
|
::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home)
|
268
314
|
end
|
269
|
-
|
315
|
+
|
316
|
+
#
|
317
|
+
# Sets the shell for the user using dscl.
|
318
|
+
#
|
319
|
+
def dscl_set_shell
|
320
|
+
if @new_resource.shell || ::File.exists?("#{@new_resource.shell}")
|
321
|
+
run_dscl("create /Users/#{@new_resource.username} UserShell '#{@new_resource.shell}'")
|
322
|
+
else
|
323
|
+
run_dscl("create /Users/#{@new_resource.username} UserShell '/usr/bin/false'")
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
#
|
328
|
+
# Sets the password for the user based on given password parameters.
|
329
|
+
# Chef supports specifying plain-text passwords and password shadow
|
330
|
+
# hash data.
|
331
|
+
#
|
332
|
+
def set_password
|
333
|
+
# Return if there is no password to set
|
334
|
+
return if @new_resource.password.nil?
|
335
|
+
|
336
|
+
shadow_info = prepare_password_shadow_info
|
337
|
+
|
338
|
+
# Shadow info is saved as binary plist. Convert the info to binary plist.
|
339
|
+
shadow_info_binary = StringIO.new
|
340
|
+
command = Mixlib::ShellOut.new("plutil -convert binary1 -o - -",
|
341
|
+
:input => shadow_info.to_plist, :live_stream => shadow_info_binary)
|
342
|
+
command.run_command
|
343
|
+
|
344
|
+
# Replace the shadow info in user's plist
|
345
|
+
user_info = read_user_info
|
346
|
+
dscl_set(user_info, :shadow_hash, shadow_info_binary)
|
347
|
+
|
348
|
+
#
|
349
|
+
# Before saving the user's plist file we need to wait for dscl to
|
350
|
+
# update its caches and flush them to disk. In order to achieve this
|
351
|
+
# we need to wait first for our changes to get into the dscl cache
|
352
|
+
# and then flush the cache to disk before saving password into the
|
353
|
+
# plist file. 3 seconds is the minimum experimental value for dscl
|
354
|
+
# cache to be updated. We can get rid of this sleep when we find a
|
355
|
+
# trigger to update dscl cache.
|
356
|
+
#
|
357
|
+
sleep 3
|
358
|
+
shell_out("dscacheutil '-flushcache'")
|
359
|
+
save_user_info(user_info)
|
360
|
+
end
|
361
|
+
|
362
|
+
#
|
363
|
+
# Prepares the password shadow info based on the platform version.
|
364
|
+
#
|
365
|
+
def prepare_password_shadow_info
|
366
|
+
shadow_info = { }
|
367
|
+
entropy = nil
|
368
|
+
salt = nil
|
369
|
+
iterations = nil
|
370
|
+
|
371
|
+
if mac_osx_version_10_7?
|
372
|
+
hash_value = if salted_sha512?(@new_resource.password)
|
373
|
+
@new_resource.password
|
374
|
+
else
|
375
|
+
# Create a random 4 byte salt
|
376
|
+
salt = OpenSSL::Random.random_bytes(4)
|
377
|
+
encoded_password = OpenSSL::Digest::SHA512.hexdigest(salt + @new_resource.password)
|
378
|
+
hash_value = salt.unpack('H*').first + encoded_password
|
379
|
+
end
|
380
|
+
|
381
|
+
shadow_info["SALTED-SHA512"] = StringIO.new
|
382
|
+
shadow_info["SALTED-SHA512"].string = convert_to_binary(hash_value)
|
383
|
+
shadow_info
|
384
|
+
else
|
385
|
+
if salted_sha512_pbkdf2?(@new_resource.password)
|
386
|
+
entropy = convert_to_binary(@new_resource.password)
|
387
|
+
salt = convert_to_binary(@new_resource.salt)
|
388
|
+
iterations = @new_resource.iterations
|
389
|
+
else
|
390
|
+
salt = OpenSSL::Random.random_bytes(32)
|
391
|
+
iterations = @new_resource.iterations # Use the default if not specified by the user
|
392
|
+
|
393
|
+
entropy = OpenSSL::PKCS5::pbkdf2_hmac(
|
394
|
+
@new_resource.password,
|
395
|
+
salt,
|
396
|
+
iterations,
|
397
|
+
128,
|
398
|
+
OpenSSL::Digest::SHA512.new
|
399
|
+
)
|
400
|
+
end
|
401
|
+
|
402
|
+
pbkdf_info = { }
|
403
|
+
pbkdf_info["entropy"] = StringIO.new
|
404
|
+
pbkdf_info["entropy"].string = entropy
|
405
|
+
pbkdf_info["salt"] = StringIO.new
|
406
|
+
pbkdf_info["salt"].string = salt
|
407
|
+
pbkdf_info["iterations"] = iterations
|
408
|
+
|
409
|
+
shadow_info["SALTED-SHA512-PBKDF2"] = pbkdf_info
|
410
|
+
end
|
411
|
+
|
412
|
+
shadow_info
|
413
|
+
end
|
414
|
+
|
415
|
+
#
|
416
|
+
# Removes the user from the system after removing user from his groups
|
417
|
+
# and deleting home directory if needed.
|
418
|
+
#
|
419
|
+
def remove_user
|
420
|
+
if @new_resource.supports[:manage_home]
|
421
|
+
# Remove home directory
|
422
|
+
FileUtils.rm_rf(@current_resource.home)
|
423
|
+
end
|
424
|
+
|
425
|
+
# Remove the user from its groups
|
426
|
+
run_dscl("list /Groups").each_line do |group|
|
427
|
+
if member_of_group?(group.chomp)
|
428
|
+
run_dscl("delete /Groups/#{group.chomp} GroupMembership '#{@new_resource.username}'")
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
# Remove user account
|
433
|
+
run_dscl("delete /Users/#{@new_resource.username}")
|
434
|
+
end
|
435
|
+
|
436
|
+
#
|
437
|
+
# Locks the user.
|
438
|
+
#
|
439
|
+
def lock_user
|
440
|
+
run_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';DisabledUser;'")
|
441
|
+
end
|
442
|
+
|
443
|
+
#
|
444
|
+
# Unlocks the user
|
445
|
+
#
|
446
|
+
def unlock_user
|
447
|
+
auth_string = @authentication_authority.gsub(/AuthenticationAuthority: /,"").gsub(/;DisabledUser;/,"").strip
|
448
|
+
run_dscl("create /Users/#{@new_resource.username} AuthenticationAuthority '#{auth_string}'")
|
449
|
+
end
|
450
|
+
|
451
|
+
#
|
452
|
+
# Returns true if the user is locked, false otherwise.
|
453
|
+
#
|
454
|
+
def locked?
|
455
|
+
if @authentication_authority
|
456
|
+
!!(@authentication_authority =~ /DisabledUser/ )
|
457
|
+
else
|
458
|
+
false
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
#
|
463
|
+
# This is the interface base User provider requires to provide idempotency.
|
464
|
+
#
|
465
|
+
def check_lock
|
466
|
+
return @locked = locked?
|
467
|
+
end
|
468
|
+
|
469
|
+
#
|
470
|
+
# Helper functions
|
471
|
+
#
|
472
|
+
|
473
|
+
#
|
474
|
+
# Returns true if the system state and desired state is different for
|
475
|
+
# given attribute.
|
476
|
+
#
|
270
477
|
def diverged?(parameter)
|
271
478
|
parameter_updated?(parameter) && (not @new_resource.send(parameter).nil?)
|
272
479
|
end
|
273
|
-
|
480
|
+
|
274
481
|
def parameter_updated?(parameter)
|
275
482
|
not (@new_resource.send(parameter) == @current_resource.send(parameter))
|
276
483
|
end
|
484
|
+
|
485
|
+
#
|
486
|
+
# We need a special check function for password since we support both
|
487
|
+
# plain text and shadow hash data.
|
488
|
+
#
|
489
|
+
# Checks if password needs update based on platform version and the
|
490
|
+
# type of the password specified.
|
491
|
+
#
|
492
|
+
def diverged_password?
|
493
|
+
return false if @new_resource.password.nil?
|
494
|
+
|
495
|
+
# Dscl provider supports both plain text passwords and shadow hashes.
|
496
|
+
if mac_osx_version_10_7?
|
497
|
+
if salted_sha512?(@new_resource.password)
|
498
|
+
diverged?(:password)
|
499
|
+
else
|
500
|
+
!salted_sha512_password_match?
|
501
|
+
end
|
502
|
+
else
|
503
|
+
# When a system is upgraded to a version 10.7+ shadow hashes of the users
|
504
|
+
# will be updated when the user logs in. So it's possible that we will have
|
505
|
+
# SALTED-SHA512 password in the current_resource. In that case we will force
|
506
|
+
# password to be updated.
|
507
|
+
return true if salted_sha512?(@current_resource.password)
|
508
|
+
|
509
|
+
if salted_sha512_pbkdf2?(@new_resource.password)
|
510
|
+
diverged?(:password) || diverged?(:salt) || diverged?(:iterations)
|
511
|
+
else
|
512
|
+
!salted_sha512_pbkdf2_password_match?
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
#
|
518
|
+
# Returns true if user is member of the specified group, false otherwise.
|
519
|
+
#
|
520
|
+
def member_of_group?(group_name)
|
521
|
+
membership_info = ""
|
522
|
+
begin
|
523
|
+
membership_info = run_dscl("read /Groups/#{group_name}")
|
524
|
+
rescue Chef::Exceptions::DsclCommandFailed
|
525
|
+
# Raised if the group doesn't contain any members
|
526
|
+
end
|
527
|
+
# Output is something like:
|
528
|
+
# GroupMembership: root admin etc
|
529
|
+
members = membership_info.split(" ")
|
530
|
+
members.shift # Get rid of GroupMembership: string
|
531
|
+
members.include?(@new_resource.username)
|
532
|
+
end
|
533
|
+
|
534
|
+
#
|
535
|
+
# DSCL Helper functions
|
536
|
+
#
|
537
|
+
|
538
|
+
# A simple map of Chef's terms to DSCL's terms.
|
539
|
+
DSCL_PROPERTY_MAP = {
|
540
|
+
:uid => "generateduid",
|
541
|
+
:gid => "gid",
|
542
|
+
:home => "home",
|
543
|
+
:shell => "shell",
|
544
|
+
:comment => "realname",
|
545
|
+
:password => "passwd",
|
546
|
+
:auth_authority => "authentication_authority",
|
547
|
+
:shadow_hash => "ShadowHashData"
|
548
|
+
}.freeze
|
549
|
+
|
550
|
+
# Directory where the user plist files are stored for versions 10.7 and above
|
551
|
+
USER_PLIST_DIRECTORY = "/var/db/dslocal/nodes/Default/users".freeze
|
552
|
+
|
553
|
+
#
|
554
|
+
# Reads the user plist and returns a hash keyed with DSCL properties specified
|
555
|
+
# in DSCL_PROPERTY_MAP. Return nil if the user is not found.
|
556
|
+
#
|
557
|
+
def read_user_info
|
558
|
+
user_info = nil
|
559
|
+
|
560
|
+
begin
|
561
|
+
user_plist_file = "#{USER_PLIST_DIRECTORY}/#{@new_resource.username}.plist"
|
562
|
+
user_plist_info = run_plutil("convert xml1 -o - #{user_plist_file}")
|
563
|
+
user_info = Plist::parse_xml(user_plist_info)
|
564
|
+
rescue Chef::Exceptions::PlistUtilCommandFailed
|
565
|
+
end
|
566
|
+
|
567
|
+
user_info
|
568
|
+
end
|
569
|
+
|
570
|
+
#
|
571
|
+
# Saves the given hash keyed with DSCL properties specified
|
572
|
+
# in DSCL_PROPERTY_MAP to the disk.
|
573
|
+
#
|
574
|
+
def save_user_info(user_info)
|
575
|
+
user_plist_file = "#{USER_PLIST_DIRECTORY}/#{@new_resource.username}.plist"
|
576
|
+
Plist::Emit.save_plist(user_info, user_plist_file)
|
577
|
+
run_plutil("convert binary1 #{user_plist_file}")
|
578
|
+
end
|
579
|
+
|
580
|
+
#
|
581
|
+
# Sets a value in user information hash using Chef attributes as keys.
|
582
|
+
#
|
583
|
+
def dscl_set(user_hash, key, value)
|
584
|
+
raise "Unknown dscl key #{key}" unless DSCL_PROPERTY_MAP.keys.include?(key)
|
585
|
+
user_hash[DSCL_PROPERTY_MAP[key]] = [ value ]
|
586
|
+
user_hash
|
587
|
+
end
|
588
|
+
|
589
|
+
#
|
590
|
+
# Gets a value from user information hash using Chef attributes as keys.
|
591
|
+
#
|
592
|
+
def dscl_get(user_hash, key)
|
593
|
+
raise "Unknown dscl key #{key}" unless DSCL_PROPERTY_MAP.keys.include?(key)
|
594
|
+
# DSCL values are set as arrays
|
595
|
+
value = user_hash[DSCL_PROPERTY_MAP[key]]
|
596
|
+
value.nil? ? value : value.first
|
597
|
+
end
|
598
|
+
|
599
|
+
#
|
600
|
+
# System Helpets
|
601
|
+
#
|
602
|
+
|
603
|
+
def mac_osx_version
|
604
|
+
# This provider will only be invoked on node[:platform] == "mac_os_x"
|
605
|
+
# We do not check or assert that here.
|
606
|
+
node[:platform_version]
|
607
|
+
end
|
608
|
+
|
609
|
+
def mac_osx_version_10_7?
|
610
|
+
mac_osx_version.start_with?("10.7.")
|
611
|
+
end
|
612
|
+
|
613
|
+
def mac_osx_version_less_than_10_7?
|
614
|
+
versions = mac_osx_version.split(".")
|
615
|
+
# Make integer comparison in order not to report 10.10 less than 10.7
|
616
|
+
(versions[0].to_i <= 10 && versions[1].to_i < 7)
|
617
|
+
end
|
618
|
+
|
619
|
+
def mac_osx_version_greater_than_10_7?
|
620
|
+
versions = mac_osx_version.split(".")
|
621
|
+
# Make integer comparison in order not to report 10.10 less than 10.7
|
622
|
+
(versions[0].to_i >= 10 && versions[1].to_i > 7)
|
623
|
+
end
|
624
|
+
|
625
|
+
def run_dscl(*args)
|
626
|
+
result = shell_out("dscl . -#{args.join(' ')}")
|
627
|
+
return "" if ( args.first =~ /^delete/ ) && ( result.exitstatus != 0 )
|
628
|
+
raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") unless result.exitstatus == 0
|
629
|
+
raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") if result.stdout =~ /No such key: /
|
630
|
+
result.stdout
|
631
|
+
end
|
632
|
+
|
633
|
+
def run_plutil(*args)
|
634
|
+
result = shell_out("plutil -#{args.join(' ')}")
|
635
|
+
raise(Chef::Exceptions::PlistUtilCommandFailed,"plutil error: #{result.inspect}") unless result.exitstatus == 0
|
636
|
+
result.stdout
|
637
|
+
end
|
638
|
+
|
639
|
+
def convert_binary_plist_to_xml(binary_plist_string)
|
640
|
+
Mixlib::ShellOut.new("plutil -convert xml1 -o - -", :input => binary_plist_string).run_command.stdout
|
641
|
+
end
|
642
|
+
|
643
|
+
def convert_to_binary(string)
|
644
|
+
string.unpack('a2'*(string.size/2)).collect { |i| i.hex.chr }.join
|
645
|
+
end
|
646
|
+
|
647
|
+
def salted_sha512?(string)
|
648
|
+
!!(string =~ /^[[:xdigit:]]{136}$/)
|
649
|
+
end
|
650
|
+
|
651
|
+
def salted_sha512_password_match?
|
652
|
+
# Salt is included in the first 4 bytes of shadow data
|
653
|
+
salt = @current_resource.password.slice(0,8)
|
654
|
+
shadow = OpenSSL::Digest::SHA512.hexdigest(convert_to_binary(salt) + @new_resource.password)
|
655
|
+
@current_resource.password == salt + shadow
|
656
|
+
end
|
657
|
+
|
658
|
+
def salted_sha512_pbkdf2?(string)
|
659
|
+
!!(string =~ /^[[:xdigit:]]{256}$/)
|
660
|
+
end
|
661
|
+
|
662
|
+
def salted_sha512_pbkdf2_password_match?
|
663
|
+
salt = convert_to_binary(@current_resource.salt)
|
664
|
+
|
665
|
+
OpenSSL::PKCS5::pbkdf2_hmac(
|
666
|
+
@new_resource.password,
|
667
|
+
salt,
|
668
|
+
@current_resource.iterations,
|
669
|
+
128,
|
670
|
+
OpenSSL::Digest::SHA512.new
|
671
|
+
).unpack('H*').first == @current_resource.password
|
672
|
+
end
|
673
|
+
|
277
674
|
end
|
278
675
|
end
|
279
676
|
end
|