:
#
# NAME:
#	adduser.sh - portable add user script
#
# SYNOPSIS:
#	adduser.sh [options] "file" ...
#	adduser.sh [options] ["username" "fullname"]
#	
# DESCRIPTION:
#	Adds users and their home directory to many different *nix.
#	It can populate home directories with prototype files and
#	initiate user or group quotas if desired (and suppored by the
#	OS). 
#	
#	If given one or more "file"s (``-'' indicates stdin) users are
#	added in batch mode.  Each "file" should contain records like:
#.nf
#
#		var=value
#		username full name
#	or
#		username:passwd:uid:gid:full name:home:shell
#.fi
#
#	It does not matter whether the OS uses shadow passwd files or
#	has extra fields, adduser.sh allways deals with v7 format
#	passwd entries and converts them to the OS specific format.
#	
#	If your OS does not use a plain v7 passwd file, it is wise to
#	set the '-7' flag so that adduser will bail out if it does not
#	know what to do for your OS.
#	
#	The "var"="value" records can be used to change defaults.
#	They are evaluated as root - so take care!
#	
#	If "username" is given and contains several ``:'' chars it is
#	assumed to be a complete passwd(5) entry ready to include.
#	Note that regardless of the system, this argument should be in
#	the old seven field format.  If the user id is to duplicate an
#	existing account (eg. "uid" is 0) '-f' is required (see
#	Options below).
#
#	If "username" is present but does not contain any ``:'' chars,
#	then "username" and "fullname" are used as described below,
#	but no interaction is required.	 Only a single user can be
#	added this way.
#	
#	If "username" is not given it prompts for a
#	"username" and "fullname" which become part of the passwd file
#	entry for the new user.	 It adds "username" to "Group"
#	(creating it if necessary) and uses "uid" or the 'gid' of
#	"Group" ("GroupId") as a starting point for its search for an
#	unused 'uid'.  By default it will prompt for a passwd after adding
#	each user, but '-p' can be used to set a pre-encrypted password
#	or '-P' can be used to give a clear text password which the
#	script will encrypt and then use for each new "username".
#
#	Most of the variables used are obvious.	 "Homes" is the parent
#	directory of new users home directories.
#
#	Options:
#
#	-f	Force the nominated ids to be used.  This is needed if
#		duplicate uid's are wanted.
#
#	-u "uid"
#		If '-f' is used, then the user(s) will be given this
#		"uid", otherwise it is used as the starting point in
#		the search for an unused uid.
#	
#	-g "GroupId"
#		Put users in the group with this gid.
#
#	-G "Group"[:"GroupId"]
#		Add users to "Group" creating it if need be with
#		"GroupId".
#
#	-H "Homes"
#		Identifies the parent directory of users home
#		directory.
#
#	-S "Shell"
#		Unless a passwd entry is used as input, the user will
#		be given this shell.
#
#	-p "cryptpw"
#		A default encrypted passwd.
#
#	-P "clearpw"
#		A clear text passwd that adduser will encrypt - needs
#		perl(1).
#
#	-Q "QUOTA_PROTOTYPE"
#		Nominate the prototype quota entry.  This will cause
#		disk quotas to be enabled for new users.
#
#	-7	OS does NOT use plain v7 passwd file. adduser.sh knows
#		how to handle systems like 4.4BSD and Solaris,
#		othewise it assumes that vipw(8) will know what to do
#		with a v7 format enty.  This option signifies that
#		that assumption is false.
#
#	-A "PW_AGE"
#		Enable passwd ageing (see below).
#		
#	-l	List all the defaults.
#
#	Most options can be set on a per machine basis by creating a
#	file '.adduserrc' in the super users home directory, /etc or
#	in the directory where 'adduser.sh' is found.
#
#	If "Homes"/.adduserrc exists it will be processed after any
#	others, so can be used to set defaults on a per project basis.
#
#	If the directory "Homes"/default exists, its contents are
#	replicated in the new users home directory.
#
#	Quotas:
#
#	Using a prototype quota, is the most painless way of
#	initializing quotas.  If the "QUOTA_PROTOTYPE" variable is set
#	and the "EDQUOTA" command exists, we run:
#.nf
#
#		"EDQUOTA" "EDQUOTA_OPTS" -p "QUOTA_PROTOTYPE" "username"
#.fi
#
#	For each new users.
#	
#	Passwd Aging:
#	
#	Depending on the OS, password aging is enabled if "PW_AGE" is
#	set.  Usually 'yes' or 'immediate' does the trick, and the
#	script will do the right thing.	 On Solaris the following
#	variables can be used to control the various fields in the
#	shadow file:
#	
#	"PW_MIN"
#		Min number of days between passwd changes.
#		(default is empty).
#		 
#	"PW_MAX"
#		Max number of days between passwd changes.
#		(default is 365).
#
#	"PW_WARN"
#		Number of days before passwd expiration that user is
#		warned when they login. (default 14).
#
#	"PW_INACTIVE"
#		Number of days inactivity that will cause account to
#		be dissabled. (default empty).
#
#	"PW_EXPIRE"
#		Fixed date at which account will expire (default empty).
#	
# NOTES:
#	The script handles shadow password files on Solaris 2.x, other
#	machines may break.  It has been tested on NetBSD, SunOS,
#	Solaris and HP-UX.
#	
# AUTHOR:
#	Simon J. Gerraty <sjg@quick.com.au>
#

# RCSid:
#	$Id: adduser.sh,v 1.18 1998/11/27 01:57:28 sjg Exp $
#
#	@(#) Copyright (c) 1993 Simon J. Gerraty
#
#	This file is provided in the hope that it will
#	be of use.  There is absolutely NO WARRANTY.
#	Permission to copy, redistribute or otherwise
#	use this file is hereby granted provided that 
#	the above copyright notice and this notice are
#	left intact. 
#      
#	Please send copies of changes and bug-fixes to:
#	sjg@quick.com.au
#

Myname=`basename $0 .sh`
Mydir=`dirname $0`
case $Mydir in
.)	Mydir=`pwd`;;
esac

ETC=/etc
# for testing only
#ETC=/tmp
#VIPW="ed $ETC/passwd"

host=`(hostname) 2>/dev/null`
host=${host:-`uname -n`}
case "$host" in
*.*)
  host=`IFS=.; set -- $host; echo $1`;;
esac

# things that the rc file may override.
Homes=/home/$host
Shell=/bin/csh
[ -x /bin/ksh ] && Shell=/bin/ksh
Group=users
Passwd='**'
GroupId=1000

# look for an rc file
for d in $HOME /etc $Mydir $Homes
do
	[ -s $d/.${Myname}rc ] && { . $d/.${Myname}rc; break; }
done

EXF=/tmp/e$$
TF=/tmp/u$$
TF2=/tmp/uu$$

case `echo -n .` in -n*) N=;C="\c";; *) N=-n;C=;; esac

OS=`uname -s`
OSREL=`uname -r`

# can you believe that ln on Solaris defaults to
# overwriting an existing file!!!!! We want one that works!
test -x /usr/xpg4/bin/ln && LN=${LN:-/usr/xpg4/bin/ln}
LN=${LN:-ln}

add_path () { [ -d $1 ] && eval ${2:-PATH}="\$${2:-PATH}:$1"; }

fatal() {
        echo "$Myname: $*" >&2
        exit 1
}

# for twiddling
twdl() {
	case "$1" in
	"")
		case "$_twdl" in
		/)	_twdl=-;;
		-)	_twdl="\\";;
		"|")	_twdl=/;;
		*)	_twdl="|";;
		esac
		;;
	*)
		_twdl="$1";;
	esac
	echo $N "$_twdl$C" >&2
}
	
get_id() {
	file=$1
	name=$2
	min=${3:-1000}
	max=`expr $min + ${4:-999}`
	> $EXF
  
	id=`grep "^$name:" $file | cut -d: -f3 | head -1`
	case "$id" in
	"")
		# missing, must add it
		i=$min
		while [ $i -lt $max ]
		do
			n=`cut -d: -f1,3 $file | grep ":$i\$"`
			case "$n" in
			"")
				# an empty slot - use it
				id=$i
				break;;
			esac
			i=`expr $i + 1`
		done
		;;
	*)
		echo $id > $EXF
		;;
	esac
	echo $id
}

add_group() {
	# we put the group name in the member list so that by default no one
	# can newgrp to it...
	echo "adding $1:*:$2:$1 to $ETC/group"
	echo "$1:*:$2:$1" >> $ETC/group
}

# no longer used
upd_group() {
	[ "$mygroup" ] || mygroup=`grep "^$1:" /etc/group | cut -d: -f4`
	case ",$mygroup," in
	",,")			# empty
		add=$2;;
	*,$2,*)			# already there
		add=;;
	*)			# missing
		add=,$2;;
	esac
	[ "$add" ] && sed "/^$1:/s/\$/$add/" $ETC/group > $ETC/group.$$ &&
		mv $ETC/group.$$ $ETC/group
}

# usage: waitfor re file match sleeptime tries
waitfor()
{
	re="$1"
	file=$2
	match="${3:-$1}"
	stime=${4:-5}
	i=${5:-10}

	while [ $i -gt 0 ]
	do
		ok="`grep "$re" $file`"
		eval "case '$ok' in *${match}*) return 0;; esac"
		sleep $stime
		i=`expr $i - 1`
	done
	return 1		# failed
}

upd_passwd() {
	EDITOR=ed
	VISUAL=ed
	didit=
	export EDITOR VISUAL

        NEW_USERS="$NEW_USERS${NEW_USERS:+ }$1"
        
	echo "adding $1:$2:$3:$4:$5:$6:$7 to $ETC/passwd"
	case "$OS" in
	SunOS)
		case "$OSREL" in
		4.0)	PW_AGE=;;
		esac
		case "$PW_AGE" in
		yes|immediate)
			case "$OSREL" in
			4*)	PW_AGE=",.";;
			esac
			;;
		esac
		case "$OSREL" in
		5*)
			# Thank heaven's Solaris has a useradd command
			# because its vipw sucks.
			/usr/sbin/useradd -c "$5" -d "$6" -g$4 -s "$7" -u$3 -o $1 || return
			pw=$2
			case "$PW_AGE:$pw" in
			"":?????????????|?*:*|*,.)
				# either we want passwd aging or
				# its a crypted passwd, put it in shadow file
				# we need to lock access first though
				echo $N "locking $ETC/passwd $C"
				while :
				do
					${LN:-ln} $ETC/passwd $ETC/ptmp 2>/dev/null && break
					twdl
					sleep 2
				done
				echo
				case "$pw" in
				*,.)
					PW_AGE=immediate
					pw=`echo $pw | sed 's/,.$//'`
					;;
				esac
					
				case "$PW_AGE" in
				yes|immediate)
					PW_MAX=${PW_MAX:-365}
					PW_WARN=${PW_WARN:-14}
					#PW_INACTIVE=${PW_INACTIVE:-30}
					case "$PW_AGE" in
					yes)	PWDAYS=${PWDAYS:-`perl -e 'print int(time/86400)'`}
						PW_CHG=${PWDAYS:-0}
						;;
					immediate)
						PW_CHG=0
						;;
					esac
					;;
				esac
				mv $ETC/shadow $ETC/shadow.bak &&
				sed  "/^$1:/s,^.*,$1:$pw:$PW_CHG:$PW_MIN:$PW_MAX:$PW_WARN:$PW_INACTIVE:$PW_EXPIRE:," $ETC/shadow.bak > $ETC/shadow
				chmod 400 $ETC/shadow.bak $ETC/shadow
				rm -f $ETC/ptmp
				;;
			esac
			return	# that's all folks
			;;
		esac
		;;
	*BSD)	# NetBSD at least, have 2 extra fields...
		# sjg's systems support password aging, a value
		# of 1, means immediate change needed.
		case "$PW_AGE" in
		yes|immediate)	PW_AGE="1";;
		esac
		echo "$1:$2:$3:$4::${PW_AGE:-0}:0:$5:$6:$7" > $TF
		didit=:
		;;
	esac
	case "$PW_AGE" in
	yes|immediate)	PW_AGE=;;
	esac
	# most OS's just want this.
	test "$didit" || echo "$1:$2${PW_AGE}:$3:$4:$5:$6:$7" > $TF

	line=`grep -n '^+:' $ETC/passwd | cut -d: -f1`
	test "$line" && line="${line}-1" || line='$'
	vout=/tmp/vout.$$
	> $vout
	( waitfor '[1-9]' $vout; echo ${line}r $TF; echo w; echo q;
	) | ${VIPW:-vipw} > $vout 2>&1
	rm -f $vout
}

add_quotas() {
        if [ "$QUOTA_PROTOTYPE" ]; then
                ${EDQUOTA:-edquota} $EDQUOTA_OPTS -p $QUOTA_PROTOTYPE $*
	fi
}

add_user() { 
	group=$1; shift
  
	eval set -- `echo "'$*'" | sed "s/:/' '/g"`
  
	gid=`get_id $ETC/group $group $4 256`
	if [ "$gid" ]; then
		[ -s $EXF ] || add_group $group $gid 
		uid=`get_id $ETC/passwd $1 $3 1024`
		if [ "$uid" ]; then
			[ "$ForceUID" ] && uid=$3
			[ -s $EXF ] || upd_passwd "$1" "$2" "$uid" "$gid" "$5" "$6" "$7"
			[ -d $6 ] || { mkdir -p $6; chown $1 $6 && chgrp $group $6 && chmod 2775 $6; }
			if [ -d $Defaults ]; then
				case "$AsUser" in
				root)
					(cd $Defaults && find . -depth -print | cpio -pdm$CPIO_LINKS $6)
					;;
				*)				  
					echo "cd $Defaults && find . -depth -print | cpio -pdm$CPIO_LINKS $6" | su $uname
					;;
				esac
			fi
		else
			fatal "can't add user $1"
		fi
	else
		fatal "can't add group $group"
	fi
}

rm_user() {
	( echo /^$1:/d; echo w; echo q ) | ${VIPW:-vipw}
}

# needs perl
encrypt() {
	perl -e "print crypt('$1', '${2:-$$}'),\"\n\""
}

# ok, time to get to work...
set -- `getopt A:fH:S:G:u:p:P:lg:Q7 $*`

add_path /sbin
add_path /usr/sbin
add_path /usr/ucb
add_path /usr/etc

AddGroup=
ForceUID=
PW_AGE=

while [ $# -gt 0 ]
do
	case "$1" in
	--)	shift; break;;
	-7)
                case "$OS$OSREL" in
		SunOS5*) ;;
		*)
                        [ ! -s /etc/master.passwd ] ||
                        	fatal "Don't know how to update passwds on $OS$OSREL"
                	;;
		esac
                ;;
        -Q)	QUOTA_PROTOTYPE=$2; shift;;
                        
	-H)	Homes=$2; shift;
		# pick up group defaults...
		test -s $Homes/.${Myname}rc && . $Homes/.${Myname}rc
		;;
	-S)	Shell=$2; shift;;
	-G)	AddGroup=yes; Group=$2; shift;;
       	-g)	GroupId=$2
                g=`grep :$2: $ETC/group | head -1 | cut -d: -f1`
                Group=${g:-$Group}
                shift;;
	-u)	uid=$2; shift;;
	-p)	Passwd="$2"; shift;;
	-P)	Passwd=`encrypt $2`; shift;;
	-l)	list=yes;;
	-f)	ForceUID=yes;;
	-A)	PW_AGE="$2"; shift;
	esac
	shift
done

Defaults=${Defaults:-$Homes/default}

case "$Group" in
*:*)	eval `IFS=:; set -- $Group; echo Group=$1 GroupId=$2`;;
esac

gid=`get_id $ETC/group $Group $GroupId`
# they may just want to add the group.
[ -s $EXF ] || { test "$AddGroup" && add_group $Group $gid; }
[ "$uid" ] || uid=$gid

case "$Passwd" in
""|none)	Passwd=;;
nologin)	Passwd='*';;
esac

batch() {
        while read uname fname
	do
                case "$uname" in
		"")	break;;
		*:*:*:*)
                        add_user $Group "$uname${fname:+ }$fname"
                        ;;
		*=*)	# XXX this is dangerous!
                        eval $uname $fname
                        ;;
		*)	add_user $Group "$uname:$Passwd:$uid:$gid:$fname:$Homes/$uname:$Shell"
                        ;;
		esac
	done
}

if [ $# -gt 0 ]; then
	case "$1" in
	*:*:*:*)
		add_user $Group "$*"
		;;
	-)
                batch
                ;;
	*)
                if [ -s $1 ]; then
                        cat $* | batch
		else
        		uname=$1; shift
        		fname="$*"
        		add_user $Group "$uname:$Passwd:$uid:$gid:$fname:$Homes/$uname:$Shell"
		fi
		;;
	esac
else
	if [ "$list" = yes ]; then
		echo "Defaults:"
		for v in Group Homes Shell
		do
			eval echo "\	$v=\$$v"
		done
		[ "x$Passwd" = "x**" ] && echo "	Passwd=prompt" || echo "	Passwd=$Passwd"
		[ "$uid" ] && echo "	Initial uid=$uid"
		echo
	fi
	echo Enter username and fullname - spaces in fullname are ok, no quotes needed.
	echo An empty line terminates input.
	echo

	echo $N "username fullname: $C"
	while read uname fname
	do
		[ "$uname" ] || break
		add_user $Group "$uname:$Passwd:$uid:$gid:$fname:$Homes/$uname:$Shell"
		[ "x$Passwd" = "x**" ] && passwd $uname
		echo $N "username fullname: $C"
	done
fi

[ "$NEW_USERS" ] && add_quotas $NEW_USERS

