#!/bin/ksh
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
# Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
#

. /lib/svc/share/smf_include.sh

#
# Establish PATH for non-built in commands
#
export PATH=/usr/bin:/usr/sbin

# load builtin commands
builtin chmod
builtin chown
builtin cp
builtin grep
builtin rm
builtin rmdir

ETC_SHADOW=/etc/shadow
TMP_SHADOW=/tmp/shadow.$$

# site profile
SITE_PROFILE=/etc/svc/profile/site.xml

# property group definitions
PG_USER_ACCOUNT="user_account"
PG_ROOT_ACCOUNT="root_account"

# directory containing initial user profile files
ETC_SKEL=/etc/skel
# initial user profile files
DOT_PROFILE=".profile"
DOT_BASHRC=".bashrc"
INITIAL_DOT_PROFILE="$ETC_SKEL/$DOT_PROFILE"
INITIAL_DOT_BASHRC="$ETC_SKEL/$DOT_BASHRC"

# user account properties
# login name
PROP_USER_LOGIN="$PG_USER_ACCOUNT/login"
# password
PROP_USER_PASSWORD="$PG_USER_ACCOUNT/password"
# description (usually user's full name)
PROP_USER_DESCRIPTION="$PG_USER_ACCOUNT/description"
# full pathname of the program used as the user's shell on login
PROP_USER_SHELL="$PG_USER_ACCOUNT/shell"
# UID
PROP_USER_UID="$PG_USER_ACCOUNT/uid"
# GID
PROP_USER_GID="$PG_USER_ACCOUNT/gid"
# type (role, normal) - see user_attr(4)
PROP_USER_TYPE="$PG_USER_ACCOUNT/type"
# profiles
PROP_USER_PROFILES="$PG_USER_ACCOUNT/profiles"
# roles
PROP_USER_ROLES="$PG_USER_ACCOUNT/roles"
# sudoers entry
PROP_USER_SUDOERS="$PG_USER_ACCOUNT/sudoers"
# expiration date for a login
PROP_USER_EXPIRE="$PG_USER_ACCOUNT/expire"
# name of home directory ZFS dataset 
PROP_USER_HOME_ZFS_FS="$PG_USER_ACCOUNT/home_zfs_dataset"
# home directory mountpoint
PROP_USER_HOME_MOUNTPOINT="$PG_USER_ACCOUNT/home_mountpoint"

# root account properties
# password
PROP_ROOT_PASSWORD="$PG_ROOT_ACCOUNT/password"
# type (e.g. role) - see user_attr(4)
PROP_ROOT_TYPE="$PG_ROOT_ACCOUNT/type"
# expiration date for a login
PROP_ROOT_EXPIRE="$PG_ROOT_ACCOUNT/expire"

# default value for unconfigured properties
SMF_UNCONFIGURED_VALUE=""

#
# get_smf_prop()
#
# Description:
#     Retrieve value of SMF property.
#     For 'astring' type of property, take care of removing quoting backslashes,
#     since according to svcprop(1) man page, shell metacharacters
#     (';', '&', '(', ')', '|', '^', '<', '>', newline, space, tab, backslash,
#     '"', single-quote, '`') are quoted by backslashes (\).
#
# Parameters:
#     $1 - SMF property name
#
# Returns:
#     0 - property was configured in SC manifest
#     1 - property was not configured in SC manifest
#
get_smf_prop()
{
	typeset prop_name="$1"
	typeset prop_value
	typeset prop_type

	#
	# If property is not set for service instance (which means it was not
	# defined in SC manifest), return with 'unconfigured' value.
	#
	svcprop -Cq -p "$prop_name" $SMF_FMRI
	if (( $? != 0 )) ; then
		print -u1 $SMF_UNCONFIGURED_VALUE
		return 1
	fi

	#
	# retrieve property.
	#
	prop_value=$(svcprop -p "$prop_name" $SMF_FMRI)
	if (( $? != 0 )) ; then
		print -u2 "Failed to obtain value of <$prop_name> property" \
		    "which is suspicious, defaulting to" \
		    "<$SMF_UNCONFIGURED_VALUE>."

		print -u1 $SMF_UNCONFIGURED_VALUE
		return 1
	fi

	# for 'astring' type, remove backslashes from quoted metacharacters
	prop_type=$(svccfg -s $SMF_FMRI listprop "$prop_name" |
	    nawk '{ print $2 }')

	if [[ $prop_type == "astring" ]] ; then	
		prop_value=$(print $prop_value | sed -e 's/\\\(.\)/\1/g')

		if (( $? != 0 )) ; then
			print -u2 "Failed when trying to remove '\' from" \
			    "<$prop_name> property, defaulting to" \
			    "<$SMF_UNCONFIGURED_VALUE>."

			print -u1 $SMF_UNCONFIGURED_VALUE
			return 1
		fi

		#
		# Since according to svcprop(1) man page empty ASCII string
		# value is presented as a pair of double quotes (""), we need
		# to check for this combination and replace it
		# with empty string.
		#
		[[ "$prop_value" == "\"\"" ]] && prop_value=""
	fi

	print -u1 "$prop_value"
	return 0
}

#
# set_password()
#
# Description:
#     configure password by modifying shadow(4) file
#
# Parameters:
#     $1 - login name
#     $2 - encrypted password
#
# Returns:
#     aborts with $SMF_EXIT_ERR_FATAL in case of failure
#
set_password()
{
	typeset user=$1
	typeset pass=$2

	# create temporary file
	cp $ETC_SHADOW $TMP_SHADOW
	if (( $? != 0 )) ; then
		print -u2 "Failed to create temporary file $TMP_SHADOW," \
		    "aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi

	#
	# read shadow(4) file and set field 'password' to desired value
	# for matching login name
	#
	# format of shadow(4):
	# username:password:lastchg:min:max:warn:inactive:expire:flag
	#

	nawk -F: '{
		if ( $1 == login ) 
			printf "%s:%s:%s:%s:%s:%s:%s:%s:%s\n",
			    $1, passwd, $3, $4, $5, $6, $7, $8, $9
		else
			print
	}' passwd="$pass" login="$user" $TMP_SHADOW > $ETC_SHADOW

	if (( $? != 0 )) ; then
		print -u2 "Failed to set password in $ETC_SHADOW, aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi

	# remove temporary file
	rm -f $TMP_SHADOW

	if (( $? != 0 )) ; then
		print -u2 "Failed to remove temporary file $TMP_SHADOW," \
		    "aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi
}

#
# configure_account_type()
#
# Description:
#     set 'type' of user account - needs to be done separately, since
#     useradd -K type=<type> is not supported - see useradd(1M) man page
#
# Parameters:
#     $1 - login
#     $2 - account type
#
# Returns:
#     aborts with $SMF_EXIT_ERR_FATAL in case of failure
#
configure_account_type()
{
	typeset account="$1"
	typeset type="$2"

	usermod -K type="$type" "$account"

	if (( $? != 0 )) ; then
		print -u2 "Failed to configure <$account> account as type" \
		    "<$type>, aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi
}

#
# set_expiration_date()
#
# Description:
#     sets expiration date for account, if SMF property is set to "0" (zero)
#     user is forced to change the password at next login
#
# Parameters:
#     $1 - login
#     $2 - expiration date
#
# Returns:
#     aborts with $SMF_EXIT_ERR_FATAL in case of failure
#
set_expiration_date()
{
	typeset account="$1"
	typeset expire="$2"

	if [[ "$expire" == "0" ]] ; then
		print -u1 " User will be prompted to change password for"\
		    "account <$account> at the next login."

		passwd -f "$account"

		if (( $? != 0 )) ; then
			print -u2 "Calling passwd(1) -f failed for user" \
			    "<$account>, aborting."

			exit $SMF_EXIT_ERR_FATAL
		fi
	else
		usermod -e "$expire" "$account"

		if (( $? != 0 )) ; then
			print -u2 "Failed to set expiration date to" \
			    "<$expire> for account <$account>, aborting."

			exit $SMF_EXIT_ERR_FATAL
		fi
	fi
}

#
# create_initial_user_profile()
#
# Description:
#     Creates initial user's profile by copying .profile and .bashrc
#     (in case bash is used as user's shell) from /etc/skel/ directory
#
# Parameters:
#     $1 - account
#     $2 - home directory
#     $3 - shell
#
# Returns:
#     aborts with $SMF_EXIT_ERR_FATAL in case of failure
#
create_initial_user_profile()
{
	typeset account="$1"
	typeset home_dir="$2"
	typeset user_shell="$3"

	cp "$INITIAL_DOT_PROFILE" "${home_dir}/"
	if (( $? != 0 )) ; then
		print -u2 "Failed to copy $INITIAL_DOT_PROFILE to" \
		    "${home_dir}/, aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi

	chmod 0644 "$home_dir/$DOT_PROFILE"
	if (( $? != 0 )) ; then
		print -u2 "Failed to change permissions to 0644" \
		    "for ${home_dir}/$DOT_PROFILE, aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi

	if [[ "$user_shell" == ~(E)bash$ ]] ; then
		print -u1 " bash(1) selected as a shell for <$account>" \
		    "account, copying initial bash profile" \
		    "$INITIAL_DOT_BASHRC to home directory."

		cp "$INITIAL_DOT_BASHRC" "${home_dir}/"

		if (( $? != 0 )) ; then
			print -u2 "Failed to copy $INITIAL_DOT_BASHRC to" \
			    "${home_dir}/, aborting."

			exit $SMF_EXIT_ERR_FATAL
		fi

		chmod 0644 "$home_dir/$DOT_BASHRC"
		if (( $? != 0 )) ; then
			print -u2 "Failed to change permissions to 0644" \
			    "for ${home_dir}/$DOT_BASRC, aborting."

			exit $SMF_EXIT_ERR_FATAL
		fi
	fi

	#
	# set correct ownership for files and home directory
	#

	chown -R $account:$gid "$home_dir"
	if (( $? != 0 )) ; then
		print -u2 "Failed to set ownership to $account:$gid for" \
		    "${home_dir} directory and user files, aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi
}

#
# create_user_account()
#
# Description:
#     creates user account
#
# Returns:
#     aborts with $SMF_EXIT_ERR_FATAL in case of failure
#
create_user_account()
{
	typeset login_name
	typeset uid
	typeset gid
	typeset shell
	typeset roles
	typeset home_zfs_fs
	typeset home_mntpoint
	typeset desc
	typeset profiles
	typeset account_type
	typeset sudoers
	typeset password
	typeset expire

	# CLI options for useradd(1M)
	typeset useradd_opt=""

	#
	# User account can't be created if login is not provided.
	# Do not treat it as fatal error, just log it
	#
	login_name=$(get_smf_prop $PROP_USER_LOGIN)
	if [[ -z "$login_name" ]]; then
		print -u1 " Login name not provided, user account" \
		    "will not be created."
		return
	fi

	#
	# If user account already exists, do not proceed with the
	# configuration. Only creating user account from scratch
	# is supported. Thus messing with existing configuration could
	# produce undetermined results.
	#
	grep "^${login_name}:" $ETC_SHADOW
	if (( $? == 0 )) ; then
		print -u1 " Login <$login_name> already exists, skipping" \
		    "user account configuration."

		return
	fi

	# get UID. If not provided, let useradd(1M) fill in the default
	uid=$(get_smf_prop $PROP_USER_UID)
	(( $? == 0 )) && useradd_opt="$useradd_opt -u $uid"

	# get GID. If not provided, use 10 (staff) as a default
	gid=$(get_smf_prop $PROP_USER_GID)
	(( $? != 0 )) && gid=10
	useradd_opt="$useradd_opt -g $gid"

	# get user's shell. If not provided, let useradd(1M) fill in the default
	shell=$(get_smf_prop $PROP_USER_SHELL)
	[[ -n "$shell" ]] &&
	    useradd_opt="$useradd_opt -s $shell"

	# get list of comma separated roles
	roles=$(get_smf_prop $PROP_USER_ROLES)
	[[ -n "$roles" ]] &&
	    useradd_opt="$useradd_opt -R $roles"

	#
	# get name of home directory ZFS dataset
	# If not provided, use <root_pool>/export/home/<login_name>
	# as a default.
	#
	home_zfs_fs=$(get_smf_prop $PROP_USER_HOME_ZFS_FS)
	[[ -z "$home_zfs_fs" ]] &&
	    home_zfs_fs="rpool/export/home/$login_name"

	#
	# get home directory mountpoint
	#
	home_mntpoint=$(get_smf_prop $PROP_USER_HOME_MOUNTPOINT)

	#
	# Configure ZFS dataset for user's home directory
	# If running in non-global zone, ZFS dataset was created in global zone
	# and delegated to non-global zone
	#
	print -u1 " Creating user home directory on <$home_zfs_fs> ZFS" \
	    "dataset."

	#
	# Check if ZFS dataset exists. If it does not, take appropriate action
	# taking running environment into account:
	#  global zone: create ZFS dataset
	#  non-global zone: inform user and abort
	#
	zfs list "$home_zfs_fs" > /dev/null 2>&1

	if (( $? != 0 )) ; then
		if smf_is_globalzone; then
			#
			# set also mountpoint if provided, otherwise let zfs
			# inherit the mountpoint from parent dataset
			#
			if [[ -n "$home_mntpoint" ]] ; then
				zfs create -o mountpoint="$home_mntpoint" \
				    "$home_zfs_fs"
			else
				zfs create "$home_zfs_fs"
			fi

			if (( $? != 0 )) ; then
				print -u2 "Failed to create ZFS dataset" \
				    "<$home_zfs_fs>, aborting."

				exit $SMF_EXIT_ERR_FATAL
			fi
		else
			print -u2 "ZFS dataset <$home_zfs_fs> does not exist."
			print -u2 "Please create it in global zone and" \
			    "delegate it to the non-global zone."
			print -u2 "See zonecfg(1M) and zfs(1M) commands" \
			    "for more details."
			print -u2 "Aborting."

			exit $SMF_EXIT_ERR_FATAL
		fi
	else
		#
		# If ZFS mountpoint is not explicitly configured, go with
		# existing ZFS mountpoint. If ZFS dataset has mountpoint set
		# to 'legacy' (which is the case for ZFS datasets delegated
		# to non-global zones), use '/export/home/$login_name' as
		# a default mountpoint.
		#
		if [[ -z "$home_mntpoint" ]] ; then
			zfs_mntpoint=$(zfs get -H mountpoint $home_zfs_fs |
			    nawk '{ print $3 }')

			if (( $? != 0 )) ; then
				home_mntpoint="/export/home/$login_name"

				print -u1 " Could not determine mountpoint" \
				    "for ZFS dataset <$home_zfs_fs>," \
				    "<$home_mntpoint> will be used."
			elif [[ "$zfs_mntpoint" == "legacy" ]] ; then
				home_mntpoint="/export/home/$login_name"

				print -u1 " ZFS dataset <$home_zfs_fs>," \
				    "uses legacy mountpoint, it will be set" \
				    "to <$home_mntpoint> instead."
			fi
		fi

		if [[ -n "$home_mntpoint" ]] ; then
			print -u1 " ZFS dataset <$home_zfs_fs> exists, only" \
			    "ZFS mountpoint will be set to <$home_mntpoint>."

			zfs set mountpoint="$home_mntpoint" "$home_zfs_fs"

			if (( $? != 0 )) ; then
				print -u2 "Failed to set mountpoint to" \
				    "<$home_mntpoint> for ZFS dataset" \
				    "<$home_zfs_fs>, aborting."

				exit $SMF_EXIT_ERR_FATAL
			fi
		fi

		# if in global zone, make sure existing ZFS dataset is mounted
		if smf_is_globalzone; then
			zfs mount "$home_zfs_fs"

			if (( $? != 0 )) ; then
				print -u2 "Could not mount ZFS dataset" \
				    "<$home_zfs_fs>, aborting."

				exit $SMF_EXIT_ERR_FATAL
			fi
		fi
	fi

	#
	# now when ZFS dataset has been configured, use its mountpoint
	# as user's home directory
	#

	home_mntpoint=$(zfs get -H mountpoint $home_zfs_fs |
	    nawk '{ print $3 }')

	if (( $? != 0 )) ; then
		print -u2 "Could not determine mountpoint for ZFS dataset" \
		    "<$home_zfs_fs>, aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi

	print -u1 " Home mountpoint: $home_mntpoint"

	# set permissions to 0755 for home directory
	chmod 0755 "$home_mntpoint"
	if (( $? != 0 )) ; then
		print -u2 "Failed to change permissions to 0755 for" \
		    "${home_mntpoint} directory, aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi

	# create user account by means of useradd(1M)
	print -u1 " Calling useradd(1M) to create user account."
	print -u1 " cmd: useradd $useradd_opt -d $home_mntpoint $login_name"

	useradd $useradd_opt -d "$home_mntpoint" $login_name
	typeset -i ret=$?

	if [[ $ret != 0 ]] ; then
		printf "useradd(1M) failed to create user account, ret=%d," \
		    "aborting.\n" $ret

		exit $SMF_EXIT_ERR_FATAL
	fi

	# set description for user account (usually full user name)
	desc=$(get_smf_prop $PROP_USER_DESCRIPTION)
	if [[ -n "$desc" ]] ; then
		print -u1 " Setting description to <$desc> for account" \
		    "<$login_name>."

		usermod -c "$desc" "$login_name"

		if (( $? != 0 )) ; then
			print -u2 "Failed to set description to <$desc> for" \
			    "<$login_name> account, aborting."

			exit $SMF_EXIT_ERR_FATAL
		fi
	fi

	# assign profiles to user account
	profiles=$(get_smf_prop $PROP_USER_PROFILES)
	if [[ -n "$profiles" ]] ; then
		print -u1 " Assigning profiles <$profiles> to user account" \
		    "<$login_name>."

		usermod -P "$profiles" "$login_name"

		if (( $? != 0 )) ; then
			print -u2 "Failed to assign profiles <$profiles> to" \
			    "<$login_name> account, aborting."

			exit $SMF_EXIT_ERR_FATAL
		fi	
	fi

	# set type of user account
	account_type=$(get_smf_prop $PROP_USER_TYPE)
	if [[ -n "$account_type" ]] ; then
		print -u1 " Configuring <$login_name> account as type" \
		    "<$account_type>."

		configure_account_type "$login_name" "$account_type"
	fi

	# if provided, set password for created user
	password=$(get_smf_prop $PROP_USER_PASSWORD)
	if (( $? == 0 )); then
		print -u1 " Setting password for user <$login_name>."
		set_password "$login_name" "$password"
	fi

	#
	# configure expiration date
	#
	# if required, forces the user to change password at the next login by
	# expiring the password
	#
	expire=$(get_smf_prop $PROP_USER_EXPIRE)
	if [[ -n "$expire" ]] ; then
		print -u1 " Setting expire date to <$expire> for user" \
		    "<$login_name>."

		set_expiration_date "$login_name" "$expire"
	fi

	#
	# Configure sudoers entry, if provided
	#
	sudoers=$(get_smf_prop $PROP_USER_SUDOERS)
	if [[ -n "$sudoers" ]] ; then
		print -u1 " Setting sudoers entry '$sudoers' for user" \
		    "<$login_name>."

		print "$login_name $sudoers" >>/etc/sudoers
	fi

	#
	# Create initial user's profile by copying .profile and .bashrc
	# (in case bash is used as user's shell) from /etc/skel/ directory
	#
	create_initial_user_profile "$login_name" "$home_mntpoint" "$shell"

	#
	# Now unmount the ZFS dataset and remove mountpoint.
	# svc:/system/filesystem/local:default SMF service will later in the
	# boot process take care of mounting all ZFS datasets and creating
	# mountpoints in required order.
	#
	print -u1 " Unmounting <$home_zfs_fs> home directory ZFS dataset."
	zfs unmount "$home_zfs_fs"
	if (( $? != 0 )) ; then
		print -u2 "Failed to unmount <$home_zfs_fs> ZFS dataset," \
		   "aborting."

		exit $SMF_EXIT_ERR_FATAL
	fi

	#
	# do not check return code from rmdir - we know it might fail
	# due to the fact that some of subdirectories might not be empty
	#
	print -u1 " Removing <$home_mntpoint> home directory ZFS mountpoint."
	rmdir -ps $home_mntpoint
}

#
# configure_root_account()
#
# Description:
#     configures root account
#
# Returns:
#     aborts with $SMF_EXIT_ERR_FATAL in case of failure
#
configure_root_account()
{
	typeset password
	typeset account_type
	typeset expire

	# password
	password=$(get_smf_prop $PROP_ROOT_PASSWORD)
	if (( $? == 0 )); then
		print -u1 " Setting root password."
		set_password root "$password"
	fi

	# configure account type (e.g. role)
	# set type of user account
	account_type=$(get_smf_prop $PROP_ROOT_TYPE)
	if [[ -n "$account_type" ]] ; then
		print -u1 " Configuring root account as type <$account_type>."
		configure_account_type root "$account_type"
	fi

	# set expiration date
	expire=$(get_smf_prop $PROP_ROOT_EXPIRE)
	if [[ -n "$expire" ]] ; then
		print -u1 " Setting expire date to <$expire> for root."
		set_expiration_date root "$expire"
	fi
}

#
# remove_pg()
#
# Description:
#     removes property group from service specified by $SMF_FMRI
# 
# Parameters:
#     $1 - property group
#
# Returns:
#     aborts with $SMF_EXIT_ERR_FATAL in case of failure
#
remove_pg()
{
	typeset pg=$1

	print -u1 " Removing property group <$pg>."
	svccfg -s $SMF_FMRI delpg $pg

	if (( $? != 0 )) ; then
		print -u2 "Failed to remove <$pg> property group, aborting."
		exit $SMF_EXIT_ERR_FATAL
	fi
}

## Main ##

# check if root account is to be configured
svcprop -C -q -p $PG_ROOT_ACCOUNT $SMF_FMRI
(( $? == 0 )) && configure_root=true || configure_root=false

# check if user account is to be configured
svcprop -C -q -p $PG_USER_ACCOUNT $SMF_FMRI
(( $? == 0 )) && configure_user=true || configure_user=false

# configure root acount
if $configure_root; then
	print -u1 "Configuring root account."

	configure_root_account

	remove_pg $PG_ROOT_ACCOUNT

	print -u1 "root account successfully configured."
fi

# configure user acount
if $configure_user; then
	print -u1 "Configuring user account."

	create_user_account

	remove_pg $PG_USER_ACCOUNT

	print -u1 "User account successfully configured."
fi

#
# remove site.xml link pointing to System Configuration profile.
# This is workaround for the fact that the profile is applied during
# both Early as well as Late Manifest Import process. We need to assure
# that the profile is applied only once, so that configuration process
# is not run twice during first boot.
#

if [[ -L "$SITE_PROFILE" ]] ; then
	rm "$SITE_PROFILE"

	if (( $? != 0 )) ; then
		print -u2 "Failed to remove $SITE_PROFILE link, aborting."
		exit $SMF_EXIT_ERR_FATAL
	fi

	print -u1 "System successfully configured."
fi

exit $SMF_EXIT_OK

