Forked from pudquick/chef_user_resource_monkeypatching.rb
Created
December 12, 2018 20:53
-
-
Save erikng/094aec4a995aed7aa52863d4cfce56e5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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