chef 10.32.2-x86-mingw32 → 10.34.0-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
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