#!/bin/sh

###
### File:  makeuser
###
### Copyright (c) 2001-2004 Apple Computer, Inc.  All Rights Reserved.
###
### Authors:	Soren Spies (SS)
### 			Brian Latimer (BDL)
### 
### History:
###	1.0		2001/09/21	SS	Initial implementation
###	2.0		2002/10/10	BDL	Overhaul for 10.2 compatibility/modified functionality
###	2.1		2003/04/18	BDL	Minor updates to help text, password handling
###	2.2		2003/05/01	BDL	Update to naprivs switch verification; and "sanitized" for external release
###	2.3		2003/06/11	BDL	Need to define NetInfo "sharedDir" key, to allow AFP guest access
###	2.4		2003/08/28	BDL	Brute-force approach for defining NetInfo keys, due to locked media issues
###	2.5		2003/09/10	BDL	Change switches to be all lowercase (not mixed-case)
###	2.6		2003/09/13	BDL	Enforce execution as root
###	2.7		2003/09/17	BDL	Delimit 'basename' ref's, to allow for spaces in path
###	2.8		2003/09/19	BDL	Additional provisions for spaces in other entries ('eval' execution)
###	3.0		2003/11/09	BDL	Significant update for Mac OS X 10.3 Panther (GID provisions, etc.) 
###	3.1		2003/12/04	BDL	Merged Mac OS X 10.2 Jaguar functionality back in - checks version of
###							system on target drive for conditional processing
###	3.2		2003/12/09	BDL	Under Panther, consider short name matching an existing group name, and
###							fail if it matches (must then override group, or choose another short name)
###	3.3		2003/12/22	BDL	Touch .SetupRegComplete, as well - to avoid MiniBuddy launch
###	3.4		2004/01/28	BDL	Updated system version processing (use last-known type for newer OS's); 
###							reordering of root execution enforcement; cleanup of usage text
###	4.0		2004/02/06	BDL	Implemented shadow password processing, as an alternative to "-cryptpass" 
###	4.1		2004/02/13	BDL	Allow both -shadowpass and -cryptpass - use whichever is most appropriate;
###							Evolution build environment execution provisions
###	4.2		2004/02/24	BDL	If unrecognized switch, echo it to the user, to allow for easier debugging
###							Also, accept "-picture" == "-loginpic", for old Evolution compatibility
###	4.3		2004/03/17	BDL	Minor switch updates
###	4.4		2004/05/20	BDL	New processing for optional user-specific templates: If a user-specific template for the
###							specified short name exists, copy its content to newly-created user's home, as well
###
### Description:
###	Will create a user with the specified name & properties, copying appropriate
###	User Template information into a user home directory hierarchy, configuring 
###	custom data as defined, etc.
###	
###	Note that the newly-create user's home is created from a copy of the standard User Template data:
###		'/System/Library/User Template/English.lproj' and '.../Non_localized'
###	but if it is found, also user-specific template data from:
###		'/System/Library/User Template/Custom_user/{username}'
###
###	Can operate on local (active) NetInfo database; or, if alternate NIDB selection
###	(non-boot drive, etc.) is desired, use "-raw" and/or "-sysroot" parameters
###	
###	By default, will also disable Setup Assistant (i.e. we are performing this
###	action as an alternative to running MacBuddy).
###
###
###	To obtain a shadow password value (only valid for post-10.2 systems), perform the following:
###		1) Within the 'Accounts' prefpane, specify a password for a given user
###		2) Within Terminal:  nicl . -read users/{username} generateduid
###			Result:  "generateduid: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
###		3) Within Terminal:  cat /private/var/db/shadow/hash/{above uid value}
###
###	  The result (contents of that file) will be the 104-character shadowpass value needed.
###
###	To obtain a (less-secure) crypted password value, for the '-cryptpass' parameter:
###		1) Within Terminal:  openssl passwd
###		2) Enter the desired password
###		2) Verify the desired password
###
###	  The result will be the 13-character cryptpass value needed.
###


# If executing within Evolution environment, doScript destination volume is always the first argument
if [ "$DTDOSCRIPT" ]; then
	SYSROOT=$1
	shift
# Otherwise, assume local volume (can override with -sysroot parameter)
else
	SYSROOT="/"
fi

NIDB="private/var/db/netinfo/local.nidb"

FIRSTUID=${FIRSTUID:-501}		# we increment beyond existing users below

TEMPLATE_ENGLISH="System/Library/User Template/English.lproj"
TEMPLATE_NONLOC="System/Library/User Template/Non_localized"
TEMPLATE_USER="System/Library/User Template/Custom_user"

SHADOWDIR="private/var/db/shadow/"
NULLSHADOW="31D6CFE0D16AE931B73C59D7E0C089C0AAD3B435B51404EEAAD3B435B51404EEDA39A3EE5E6B4B0D3255BFEF95601890AFD80709"

SYSTEMVERSION="System/Library/CoreServices/SystemVersion"

APPLESETUPDONE="private/var/db/.AppleSetupDone"
SETUPREGCOMPLETE="Library/Receipts/.SetupRegComplete"

usage() {
	echo "makeuser - scripted user creation under Mac OS X 10.2 or later"
    echo "Usage:   `basename \"$0\"` -user <user> -realname <realname> [ -password <shadowpass> | -cryptpass <cryptpass> ] [<opts>]"
	echo "   e.g. makeuser -user teacher -realname 'School Teacher' -cryptpass 'LEbsPcSWgMCAE' -sysroot '/Volumes/Other HD/' -admin -deskpic '/Library/Desktop Pictures/Aqua Graphite.jpg' "
	echo ""
	echo "   <user> and <realname> are mandatory"
	echo ""
	echo "   <shadowpass> should be created manually, extracted, and specified as input"
	echo "      (see contents of /private/var/db/shadow/hash/{generateduid} for value)"
	echo "   <cryptpass> can be generated with 'openssl passwd'"
	echo "      (this is the only valid password option for 10.2.x systems)"
	echo "   * If neither option specified, user will be created with NULL password"
	echo ""
	echo "   <opts> are any of:"
	echo "     -admin        - give user administrative capabilities"
	echo "     -hint <str>   - set password hint to the given string"
	echo "     -loginpic <p> - set user's login picture to the given path"
	echo "     -deskpic <p>  - set user's desktop picture to the given path"
	echo ""
	echo "   and, not as widely used:"
	echo "     -naprivs <v>  - set Remote Desktop privs ('naprivs' key) to the given value"
	echo "     -preserveSA   - Setup Assistant should still run on first boot"
	echo "     -preserveRA   - Registration Asst. should still run upon first admin login"
	echo "     -pwchange <v> - user's password change field set to value"
	echo "     -pwexpire <v> - user's password expire field set to value"
	echo "     -group <grp>  - override default group with group name provided"
	echo "     -shell <sp>   - override default shell with shell at the provided path"
	echo "     -raw          - edit netinfo files directly (default when -sysroot != /)"
	echo "     -sysroot <r>  - modify the system rooted on the given volume (e.g. '/Volumes/Server HD/')"
	echo ""
}


die() {
	[ "$#" -gt 0 ] && echo "`basename \"$0\"`: ERROR -" "$@" >&2
	exit 1
}

# If no parameters, throw usage and quit
[ $# = 0 ] && usage >&2 && exit 1

unset user realname shadowpass cryptpass
unset authority passwdval customuid shadowval
unset ADMINUSER HINT LOGINPIC DESKTOPPIC
unset GROUP NAPRIVS LEAVEASD LEAVEREG
unset PWCHANGE PWEXPIRE SHELL
unset RAW
unset theVers sysVers

while [[ $1 = -* ]]; do
	case "$1" in
		-h | -help)		usage; exit 0;;
		-user) shift; user="$1"
			[ -z "$user" ] || [[ "$user" = -* ]] && usage >&2 && die "need user";;
		-realname) shift; realname="$1"
			[ -z "$realname" ] || [[ "$realname" = -* ]] && usage >&2 && die "need realname";;
		-password | -shadowpass) shift; shadowpass="$1"
			[ -z "$shadowpass" ] || [[ "$shadowpass" = -* ]] && usage >&2 && die "need shadowpass value (for no passwd, don't specify switch)";;
		-cryptpass) shift; cryptpass="$1"
			[ -z "$cryptpass" ] || [[ "$cryptpass" = -* ]] && usage >&2 && die "need cryptpass value (for no passwd, don't specify switch)";;

		-admin) 	ADMINUSER=1;;
		-hint) shift; HINT="$1"
			[ -z "$HINT" ] || [[ "$HINT" = -* ]] && usage >&2 && die "need password hint";;
		-loginpic | -picture) shift; LOGINPIC="$1"
			[ -z "$LOGINPIC" ] || [[ "$LOGINPIC" = -* ]] && usage >&2 && die "need path to login picture";;
		-deskpic) shift; DESKTOPPIC="$1"
			[ -z "$DESKTOPPIC" ] || [[ "$DESKTOPPIC" = -* ]] && usage >&2 && die "need path to desktop pic";;

		-group) shift; GROUP="$1"
			[ -z "$GROUP" ] || [[ "$GROUP" = -* ]] && usage >&2 && die "need alternate groupname";;
		-naprivs) shift; NAPRIVS="$1"
			[ -z "$NAPRIVS" ] && usage >&2 && die "need naprivs value";;

		-preserveSA) LEAVEASD=1;;
		-preserveMB) LEAVEASD=1;;	# in case "MacBuddy" is stuck in our head

		-preserveRA) LEAVEREG=1;;

		-pwchange) shift; PWCHANGE="$1"
			echo "WARNING: unknown exactly how system uses 'change' field" >&2
			[ -z "$PWCHANGE" ] || [[ "$PWCHANGE" = -* ]] && usage >&2 die "need password change value";;
		-pwexpire) shift; PWEXPIRE="$1"
			echo "WARNING: unknown exactly how system uses 'expire' field" >&2
			[ -z "$PWEXPIRE" ] || [[ "$PWEXPIRE" = -* ]] && usage >&2 die "need password expire value";;
		-shell) shift; SHELL="$1"
			[ -z "$SHELL" ] || [[ "$SHELL" = -* ]] && usage >&2 && die "need shell";;

		-raw) 	RAW=1;;
		-sysroot) shift; SYSROOT="$1"
			[ -z "$SYSROOT" ] || [[ "$SYSROOT" = -* ]] && usage >&2 && die "need sysroot";;

		*) usage >&2; die "'$1' switch not recognized";;
	esac
	shift
done


# Ensure we are running this script as root (to allow access to all NetInfo interactions)
if [ "`whoami`" != "root" ] ; then
  die "script must be run as root"
  exit
fi


# Check for presence of required parameters, then begin other verifications

if [ -z "$user" ] || [ -z "$realname" ]; then
	usage >&2
	die "<user> and <realname> are required"
fi

if echo "$user"|/usr/bin/perl -ne "die if /[^a-zA-Z0-9]/" > /dev/null; then
	echo "WARNING: user names with special characters are a Very Bad Idea" >&2
fi

## Sleep due to a "Restricted Access" bug if nicl is called to often, too quickly
## /bin/sleep 2

# Provisions for alternate nidb selection
[ -d "$SYSROOT" ] || die "$SYSROOT doesn't seem to exist"

if [ "$RAW" -o "$SYSROOT" != "/" ]; then
	if [ "$SYSROOT" = / ]; then
		echo "WARNING: -raw is dangerous to a normally-booted system" >&2
	else
		[[ "$SYSROOT" = */ ]] && SYSROOT=`echo "$SYSROOT"|sed 's!/$!!'`
	fi
	df "$SYSROOT"|/usr/bin/perl -ne "die if /$SYSROOT\Z/" >/dev/null || die "$SYSROOT not a mountpoint"
	niclcmd="eval nicl -raw \"$SYSROOT/$NIDB\""		# Odd provision for spaces in volume name
else
	niclcmd="eval nicl -raw /var/db/netinfo/local.nidb"			# 'eval' this, as well (to ensure common behavior below)
fi

echo "`basename \"$0\"`: niclcmd is '$niclcmd'" >&2


# Determine system version of target drive
theVers="`defaults read \"$SYSROOT/$SYSTEMVERSION\" ProductVersion `"
case $theVers in

   	10.0* | 10.1*)	die "Unsupported OS version: '$theVers'" ;;

	10.2*)	sysVers="Jaguar";
			if [ -z "$SHELL" ]; then
				SHELL="/bin/tcsh"
			fi;;

	10.3*)	sysVers="Panther";
			if [ -z "$SHELL" ]; then
				SHELL="/bin/bash"
			fi;;

   		*)	sysVers="Panther";
			echo "WARNING: Later OS version encountered; $sysVers-style user will be created" >&2 ;
			if [ -z "$SHELL" ]; then
				SHELL="/bin/bash"
			fi;;
esac

# Verify basic password validity (character length, format)
if [ "$shadowpass" ] && [ `echo -n $shadowpass | wc -m` != 104 ]; then
	die "Shadow password must be exactly 104 characters"

elif [ "$shadowpass" ] && [ `echo -n "$shadowpass" | /usr/bin/perl -ne "die if /[^A-F0-9]/"` > /dev/null ]; then
	die "Shadow password must be comprised of only uppercase, hexadecimal digits"

elif [ "$cryptpass" ] && [ `echo -n $cryptpass | wc -m` != 13 ]; then
	die "Crypted password must be exactly 13 characters"

fi

# Sanity check the password(s) provided against the system version of the target
#
# If Shadowpass and Cryptpass are both supplied, just take whichever is appropriate for the target system
if [ "$shadowpass" ] && [ "$cryptpass" ]; then
	if [ "$sysVers" == "Jaguar" ]; then
		shadowpass=""		# cryptpass will be used; blank out shadowpass
		echo "WARNING: Both shadow and crypt passwords supplied - Mac OS X 10.2.x can only use crypted passwords" >&2
	else
		cryptpass=""		# shadowpass will be used; blank out cryptpass
		echo "WARNING: Both shadow and crypt passwords supplied - using shadow password for enhanced security" >&2
	fi

# Shadow password valid ONLY for post-Jaguar implementations; throw error if attempted on Jaguar
elif [ "$sysVers" == "Jaguar" ] && [ "$shadowpass" ]; then
	usage >&2
	die "Shadow password not valid for Mac OS X 10.2.x systems; provide -cryptpass instead"


# For post-Jaguar systems, crypted passwords are allowed, but not recommended; provide warning if provided
elif [ "$sysVers" != "Jaguar" ] && [ "$cryptpass" ]; then
	echo "WARNING: Mac OS X versions after 10.2.x should use shadow passwords; these are more secure than crypted passwords" >&2

fi


# Basic input looks good; report what we intend to do
echo "Creating $sysVers-style user under $theVers ..."



# Find first available UID after $FIRSTUID

### NOTE:
###	Specification of a given uid is not allowed - use 'modifyUID' after user creation, if desired

uid=$FIRSTUID

while $niclcmd -read /users/uid=$uid > /dev/null 2>&1; do
	uid=$((uid+1))
done


# See if this username already exists
$niclcmd -read /users/"$user" > /dev/null 2>&1 && die "user '$user' already exists"


# If a different default group was specified, then determined GID for this group
if [ "$GROUP" ]; then
	gid=`$niclcmd -read /groups/"$GROUP" gid | awk '{print $2}'`

# Else, under Jaguar, set group to "staff"
elif [ "$sysVers" == "Jaguar" ]; then
	GROUP="staff"
	gid=`$niclcmd -read /groups/"$GROUP" gid | awk '{print $2}'`
	
# Else, under Panther, create a new group with the same name & ID as this user
elif [ "$sysVers" == "Panther" ]; then
	GROUP=$user

	# ... as long as this username does not match an existing GROUP name
	$niclcmd -read /groups/"$GROUP" > /dev/null 2>&1 && die "group '$GROUP' already exists; either override group, or choose another short name"

	gid=$uid

	# Now, actually create the group record within NetInfo
	$niclcmd -create /groups/"$GROUP"					|| die "creating group"
	$niclcmd -create /groups/"$GROUP" users ""			|| die "setting group users"
	$niclcmd -create /groups/"$GROUP" passwd "\"*\""	|| die "setting group passwd"
	$niclcmd -create /groups/"$GROUP" gid "\"$gid\"" 	|| die "setting group gid"

fi

# Ensure gid is set correctly, corresponding to group name
[ -z "$gid" ] && die "couldn't find GID for $GROUP"


# Everything else is good; actually create the user record within NetInfo
$niclcmd -create /users/"$user"				|| die "creating user"



### File system interactions
###

# Determine home directory, and create (if it doesn't already exist)
dir=/Users/"$user"
userdir="${SYSROOT}$dir"
if [ -d "$userdir" ]; then
	echo "WARNING: User home directory $userdir already exists" >&2
else
	mkdir "$userdir" || die "creating home directory '$userdir' "
fi

###  TO DO: Handle language preference (don't always blindly take English)
ditto -rsrc "$SYSROOT/$TEMPLATE_ENGLISH" "$userdir"
ditto -rsrc "$SYSROOT/$TEMPLATE_NONLOC" "$userdir"

# If user-specific template exists, then copy this content, as well
if [ -d "$SYSROOT/$TEMPLATE_USER/$user" ]; then
	ditto -rsrc "$SYSROOT/$TEMPLATE_USER/$user" "$userdir"
##	rm -rf "$SYSROOT/$TEMPLATE_USER/$user"
fi


# Set "default" key for desktop picture (no longer dependent on displayID)
if [ "$DESKTOPPIC" ]; then
	defaults write "$userdir/Library/Preferences/"com.apple.desktop \
"{ \
    Background = { \
        default = { \
            ImageFilePath = \"$DESKTOPPIC\"; \
            Placement = Crop; \
        }; \
    }; \
}"
fi

# Set owner and group on everything in home directory hierarchy
chown -R $uid "$userdir" 	|| die "setting owner of '$userdir' to $uid"
chgrp -R $gid "$userdir" 	|| die "setting group of '$userdir' to $gid"



# Setup password provisions, based on type of password entered
if [ "$cryptpass" ] || [ "$sysVers" == "Jaguar" ]; then			# Cryptpass is only valid option - and, no need for generateduid

	authority=";basic;"
	passwdval="$cryptpass"					# even if blank...

else

	authority=";ShadowHash;"
	passwdval="********"					# flag to system, to reference ...shadow/hash/{genuid} file

	# Create a custom UID for this user
	###  NOTE: Final 12 chars will be the ethernet addr of the CREATING machine; not target, as would be expected
	customuid=`uuidgen`							|| die "creating generated uid"

	# Create ...shadow/hash/ directory to hold uid file (if it doesn't already exist)
	if ! [ -d "$SYSROOT/$SHADOWDIR/hash/" ]; then
		mkdir -p "$SYSROOT/$SHADOWDIR/hash/"	|| die "creating ...shadow/hash/ directory"
	fi
	
	# If no password provided, set shadow password to value representing NULL entry
	if [ -z "$shadowpass" ]; then
		shadowval="$NULLSHADOW"
	else
		shadowval="$shadowpass"
	fi

	# Populate uid-based shadowhash file with the shadow password
	echo -n $shadowval > "$SYSROOT/$SHADOWDIR/hash/$customuid"	|| die "cannot write to $customuid file" 

	# Finally, ensure correct ownership and permissions on this entire shadowhash area
	chown -R root:wheel "$SYSROOT/$SHADOWDIR"	|| die "setting ownership of ...shadow/hash/ area"
	chmod -R go-rwx 	"$SYSROOT/$SHADOWDIR"	|| die "setting mode of ...shadow/hash/ area"		# Ensure non-readable by any other than root

fi


### Additional NetInfo interactions
###

# Define all standard user keys, roughly in the order they are defined by the standard API
$niclcmd -create /users/"$user" shell "\"$SHELL\""				|| die "setting shell"
$niclcmd -create /users/"$user" home "\"$dir\""					|| die "setting home"
$niclcmd -create /users/"$user" gid "\"$gid\"" 					|| die "setting gid"

$niclcmd -create /users/"$user" authentication_authority "\"$authority\""	|| die "setting authentication authority"
$niclcmd -create /users/"$user" passwd "\"$passwdval\""						|| die "setting passwd value"
if [ "$shadowval" ]; then				# cryptpass not specified; system capable of shadow passwords
	$niclcmd -create /users/"$user" generateduid "\"$customuid\""			|| die "setting generateduid"
fi

$niclcmd -create /users/"$user" realname "\"$realname\""		|| die "setting realname"
$niclcmd -create /users/"$user" hint "\"$HINT\"" 				|| die "setting hint"
$niclcmd -create /users/"$user" sharedDir Public				|| die "setting sharedDir"
$niclcmd -create /users/"$user" uid "\"$uid\"" 					|| die "setting uid"

$niclcmd -create /users/"$user" _writers_passwd "\"$user\""		|| die "setting _writers_passwd"
$niclcmd -create /users/"$user" _writers_tim_passwd "\"$user\""	|| die "setting _writers_tim_passwd"
$niclcmd -create /users/"$user" _writers_picture "\"$user\""	|| die "setting _writers_picture"
$niclcmd -create /users/"$user" _writers_hint "\"$user\""		|| die "setting _writers_hint"
if [ "$sysVers" != "Jaguar" ]; then
	$niclcmd -create /users/"$user" _writers_realname "\"$user\""	|| die "setting _writers_realname"
fi


# Add entries for optional parameters, if supplied

if [ "$LOGINPIC" ]; then
	$niclcmd -create /users/"$user" picture "\"$LOGINPIC\"" 	|| die "adding loginpic"
fi
	
if [ "$NAPRIVS" ]; then
	$niclcmd -create /users/"$user" naprivs "\"$NAPRIVS\"" 		|| die "adding naprivs"
fi

if [ "$PWCHANGE" ]; then
	$niclcmd -create /users/"$user" change "\"$PWCHANGE\""		|| die "adding pw change value"
fi

if [ "$PWEXPIRE" ]; then
	$niclcmd -create /users/"$user" expire "\"$PWEXPIRE\""		|| die "adding pw expire value"
fi


# If "-admin" specified, add the user "$user" to the groups needed
if [ "$ADMINUSER" ]; then
	$niclcmd -append /groups/admin users "$user"				|| die "adding user to admin group"

	if [ "$sysVers" != "Jaguar" ]; then
		$niclcmd -append /groups/appserverusr users "$user"		|| die "adding user to appserverusr group"
		$niclcmd -append /groups/appserveradm users "$user"		|| die "adding user to appserveradm group"
	fi
fi

# Suppress the launch of Setup Assistant, and/or Registration Assistant, unless we specify otherwise
if [ -z "$LEAVEASD" ]; then
	touch "$SYSROOT/$APPLESETUPDONE"		|| die "adding $APPLESETUPDONE"
else
	if [ -f "$SYSROOT/$APPLESETUPDONE" ]; then
		echo "WARNING: $APPLESETUPDONE already exists; Setup Assistant will not run">&2
	fi
fi

if [ -z "$LEAVEREG" ]; then
	touch "$SYSROOT/$SETUPREGCOMPLETE"		|| die "adding $SETUPREGCOMPLETE"
else
	if [ -f "$SYSROOT/$SETUPREGCOMPLETE" ]; then
		echo "WARNING: $SETUPREGCOMPLETE already exists; Registration Assistant will not run">&2
	fi
fi


# Finally, echo our results
if [ "$shadowpass" ]; then
	echo "`basename \"$0\"`: Made user '$user' named '$realname', uid $uid, gid $gid, and shadow password"
elif [ "$cryptpass" ]; then
	echo "`basename \"$0\"`: Made user '$user' named '$realname', uid $uid, gid $gid, and crypted password '$cryptpass'"
else
	echo "`basename \"$0\"`: Made user '$user' named '$realname', uid $uid, gid $gid, and NULL password"
fi


# Th-th-th-that's all, folks!
