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.
Files changed (81) hide show
  1. data/distro/common/html/chef-client.8.html +4 -4
  2. data/distro/common/html/chef-expander.8.html +4 -4
  3. data/distro/common/html/chef-expanderctl.8.html +4 -4
  4. data/distro/common/html/chef-server-webui.8.html +4 -4
  5. data/distro/common/html/chef-server.8.html +4 -4
  6. data/distro/common/html/chef-solo.8.html +4 -4
  7. data/distro/common/html/chef-solr.8.html +5 -5
  8. data/distro/common/html/knife-bootstrap.1.html +4 -4
  9. data/distro/common/html/knife-client.1.html +4 -4
  10. data/distro/common/html/knife-configure.1.html +4 -4
  11. data/distro/common/html/knife-cookbook-site.1.html +4 -4
  12. data/distro/common/html/knife-cookbook.1.html +4 -4
  13. data/distro/common/html/knife-data-bag.1.html +4 -4
  14. data/distro/common/html/knife-environment.1.html +4 -4
  15. data/distro/common/html/knife-exec.1.html +4 -4
  16. data/distro/common/html/knife-index.1.html +4 -4
  17. data/distro/common/html/knife-node.1.html +4 -4
  18. data/distro/common/html/knife-role.1.html +4 -4
  19. data/distro/common/html/knife-search.1.html +4 -4
  20. data/distro/common/html/knife-ssh.1.html +4 -4
  21. data/distro/common/html/knife-status.1.html +4 -4
  22. data/distro/common/html/knife-tag.1.html +4 -4
  23. data/distro/common/html/knife.1.html +4 -4
  24. data/distro/common/html/shef.1.html +4 -4
  25. data/distro/common/man/man1/knife-bootstrap.1 +1 -1
  26. data/distro/common/man/man1/knife-client.1 +1 -1
  27. data/distro/common/man/man1/knife-configure.1 +1 -1
  28. data/distro/common/man/man1/knife-cookbook-site.1 +1 -1
  29. data/distro/common/man/man1/knife-cookbook.1 +1 -1
  30. data/distro/common/man/man1/knife-data-bag.1 +1 -1
  31. data/distro/common/man/man1/knife-environment.1 +1 -1
  32. data/distro/common/man/man1/knife-exec.1 +1 -1
  33. data/distro/common/man/man1/knife-index.1 +1 -1
  34. data/distro/common/man/man1/knife-node.1 +1 -1
  35. data/distro/common/man/man1/knife-role.1 +1 -1
  36. data/distro/common/man/man1/knife-search.1 +1 -1
  37. data/distro/common/man/man1/knife-ssh.1 +1 -1
  38. data/distro/common/man/man1/knife-status.1 +1 -1
  39. data/distro/common/man/man1/knife-tag.1 +1 -1
  40. data/distro/common/man/man1/knife.1 +1 -1
  41. data/distro/common/man/man1/shef.1 +1 -1
  42. data/distro/common/man/man8/chef-client.8 +1 -1
  43. data/distro/common/man/man8/chef-expander.8 +1 -1
  44. data/distro/common/man/man8/chef-expanderctl.8 +1 -1
  45. data/distro/common/man/man8/chef-server-webui.8 +1 -1
  46. data/distro/common/man/man8/chef-server.8 +1 -1
  47. data/distro/common/man/man8/chef-solo.8 +1 -1
  48. data/distro/common/man/man8/chef-solr.8 +1 -1
  49. data/lib/chef/client.rb +1 -2
  50. data/lib/chef/exceptions.rb +1 -0
  51. data/lib/chef/node.rb +1 -1
  52. data/lib/chef/provider/group/dscl.rb +27 -9
  53. data/lib/chef/provider/package/rpm.rb +2 -2
  54. data/lib/chef/provider/service/macosx.rb +5 -1
  55. data/lib/chef/provider/user/dscl.rb +569 -172
  56. data/lib/chef/resource/mount.rb +11 -10
  57. data/lib/chef/resource/user.rb +18 -0
  58. data/lib/chef/rest/rest_request.rb +1 -0
  59. data/lib/chef/shef.rb +7 -0
  60. data/lib/chef/shef/shef_session.rb +4 -4
  61. data/lib/chef/version.rb +1 -1
  62. data/spec/data/mac_users/10.7-8.plist.xml +559 -0
  63. data/spec/data/mac_users/10.7-8.shadow.xml +11 -0
  64. data/spec/data/mac_users/10.7.plist.xml +559 -0
  65. data/spec/data/mac_users/10.7.shadow.xml +11 -0
  66. data/spec/data/mac_users/10.8.plist.xml +559 -0
  67. data/spec/data/mac_users/10.8.shadow.xml +21 -0
  68. data/spec/data/mac_users/10.9.plist.xml +560 -0
  69. data/spec/data/mac_users/10.9.shadow.xml +21 -0
  70. data/spec/functional/resource/user/dscl_spec.rb +199 -0
  71. data/spec/spec_helper.rb +23 -0
  72. data/spec/unit/client_spec.rb +23 -4
  73. data/spec/unit/provider/group/dscl_spec.rb +35 -0
  74. data/spec/unit/provider/package/rpm_spec.rb +12 -0
  75. data/spec/unit/provider/service/macosx_spec.rb +37 -0
  76. data/spec/unit/provider/user/dscl_spec.rb +705 -284
  77. data/spec/unit/resource/mount_spec.rb +11 -0
  78. data/spec/unit/rest/auth_credentials_spec.rb +5 -0
  79. data/spec/unit/shef/shef_session_spec.rb +44 -2
  80. metadata +223 -116
  81. 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 /([\w\d_.-]+)\s([\w\d_.-]+)/
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 /([\w\d_.-]+)\s([\w\d_.-]+)/
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(:all_actions) do |a|
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 safe_dscl(*args)
37
- result = dscl(*args)
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
- # This is handled in providers/group.rb by Etc.getgrnam()
45
- # def user_exists?(user)
46
- # users = safe_dscl("list /Users")
47
- # !! ( users =~ Regexp.new("\n#{user}\n") )
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
- # get a free UID greater than 200
51
- def get_free_uid(search_limit=1000)
52
- uid = nil; next_uid_guess = 200
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
- def uid_used?(uid)
66
- return false unless uid
67
- users_uids = safe_dscl("list /Users uid")
68
- !! ( users_uids =~ Regexp.new("#{Regexp.escape(uid.to_s)}\n") )
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
- def set_uid
72
- @new_resource.uid(get_free_uid) if (@new_resource.uid.nil? || @new_resource.uid == '')
73
- if uid_used?(@new_resource.uid)
74
- raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{@new_resource.uid} is already in use")
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
- def modify_home
80
- return safe_dscl("delete /Users/#{@new_resource.username} NFSHomeDirectory") if (@new_resource.home.nil? || @new_resource.home.empty?)
81
- if @new_resource.supports[:manage_home]
82
- validate_home_dir_specification!
83
-
84
- if (@current_resource.home == @new_resource.home) && !new_home_exists?
85
- ditto_home
86
- elsif !current_home_exists? && !new_home_exists?
87
- ditto_home
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
- def osx_shadow_hash?(string)
96
- return !! ( string =~ /^[[:xdigit:]]{1240}$/ )
97
- end
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 guid
104
- safe_dscl("read /Users/#{@new_resource.username} GeneratedUID").gsub(/GeneratedUID: /,"").strip
105
- end
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
- def shadow_hash_set?
108
- user_data = safe_dscl("read /Users/#{@new_resource.username}")
109
- if user_data =~ /AuthenticationAuthority: / && user_data =~ /ShadowHash/
110
- true
111
- else
112
- false
113
- end
114
- end
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
- def modify_password
117
- if @new_resource.password
118
- shadow_hash = nil
119
-
120
- Chef::Log.debug("#{new_resource} updating password")
121
- if osx_shadow_hash?(@new_resource.password)
122
- shadow_hash = @new_resource.password.upcase
123
- else
124
- if osx_salted_sha1?(@new_resource.password)
125
- salted_sha1 = @new_resource.password.upcase
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
- hex_salt = ""
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
- def load_current_resource
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
- set_uid
164
+ dscl_set_uid
157
165
  dscl_set_gid
158
- modify_home
166
+ dscl_set_home
159
167
  dscl_set_shell
160
- modify_password
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
- set_uid if diverged?(:uid)
174
+ dscl_set_uid if diverged?(:uid)
167
175
  dscl_set_gid if diverged?(:gid)
168
- modify_home if diverged?(:home)
176
+ dscl_set_home if diverged?(:home)
169
177
  dscl_set_shell if diverged?(:shell)
170
- modify_password if diverged?(:password)
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
- safe_dscl("create /Users/#{@new_resource.username}")
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
- safe_dscl("create /Users/#{@new_resource.username} RealName '#{@new_resource.comment}'")
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
- def dscl_set_shell
186
- if @new_resource.password || ::File.exists?("#{@new_resource.shell}")
187
- safe_dscl("create /Users/#{@new_resource.username} UserShell '#{@new_resource.shell}'")
188
- else
189
- safe_dscl("create /Users/#{@new_resource.username} UserShell '/usr/bin/false'")
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
- def remove_user
194
- if @new_resource.supports[:manage_home]
195
- user_info = safe_dscl("read /Users/#{@new_resource.username}")
196
- if nfs_home_match = user_info.match(NFS_HOME_DIRECTORY)
197
- #nfs_home = safe_dscl("read /Users/#{@new_resource.username} NFSHomeDirectory")
198
- #nfs_home.gsub!(/NFSHomeDirectory: /,"").gsub!(/\n$/,"")
199
- nfs_home = nfs_home_match[1]
200
- FileUtils.rm_rf(nfs_home)
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
- # remove the user from its groups
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
- def locked?
216
- user_info = safe_dscl("read /Users/#{@new_resource.username}")
217
- if auth_authority_md = AUTHENTICATION_AUTHORITY.match(user_info)
218
- !!(auth_authority_md[1] =~ /DisabledUser/ )
219
- else
220
- false
221
- end
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
- def lock_user
229
- safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';DisabledUser;'")
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
- def unlock_user
233
- auth_info = safe_dscl("read /Users/#{@new_resource.username} AuthenticationAuthority")
234
- auth_string = auth_info.gsub(/AuthenticationAuthority: /,"").gsub(/;DisabledUser;/,"").strip#.gsub!(/[; ]*$/,"")
235
- safe_dscl("create /Users/#{@new_resource.username} AuthenticationAuthority '#{auth_string}'")
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