chef 11.14.2-x86-mingw32 → 11.14.6-x86-mingw32

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