Skip to content

Instantly share code, notes, and snippets.

@pudquick
Last active July 14, 2020 13:33
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save pudquick/6c38ed97a8178ec91c4049b0e20dd69c to your computer and use it in GitHub Desktop.
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