#!/usr/bin/perl ################################################################################ # LoginHook # Joseph Jackson, 26-Apr-2002 # # This login hook creates a local home directory for the user logging in # and establishes default preferences. # # The local home directory is created with appropriate ownership and mode # bits. Then ~/Library is created as a symlink to the user's AFS file space, # ensuring that preferences, keychains, and other settings follow the user # from machine to machine. For local users (admin and root) or guest accounts # where AFS space is limited, ~/Library is created on the local disk instead. # As an optimization, ~/Library/Caches is kept on the local hard drive by # linking it back to ~/.local/Caches. # # This script is executed as root. All the modifications made to the local # hard drive need to be done as root. Changes to AFS require the tokens of the # user logging in, so we permanently switch to that uid before beginning the # AFS work. # # revision history # 6 aug 2004, bpf: added call to cameradefault script, uncommented foldersync for future use # 28 jul 2004, bpf: updated MS Word fix to account for 2004 update # 23 jul 2004, bpf: separated printer default & presets code into own script, default printer changes # 1 jul 2004, bpf: make call to fetchprefs munging script # 22 jun 2004, bpf: added MSWord fix, allowed overwrite argument on copy_* # 4 jun 2004, bpf: added configuration of printer presets # 16 dec 2003, jf6b: New MacVector prefs, clean up /Library/Caches as well # 17 nov 2003, jf6b: modded launch of afsquota # 14 oct 2003, jf6b: better update foo for waves and friends # 9 oct 2003, jf6b: default printer changes # 8 oct 2003, jf6b: re-do change away from mortis for file cleaning # 15 sep 2003, jf6b: use cp instead of copy_file to make them happen always # 12 sep 2003, jf6b: MIDI stuff wants ALL the byhost files # 2 sep 2003, jf6b: Waves/Peak fixes # 27 aug 2003, jf6b: Different Safari mods, fixed ByHost stuff # 25 aug 2003, jf6b: Safari mods # 23 aug 2003, jf6b: MIDI prefs handling # 20 aug 2003, jf6b: added Waves prefs # 19 aug 2003, jf6b: added RealPlayer prefs # 19 aug 2003, jf6b: ripped out ns7.1 changes so we can get this to gamma # 14 aug 2003, jf6b: merged rbraun/jf6b 12 aug changes, symlinked IE cache to local disk # 12 aug 2003, jf6b: fixed printing support for real this time # 12 aug 2003, rbraun: create Explorer/Home.xml for IE, remove prefpanes cache # 5 aug 2003, rbraun: new prefs munging for netscape 7 # 21 jul 2003, jf6b: remove Fetch pref file calls, better printer support # 16 jul 2003, jf6b: added a missing acrobat munge routine # 22 may 2003, rbraun: added new dock and prefs munging routines, new IE prefs handling # 20 may 2003, jf6b: re-added afsquotad, printer resetting # 23 apr 2003, jf6b: stripped for 10.2 # 20 feb 2003, jf6b: copy fetch shortcuts as well # 12 jan 2003, jf6b: MacVector lib support, fixed mortis support & admin dir perms # 18 dec 2002, jf6b: ~/Library/Application Support/Temporary Files symlinked locally for large Netscape downloads # 29 oct 2002, jf6b: clean up SparkLE prefs # 17 oct 2002, jf6b: clean up root-owned home dir too # 11-Oct-2002, jackson: complete rewrite with much improved error checking and reporting # 8 oct 2002, jackson: move log to /var/log, cleanup after root-owned ~/Library # 6 oct 2002, jf6b: polished mortis foo # 5 sep 2002, jf6b: fixed KeyAccess loginhook chaining bug # 3 sep 2002, jf6b: added vuescanrc handling for scanners # 1 aug 2002, rbraun: add dock fixup stuff for netscape, and quota check # 25 jul 2002, rbraun: added netscape prefs integrity check # 17 jul 2002, rbraun: copy in prefs for netscape and create cache dir # ? jul 2002, jackson: enable duplex printing # 26 jun 2002, rbraun: IE prefs, and make sure ~/Library/Preferences exists # 20 jun 2002, rbraun: be more aggressive about fixing ~/Library link # 18 jun 2002, rbraun: support local guest accts, copy default prefs if they # don't exist, make sure ~/Library/Caches is local # 10 jun 2002, rbraun: add wrapper to run user's loginhook # 30 may 2002, jackson: call KeyAccess hook, check afs symlink # ################################################################################ # See below for definitions of these utility subroutines sub log_abort; sub log_warning; sub log_init; # Mode bit settings for a shared directory, private directory, and world-writable file $mode_shr = 0755; $mode_pvt = 0700; $mode_wrt = 0666; # Where to write errors, should any occur $logfile = "/var/log/LoginHook.log"; ################ # Create logfile # log_init(); ################# # Check Arguments # log_abort "Script invoked with bad number of arguments" if ($#ARGV != 0); $user = $ARGV[0]; log_abort "Script invoked with a blank user-id argument" if ($user eq ''); ################## # Lookup User Info # ($name,$passwd,$uid,$gid, $quota,$comment,$gcos, $homedir,$shell,$expire) = getpwnam($user) or log_abort "User '$user' not in passwd database"; log_abort "Home directory setting is blank" if ($homedir eq ''); if ($user =~ m/^(guest[0-9][0-9]+|admin|root)$/) { $isguest = 1; } else { $isguest = 0; } if (!$isguest) { $afshomedir = find_afshomedir($user); if ($afshomedir eq '') { log_warning "Couldn't determine AFS home directory"; $isguest = 1; } elsif ($afshomedir !~ m'^/afs/') { log_warning "AFS home directory setting is not valid: $afshomedir"; $isguest = 1; } } ### ### Uncomment the following section to enable temporary user files to be cleared ### out when the machine's disks begin to fill. ### ############### # Clean up disk # # Clear some disk space with find if there's not enough free, where # "enough" means "half the usable space, or at least 5 gigs available". # We approximate "usable space" by assuming the system image is 4GB. # # Catch the stderr of "df" so we can log it if it fails #$df_output = `df -k / 2>&1 | tail -1`; #if ($? >> 8 != 0) { # log_warning "Failed to run 'df' command: $! $df_output"; #} else { # my($doit) = 0; # # ($disk,$diskblocks,$diskused,$diskavail) = split(/ +/, $df_output); # if ($diskblocks !~ /[0-9]+/ or $diskused !~ /[0-9]+/ or $diskavail !~ /[0-9]+/) { # log_warning "Failed to parse output of 'df' command: $df_output"; # } else { # $doit = 1 # if ($diskavail < 5000000); # $doit = 1 # if ($diskblocks > 6000000 && $diskavail / ($diskblocks - 6000000) < #0.5); # if ($doit) { # log_warning "Cleaning up disk ($disk, $diskblocks, $diskused, $diskavail)"; # &run_command ("find /Users \! \\( -user $user -o -user root \\) -delete"); # &run_command ("find /private/tmp \! \\( -user $user -o -user root \\) -delete"); # &run_command ("find /private/var/tmp \! \\( -user $user -o -user root \\) -delete"); # &run_command ("find /Library/Caches \! \\( -user $user -o -user root \\) -delete"); # } # } #} ############################# # Create Homedir and Subdirs # # Remove existing homedir if ownership is wrong or it's not a directory # (undef, undef, undef, undef, $hd_uid, $hd_gid) = stat($homedir); system "rm -rf $homedir" if ($hd_uid != $uid || $hd_gid != $gid || (-e _ && ! -d _ )); if ($user == "admin") { &make_dir ($uid, $gid, $mode_pvt, "$homedir"); } else { &make_dir ($uid, $gid, $mode_shr, "$homedir"); } (-l "/Users/CurrentUser") and unlink "/Users/CurrentUser" or log_warning "Failed to unlink /Users/CurrentUser: $!"; &make_link("$homedir", "/Users/CurrentUser"); &make_link ("$afshomedir", "$homedir/MyAFS") if (!$isguest); &make_dir ($uid, $gid, $mode_pvt, "$homedir/.local"); &make_dir ($uid, $gid, $mode_pvt, "$homedir/.local/Caches"); &make_dir ($uid, $gid, $mode_pvt, "$homedir/.local/Temporary Files"); &make_dir ($uid, $gid, $mode_pvt, "$homedir/Documents"); ###################### # Remove local Library # # For non-guests, get ready to create Library as a link into AFS by removing # any local Library directory # It might be owned by root, so do it before switching UID/GID # if (!$isguest) { # Don't use run_command to avoid logging EISDIR errors system "rm -f $homedir/Library"; system "rm -rf $homedir/Library"; } ### ### If you are performing any tasks that must be run as root, such as printer ### setup or copying files to protected portions of the disk, they should be ### done here before we switch away from running as root. ### ################################################################################ # SWITCH FROM ROOT TO THE UID AND PRIMARY GID OF USER LOGGING IN # # This grants us access to the user's AFS space, # but limits what we can do to the local file system # $) = $gid; $> = $uid; ################# # Check AFS Quota # # If the user is over quota or we can't check the quota, # treat it like a guest account (don't link ~/Library into AFS, etc.) # if (!$isguest) { $isguest = &overquota($afshomedir); } ################ # Create Library # # For guests, create Library and Library/Prefs as local directories # For others, point Library into AFS home directory # if (!$isguest) { &make_afsdir ("$afshomedir/Library", "system:anyuser none"); &make_afsdir ("$afshomedir/Library/Preferences", "system:anyuser none"); &make_link ("$afshomedir/Library", "$homedir/Library"); # Don't use run_command to avoid logging EISDIR errors system "rm -f $afshomedir/Library/Caches"; system "rm -rf $afshomedir/Library/Caches"; &make_link ("$homedir/.local/Caches", "$afshomedir/Library/Caches"); &make_afsdir ("$afshomedir/Library/Application Support", "system:anyuser none"); if (! -l "$afshomedir/Library/Application Support/Temporary Files") { unlink "$afshomedir/Library/Application Support/Temporary Files"; &make_link ("$homedir/.local/Temporary Files", "$afshomedir/Library/Application Support/Temporary Files"); } } else { # For users over quota, we need to remove any Library that's a symlink into AFS unlink "$homedir/Library"; # ignore errors &make_dir ($uid, $gid, $mode_pvt, "$homedir/Library"); &make_dir ($uid, $gid, $mode_pvt, "$homedir/Library/Preferences"); } ############### # Install Prefs # # IE fixup ©_file ("/Library/Preferences/com.apple.internetconfig.plist", "$homedir/Library/Preferences/com.apple.internetconfig.plist", 0); &ic_fix_once("/Library/Preferences/com.apple.internetconfig.fixup.plist", "ie524"); # IE is just looking for reasons to reset the homepage... mkdir "$homedir/Library/Preferences/Explorer" if (! -e "$homedir/Library/Preferences/Explorer"); system("touch $homedir/Library/Preferences/Explorer/Home.xml") if (! -e "$homedir/Library/Preferences/Explorer/Home.xml"); system "rm -f $homedir/Library/Preferences/Explorer/Download\\ Cache"; system("touch $homedir/.local/Caches/Download\\ Cache"); &make_link ("$homedir/.local/Caches/Download Cache", "$homedir/Library/Preferences/Explorer/Download Cache"); #©_file("/Library/Preferences/com.RealNetworks.RealOne Player.plist", # "$homedir/Library/Preferences/com.RealNetworks.RealOne Player.plist", 0); ##$hwaddr=`ifconfig en0 | grep ether | cut -d\\ -f2 | sed 's/://g'`; ##©_file("/Library/Preferences/ByHost/com.apple.MIDI.$hwaddr.plist", ## "$homedir/Library/Preferences/ByHost/com.apple.MIDI.$hwaddr.plist", 0); #foreach $file ("com.apple.MIDI.00039304b5a0.plist", # "com.apple.MIDI.00039304ba9e.plist", # "com.apple.MIDI.000393137f3a.plist", # "com.apple.MIDI.000393137fe2.plist") { # if ( -e "/Library/Preferences/ByHost/$file" ) { # system("cp /Library/Preferences/ByHost/$file $homedir/Library/Preferences/ByHost/$file"); # } #} # Safari setup #mkdir "$homedir/Library/Safari" # if ( ! -e "$homedir/Library/Safari"); #©_file("/Library/Preferences/Bookmarks.plist", # "$homedir/Library/Safari/Bookmarks.plist", 0) # if ( ! -e "$homedir/Library/Safari/Bookmarks.plist"); #©_file("/Library/Preferences/com.apple.Safari.plist", # "$homedir/Library/Preferences/com.apple.Safari.plist", 0) # if ( ! -e "$homedir/Library/Preferences/com.apple.Safari.plist"); # MS Word (2004) fix: always overwrite user's OLE Reg DB with local version to prevent corruption #mkdir "$homedir/Library/Preferences/Microsoft" # if ( ! -e "$homedir/Library/Preferences/Microsoft"); #©_file("/Library/Preferences/Microsoft/OLE Registration Database 11", # "$homedir/Library/Preferences/Microsoft/OLE Registration Database 11", 1); # Fetch prefs / shortcuts munging #&run_command("/Library/Hooks/fetchprefs -u $user -d $homedir") # if ( -x "/Library/Hooks/fetchprefs" ); # Default camera hotplug helper #&run_command("/Library/Hooks/cameradefault") # if ( -x "/Library/Hooks/cameradefault" ); ################### # Modify Dock Prefs # &dock_add("/Library/Preferences/mungedock/add-preview", "add-preview_8_04"); &dock_add("/Library/Preferences/mungedock/add-safari", "add-safari_8_04"); &dock_add("/Library/Preferences/mungedock/add-terminal", "add-terminal_8_04"); &run_command("/System/Library/CoreServices/UserAccountUpdater 0 10.4 0 8H63"); #################### # Nuke SparkLE prefs # #unlink "$homedir/Library/Preferences/SPARKle Preferences", # "$homedir/Library/Preferences/Spark LE", # "$homedir/Library/Preferences/Spark LE DialogPrefs", # "$homedir/Library/Preferences/TCPluginRegistry", # "$homedir/Library/Preferences/de.TCWorks.SparkLE.plist"; ################### # Configure printing presets # # Ensure visibility of our 3 duplex presets without clobbering user's presets. # Set long-edge duplex as default. # #&run_command("/Library/Hooks/printpresets") # if ( -x "/Library/Hooks/printpresets" ); ##################### # Set default printer # #&run_command("/Library/Hooks/printdefault") # if ( -x "/Library/Hooks/printdefault" ); ############# # Run as User # # Run a few things without any chance of root access # # Since Perl doesn't realize that you can't drop the real user ID # out of root without having the effective ID in root, and doesn't # give you any way to do that, we need a C wrapper. # Note that the wrapper ignores exec errors, so this works even if # there's no personal loginhook file. # The afsquotd wasn't starting properly from run_command(), # so we forego error checking and use system() directly. # # AFS Quota monitoring app if ((!$isguest) || (-l "$homedir/MyAFS")) { system "/Library/Hooks/hookwrap /usr/local/bin/afsquotd"; } # Locked keychain warning app system "/Library/Hooks/hookwrap /usr/local/bin/kczapd"; # User-written additions # (Runs as themesleves, not root, so it's not a security problem) if (!$isguest) { system "/Library/Hooks/hookwrap $afshomedir/.MacOSXLoginHook"; } exit 0; ################################################################################ # SUBROUTINES ################################################################################ ########## # log_init # # Create the logfile and allow anyone to write to it (mode bits rw-rw-rw-) # We open it up now so logging will work after changing to the UID of the incoming user # sub log_init { if (! open LOG, ">>$logfile") { warn "Unable to create logfile '$logfile': $!"; } else { close LOG; } chmod $mode_wrt, $logfile or warn "Unable to set logfile mode bits: chmod $mode_wrt $logfile: $!"; } ############# # log_warning # # Write a failure message to the log file # Each line is stamped with the time and the user-id trying to log in # sub log_warning { $now_string = localtime; if (! open LOG, ">>$logfile") { warn "Unable to open logfile '$logfile': $!"; return; } print LOG $now_string, " ", $user, ": ", @_, "\n"; close LOG; } ########### # log_abort # # Abort the script after writing a failure message # Note: Exiting with a non-zero status causes LoginWindow under 10.1 to hang # so we always exit cleanly. There is no way for this script to abort the login. # sub log_abort { log_warning "LoginHook aborting:"; log_warning @_; exit 0; } ########## # make_dir # # Ensure that a directory exists and try to set the ownership and mode bits # This routine tests to see if the directory already exists, # so you don't need to use the "-d" test in the main script. # # If the directory can't be created, aborts the script # If the ownership or mode bits can't be set, report the error but continue # sub make_dir { my($uid, $gid, $mode, $dir) = @_; if (-d $dir) { # The directory already exists, so continue below } elsif (-e $dir) { # Something has that name, but it's not a directory. Abort. log_abort "make_dir failure: a file of the name '$dir' already exists"; } else { mkdir $dir or log_abort "make_dir failure: mkdir '$dir' failed: $!"; } chown $uid, $gid, $dir or log_warning "make_dir warning: chown $uid $gid $dir failed: $!"; chmod $mode, $dir or log_warning "make_dir warning: chmod $mode $dir failed: $!"; } ############# # make_afsdir # # Ensure that a directory exists and set the ACL if we created it # Only set ACL if directory doesn't exist so we don't override the user's ACL customizations # # If the directory can't be created, aborts the script # If the ACL can't be set, report the error but continue # sub make_afsdir { my($dir, $acl) = @_; my($output); if (-d $dir) { # The directory already exists, so continue below } elsif (-e $dir) { # Something has that name, but it's not a directory. Abort. log_abort "make_afsdir failure: a file of the name '$dir' already exists"; } else { mkdir $dir or log_abort "make_afsdir failure: mkdir '$dir' failed: $!"; $output = `fs setacl -dir '$dir' -acl $acl 2>&1`; log_warning "make_afsdir warning: fs setacl failed: $! $output" if ($? >> 8 != 0); } } ########### # make_link # # Create a symlink, if necessary # If the link already exists, it doesn't verify it points to the right thing # On error, aborts the script # sub make_link { my($orig, $link) = @_; if (! -l $link) { symlink ($orig, $link) or log_abort "make_link failure: symlink $orig $link failed: $!"; } } ################# # find_afshomedir # # Find the official AFS home directory for the user # All users have an entry in the "usr" directory, but that's just a symlink # to one of the "usr0" through "usr25" directories: # /afs/andrew.cmu.edu/usr/jackson -> ../usr25/jackson # sub find_afshomedir { my($user) = @_; my($result) = "/afs/andrew.cmu.edu/usr/$user"; if ( !defined($_ = readlink $result) ) { log_abort "Can't read AFS home directory symlink for user $user: $!"; } if ( ! s;../usr;/afs/andrew.cmu.edu/usr; ) { log_abort "Can't set AFS home directory for user $user"; } else { $result = $_; } return $result; } ########### # overquota # # If the given directory is over quota or we can't check the quota, # return true. If under quota, return false. # Any errors are logged but the script continues to run. # sub overquota { my($afsdir) = @_; # Return this value if an error prevents us from looking at the quota info my($isover) = 1; # Run the command, saving both stdout and stderr in $quota my($quota) = `fs quota $afsdir 2>&1`; if (($? >> 8) != 0) { # If exec couldn't run the command, the error is in $! # If the command ran but exited with an error status, stderr is in $quota log_warning "Failed to run 'fs quota $afsdir': $! $quota"; } else { # Chop off everything after the percent symbol # Sample output: "95% of quota used." $quota =~ s/%.*//; if ($quota !~ /[0-9]+/) { log_warning "Failed to parse output of 'fs quota': $quota"; } else { $isover = ($quota > 99); } } return $isover; } ########### # copy_file # # Copy a file if the original exists. Overwrite an existing destination only if # specified. If the copy is attempted and fails, return false (otherwise true). # sub copy_file { my($orig, $dest, $overwrite) = @_; return ©($orig, $dest, $overwrite, 0, 0); } ########### # copy_tree # # Copy a directory tree if the original exists. Overwrite an existing destination # only if specified. If the copy is attempted and fails, return false (otherwise true). # sub copy_tree { my($orig, $dest) = @_; return ©($orig, $dest, $overwrite, 1, 0); } ########### # copy_rsrc # # Copy a file w/ resource fork if the original exists. Overwrite an existing destination # only if specified. If the copy is attempted and fails, return false (otherwise true). # sub copy_rsrc { my($orig, $dest) = @_; return ©($orig, $dest, $overwrite, 0, 1); } ################ # copy_tree_rsrc # # Copy a directory tree w/ resource forks if the original exists. Overwrite an existing # destination only if specified. If the copy is attempted and fails, return false # (otherwise true). # sub copy_tree_rsrc { my($orig, $dest) = @_; return ©($orig, $dest, $overwrite, 1, 1); } ########### # copy # # Implement the copy operation for all the copy_* functions # Only copy if the original exists. Use the overwrite argument to determine whether to copy # if the destination already exists. # If the copy is attempted and fails, return false otherwise return true # sub copy { my($orig, $dest, $overwrite, $treemode, $rsrcmode) = @_; # Pick the right tool for the job my($command) = $rsrcmode ? "/Developer/Tools/CpMac" : "cp"; # The tools need different flags for copying a tree my($treeflag) = $rsrcmode ? "-r" : "-R"; return 1 if (! -e $orig or (-e $dest and ! $overwrite)); if ($treemode) { return &run_command ("$command $treeflag '$orig' '$dest'"); } else { return &run_command ("$command '$orig' '$dest'"); } } ############# # run_command # # Similar to the "system" function but reports any errors encountered # sub run_command { my($command) = @_; # run the command but redirect stderr so we also collect it in $output my($output) = `$command 2>&1`; if ($? >> 8 != 0) { # If exec couldn't run the command, the error is in $! # If the command ran but exited with an error status, stderr is in $output log_warning "Failed to run '$command': $! $output"; return 0; } else { return 1; } } ############################################# # Subroutines for handling dock modifications sub dock_add { my ($spec, $id) = @_; if (system("grep -sx ".$id." $homedir/Library/Preferences/edu.cmu.andrew.dockupdate.txt")) { &run_command ("/Library/Hooks/mungedock -a " . "$homedir/Library/Preferences/com.apple.dock.plist " . $spec) || return 0; if (open (DOCKLOG, '>>', "$homedir/Library/Preferences/edu.cmu.andrew.dockupdate.txt")) { print DOCKLOG "$id\n"; close(DOCKLOG); return 1; } else { log_warning "Failed to open edu.cmu.edu.andrew.dockupdate.txt for update $id: $!"; return 0; } } else { return 1; } } sub dock_fix_once { my ($spec, $id) = @_; if (system("grep -sx ".$id." $homedir/Library/Preferences/edu.cmu.andrew.dockupdate.txt")) { &run_command ("/Library/Hooks/mungedock -m " . "$homedir/Library/Preferences/com.apple.dock.plist " . $spec) || return 0; if (open (DOCKLOG, '>>', "$homedir/Library/Preferences/edu.cmu.andrew.dockupdate.txt")) { print DOCKLOG "$id\n"; close(DOCKLOG); return 1; } else { log_warning "Failed to open edu.cmu.edu.andrew.dockupdate.txt for update $id: $!"; return 0; } } else { return 1; } } sub dock_fix_always { my ($spec) = @_; return &run_command ("/Library/Hooks/mungedock -m " . "$homedir/Library/Preferences/com.apple.dock.plist " . $spec); } ##################################### # Subroutines for munging preferences sub ic_fix_once { my ($spec, $id) = @_; if (system("grep -sx ".$id." $homedir/Library/Preferences/edu.cmu.andrew.prefupdate.txt")) { &run_command ("/Library/Hooks/icmunge " . "$homedir/Library/Preferences/com.apple.internetconfig.plist " . $spec) || return 0; if (open (DOCKLOG, '>>', "$homedir/Library/Preferences/edu.cmu.andrew.prefupdate.txt")) { print DOCKLOG "$id\n"; close(DOCKLOG); return 1; } else { log_warning "Failed to open edu.cmu.edu.andrew.prefupdate.txt for update $id: $!"; return 0; } } else { return 1; } } sub ic_fix_always { my ($spec) = @_; return &run_command ("/Library/Hooks/icmunge " . "$homedir/Library/Preferences/com.apple.internetconfig.plist " . $spec); }