Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
require 'base64'
require 'plist'
module Chef::Provider::User::DsclMojaveUserExtensions
# new for 10.14+
def mac_osx_version_greater_than_10_13?
Gem::Version.new(node['platform_version']) > Gem::Version.new('10.13.99')
end
# updated for 10.14+
def load_current_resource
super
# fixes bug where chef compared hash to plaintext password
# only applies to salted_sha512_pbkdf2, which is in 10.8+
if mac_osx_version_greater_than_10_7?
if !new_resource.password.nil? && !current_resource.password.nil?
# only run if we have passwords to compare
if !salted_sha512_pbkdf2?(new_resource.password)
# if we're not using a hex hash but instead a real password
if salted_sha512_pbkdf2_password_match?
# if the hash matches the password, make the resource.password match
current_resource.password(new_resource.password)
end
end
end
end
current_resource
end
# Brought into the extension namespace
DSCL_PROPERTY_MAP = Chef::Provider::User::Dscl::DSCL_PROPERTY_MAP
# new for 10.14+
# mapping for raw DS attribute names, which dscl outputs
DSCL_RAW_PROPERTY_MAP = {
uid: 'dsAttrTypeStandard:UniqueID',
gid: 'dsAttrTypeStandard:PrimaryGroupID',
home: 'dsAttrTypeStandard:NFSHomeDirectory',
shell: 'dsAttrTypeStandard:UserShell',
comment: 'dsAttrTypeStandard:RealName',
password: 'dsAttrTypeStandard:Password',
auth_authority: 'dsAttrTypeStandard:AuthenticationAuthority',
shadow_hash: 'dsAttrTypeNative:ShadowHashData',
}.freeze
# new for 10.14+
# array of data type attributes that dscl improperly outputs as strings
# that we need to repair
DSCL_DATA_KEYS = [
'dsAttrTypeNative:ShadowHashData',
].freeze
# new for 10.14+
# runner for dsimport now that we can't write to user plists directly
def run_dsimport(*args)
result = shell_out('/usr/bin/dsimport', *(args.compact))
raise(Chef::Exceptions::DsimportCommandFailed,
"dsimport error: #{result.inspect}") unless result.exitstatus == 0
result.stdout
end
# new for 10.14+
# runner for dscl in plist mode
def run_dscl_plist(*args)
result = shell_out('/usr/bin/dscl', '-plist', '.',
"-#{args[0]}", *((args[1..-1]).compact))
return '' if ( result.exitstatus != 0 )
# Unlike run_dscl, we don't want to raise an error here
result.stdout
end
# new for 10.14+
# the output of dscl -plist isn't identical to reading the user
# .plist XML file directly, this repairs the portions we care about
def reformat_user_info(user_hash)
return if user_hash.nil?
user_info = {}
user_hash.each do |k,v|
if DSCL_DATA_KEYS.include?(k)
# this key is usually a data key, fix the value if we detect it to be
if v.first.match('^(\h+ ?)+$')
v = [StringIO.new([v.first.delete(' ')].pack('H*'))]
end
end
if DSCL_RAW_PROPERTY_MAP.has_value?(k)
# remap keys to match what they were in the XML .plist
k = DSCL_PROPERTY_MAP[DSCL_RAW_PROPERTY_MAP.key(k)]
end
user_info[k] = v
end
user_info
end
# patched for 10.14+
def create_user
# if we're not on 10.14+, return prior behavior
unless mac_osx_version_greater_than_10_13?
return super
end
dscl_create_user
# set_password modifies the plist file of the user directly. So update
# the password first before making any modifications to the user.
set_password
dscl_create_comment
# dscl_set_uid - it is illegal to change the uid after the user is created
dscl_set_gid
dscl_set_home
dscl_set_shell
end
# patched for 10.14+
def dscl_create_user
# if we're not on 10.14+, return prior behavior
unless mac_osx_version_greater_than_10_13?
return super
end
# We now need to figure out and specify the uid at creation time
new_resource.uid(get_free_uid) if new_resource.uid.nil? || new_resource.uid == ""
run_dscl("create", "/Users/#{new_resource.username}", "UniqueID", new_resource.uid)
end
# patched for 10.14+
def read_user_info
# if we're not on 10.14+, return prior behavior
unless mac_osx_version_greater_than_10_13?
return super
end
# We flush the cache here in order to make sure that we read
# fresh information for the user.
shell_out('/usr/bin/dscacheutil', '-flushcache')
user_info = nil
begin
user_plist = run_dscl_plist('read', "/Users/#{new_resource.username}")
user_record = Plist.parse_xml(user_plist)
user_info = reformat_user_info(user_record)
rescue Chef::Exceptions::PlistUtilCommandFailed
end
user_info
end
# patched for 10.14+
def set_password
# if we're not on 10.14+, return prior behavior
unless mac_osx_version_greater_than_10_13?
return super
end
# Return if there is no password to set
return if new_resource.password.nil?
shadow_info = prepare_password_shadow_info
# Shadow is saved as binary plist. Convert the info to binary plist.
shadow_info_binary = StringIO.new
shell_out('/usr/bin/plutil', '-convert', 'binary1', '-o', '-', '-',
input: shadow_info.to_plist,
live_stream: shadow_info_binary)
if user_info.nil?
# User is just created. read_user_info() will read the fresh
# information for the user with a cache flush. However with
# experimentation we've seen that dscl cache is not immediately
# updated after the creation of the user.
# This is odd and needs to be investigated further.
sleep 3
@user_info = read_user_info
end
# Replace the shadow info in user's plist
dscl_set(user_info, :shadow_hash, shadow_info_binary)
# 10.14 removed the ability to write to user plists directly
# instead, we need to use dsimport to merge the value into the record
begin
t_name = "#{Chef::Config['file_cache_path']}/shash.tmp"
b64_shadow = ::Base64.strict_encode64(shadow_info_binary.string)
# the dsimport record format is:
# record definition delimiter (space in hex)
# escape delimiter (backslash in hex)
# record value delimiter (colon in hex)
# record array value delimimter (comma in hex)
# OpenDirectory record type
# number of attributes per record
# [delimited list of record attribute names]
# we are defining a minimal record: record name + shadowhashdata
t_user = 'dsRecTypeStandard:Users'
r_name = 'dsAttrTypeStandard:RecordName'
r_shad = 'base64:dsAttrTypeNative:ShadowHashData'
t_dsimport = <<~HEREDOC
0x0A 0x5C 0x3A 0x2C #{t_user} 2 #{r_name} #{r_shad}
#{new_resource.username}:#{b64_shadow}
HEREDOC
# unfortunately dsimport only works with real files using mmap
# so we ensure that the file does not exist already by using EXCL
# to fail on open (like a lock file) to make sure we have full
# control and ensure 0600 permissions during its usage
exclusive_mode = ::File::WRONLY|::File::CREAT|::File::EXCL
::File.delete(t_name) if ::File.exist?(t_name)
::File.open(t_name, exclusive_mode, 0600) do |f|
f.write t_dsimport
end
result = run_dscl('delete',
"/Users/#{new_resource.username}",
'ShadowHashData')
result = run_dsimport(t_name, '/Local/Default', 'M')
::File.delete(t_name) if ::File.exist?(t_name)
result = run_dscl('create',
"/Users/#{new_resource.username}",
'Password', '********')
rescue => e
# if there's an error, delete the temp file
::File.delete(t_name) if ::File.exist?(t_name)
log_fatal(
:exception => e,
:message => '[User::Dscl::set_password] Exception with hash: ' +
new_resource.username,
)
end
end
end
module Chef::Provider::Group::DsclMojaveGroupExtensions
# new for 10.14+
def mac_osx_version_greater_than_10_13?
Gem::Version.new(node['platform_version']) > Gem::Version.new('10.13.99')
end
# new for 10.14+
# runner for dseditgroup manipulations
def run_dseditgroup(*args)
# Ensure that our information is accurate
shell_out('/usr/bin/dscacheutil', '-flushcache')
result = shell_out('/usr/sbin/dseditgroup', '-o', 'edit', '-n',
'/Local/Default', '-t', 'user', *(args.compact))
raise(Chef::Exceptions::DseditgroupCommandFailed,
"dseditgroup error: #{result.inspect}") unless result.exitstatus == 0
result.stdout
end
# patched for 10.14+
def set_members
unless mac_osx_version_greater_than_10_13?
return super
end
# First reset the memberships if the append is not set
unless new_resource.append
logger.trace("#{new_resource} removing group members #{current_resource.members.join(' ')}") unless current_resource.members.empty?
safe_dscl("create", "/Groups/#{new_resource.group_name}", "GroupMembers", "") # clear guid list
safe_dscl("create", "/Groups/#{new_resource.group_name}", "GroupMembership", "") # clear user list
current_resource.members([ ])
end
# Add any members that need to be added
if new_resource.members && !new_resource.members.empty?
members_to_be_added = [ ]
new_resource.members.each do |member|
members_to_be_added << member unless current_resource.members.include?(member)
end
unless members_to_be_added.empty?
logger.trace("#{new_resource} setting group members #{members_to_be_added.join(', ')}")
# safe_dscl("append", "/Groups/#{new_resource.group_name}", "GroupMembership", *members_to_be_added)
members_to_be_added.each do |username|
run_dseditgroup('-a', username, new_resource.group_name)
end
end
end
# Remove any members that need to be removed
if new_resource.excluded_members && !new_resource.excluded_members.empty?
members_to_be_removed = [ ]
new_resource.excluded_members.each do |member|
members_to_be_removed << member if current_resource.members.include?(member)
end
unless members_to_be_removed.empty?
logger.trace("#{new_resource} removing group members #{members_to_be_removed.join(', ')}")
# safe_dscl("delete", "/Groups/#{new_resource.group_name}", "GroupMembership", *members_to_be_removed)
members_to_be_removed.each do |username|
run_dseditgroup('-d', username, new_resource.group_name)
end
end
end
end
end
class Chef
class Provider
class User
class Dscl
prepend Chef::Provider::User::DsclMojaveUserExtensions
prepend Chef::Provider::Group::DsclMojaveGroupExtensions
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.