#!/bin/bash
# persona_test_run.sh
#
# This file aims to be a comprehensive test suite for the persona subsystem.
# It uses two tools:
#   1. persona_mgr - create, destroy, lookup personas
#   2. persona_spawn - spawn processes into personas with a variety of options
# The script relies heavily on the particular output of these tools, so if you
# are modifying / extending those tools, this file also need to be updated to
# properly capture the new output. Specifically, the get_persona_info function
# needs to be maintained / updated.
#
# NOTE: the function get_persona_info() also needs to be kept up to date with
# the types of personas found in bsd/sys/persona.h

PERSONA_MGR="${PWD}/persona_mgr"
PERSONA_SPAWN="${PWD}/persona_spawn"
PERSONA_SPAWN_UNENTITLED="${PWD}/persona_spawn_unentitled"

TEST_DEFAULT_PERSONA=0

if [ ! -d "$TMPDIR" ]; then
	echo "Couldn't find temp directory '$TMPDIR': check permissions/environment?"
	exit 255
fi

if [ ! -e "${PERSONA_MGR}" ] ||  [ ! -x "${PERSONA_MGR}" ]; then
	echo "Can't find '${PERSONA_MGR}': skipping test"
	exit 0
fi
if [ ! -e "${PERSONA_SPAWN}" ] || [ ! -x "${PERSONA_SPAWN}" ]; then
	echo "Can't find '${PERSONA_SPAWN}': skipping test"
	exit 0
fi

function check_for_persona_support() {
	local errno=0
	${PERSONA_MGR} support || errno=$?
	if [ $errno -eq 78 ]; then
		echo "Persona subsystem is not supported - skipping tests"
		exit 0
	fi
	return 0
}
check_for_persona_support


## bail [failure_msg]
#
# exit the script with an error code that corresponds to the line number
# from which this function was invoked. Because we want to exit with a
# non-zero exit code, we use: 1 + (254 % line).
#
function bail() {
	local msg="$1"
	local line=$2
	if [ -z "$line" ]; then
		line=${BASH_LINENO[0]}
	fi
	echo "[$line] ERROR: $msg" 1>&2
	exit $((1 + $line % 254))
}

## check_return [message_on_failure]
#
# Check the return value of the previous command or script line. If the
# value of '$?' is not 0, then call bail() with an appropriate message.
#
function check_return() {
	local err=$?
	local msg=$1
	local line=$2
	if [ -z "$line" ]; then
		line=${BASH_LINENO[0]}
	fi
	echo "CHECK: $msg"
	if [ $err -ne 0 ]; then
		bail "e=$err: $msg" $line
	fi

	return 0
}

## expect_failure [message_on_success]
#
# Check the return value of the previous command or script line. If the
# value of '$?' is 0 (success), then call bail() with a message saying
# that we expected this previous command/line to fail.
# 
function expect_failure() {
	local err=$?
	local msg=$1
	local line=$2
	if [ -z "$line" ]; then
		line=${BASH_LINENO[0]}
	fi
	if [ $err -eq 0 ]; then
		bail "found success, expected failure: $msg" $line
	fi

	echo "EXPECT: failure: $msg"
	return 0
}

## test_num [debug_info] [number]
#
# Check that a variable value is a number, bail() on error.
#
function test_num() {
	local type=$1
	local num=$2
	local line=$3
	if [ -z "$line" ]; then
		line=${BASH_LINENO[0]}
	fi
	if [ -z "$num" ]; then
		bail "invalid (NULL) $type" $line
	fi
	[ "$num" -eq "$num" ] 2>/dev/null
	if [ $? -ne 0 ]; then
		bail "invalid $type: $num" $line
	fi

	return 0
}

## global variables used to return values to callers
_ID=-1
_TYPE="invalid"
_LOGIN=""
_UID=-1

## get_persona_info {persona_id} {persona_login}
#
# Lookup persona info for the given ID/login. At least one of the ID/login
# parameters must be valid
function get_persona_info() {
	local pna_id=${1:-1}
	local pna_login=${2:- }
	local line=$3
	if [ -z "$line" ]; then
		line=${BASH_LINENO[0]}
	fi

	local largs="-u ${pna_id}"
	if [ "${pna_login}" != " " ]; then
		largs+=" -l ${pna_login}"
	fi

	_ID=-1
	_TYPE=-1
	_LOGIN=""
	_UID=-1

	local file="${TMPDIR}/plookup"

	${PERSONA_MGR} lookup ${largs} > "${file}"
	check_return "persona lookup of: ${largs}" $line

	_ID=$(cat "${file}" | grep "+id: " | head -1 | sed 's/.*+id:[ ]*\([0-9][0-9]*\).*/\1/')
	test_num "Persona ID lookup:${largs}" "$_ID"

	local type=$(cat "${file}" | grep "+type: " | head -1 | sed 's/.*+type:[ ]*\([0-9][0-9]*\).*/\1/')
	test_num "+type lookup:${largs}" "$type"
	##
	## NOTE: keep in sync with bsd/sys/persona.h types!
	##
	if [ $type -eq 1 ]; then
		_TYPE=guest
	elif [ $type -eq 2 ]; then
		_TYPE=managed
	elif [ $type -eq 3 ]; then
		_TYPE=priv
	elif [ $type -eq 4 ]; then
		_TYPE=system
	else
		_TYPE=invalid
	fi

	_LOGIN=$(cat "${file}" | grep "+login: " | head -1 | sed 's/.*+login:[ ]*"\([^"]*\)".*/\1/')
	if [ -z "$_LOGIN" ]; then
		bail "invalid login for pna_id:$_ID: '$_LOGIN'" $line
	fi

	# these are always the same
	_UID=$_ID
}

## validate_child_info [output_file] [persona_id] {uid} {gid} {groups}
#
# Parse the output of the 'persona_spawn' command and validate that
# the new child process is in the correct persona with the correct
# process attributes.
#
function validate_child_info() {
	local file=$1
	local pna_id=$2
	local uid=${3:--1}
	local gid=${4:--1}
	local groups=${5:- }
	local line=$6
	if [ -z "$line" ]; then
		line=${BASH_LINENO[0]}
	fi
	local l=( )

	# get the child's PID
	local cpid="$(cat "$file" | grep "Child: PID:" | sed 's/.*Child: PID:\([0-9][0-9]*\).*/\1/')"
	test_num "Child PID" "$cpid" $line

	# validate the child's persona
	l=( $(cat "$file" | grep "Child: Persona:" | sed 's/.*Child: Persona: \([0-9][0-9]*\) (err:\([0-9][0-9]*\))/\1 \2/') )
	if [ ${#l[@]} -ne 2 ]; then
		bail "Invalid Child[$cpid] Persona line" $line
	fi
	test_num "Child Persona ID" "${l[0]}" $line
	test_num "kpersona_info retval" "${l[1]}" $line

	if [ ${l[0]} -ne $pna_id ]; then
		bail "Child[$cpid] persona:${l[0]} != specified persona:$pna_id" $line
	fi

	# Validate the UID/GID
	l=( $(cat "$file" | grep "Child: UID:" | sed 's/.*UID:\([0-9][0-9]*\), GID:\([0-9][0-9]*\).*/\1 \2/') )
	if [ ${#l[@]} -ne 2 ]; then
		bail "Invalid Child[$cpid] UID/GID output" $line
	fi
	if [ $uid -ge 0 ]; then
		if [ $uid -ne ${l[0]} ]; then
			bail "Child[$cpid] UID:${l[0]} != specified UID:$uid" $line
		fi
	fi
	if [ $gid -ge 0 ]; then
		if [ $gid -ne ${l[1]} ]; then
			bail "Child[$cpid] GID:${l[1]} != specified GID:$gid" $line
		fi
	fi

	# TODO: validate / verify groups?

	return 0
}


## spawn_child [persona_id] [uid] {gid} {group_spec}
#
# Create a child process that is spawn'd into the persona given by
# the first argument (pna_id). The new process can have its UID, GID,
# and group membership properties overridden.
#
function spawn_child() {
	local pna_id=$1
	local uid=$2
	local gid=${3:--1}
	local groups=${4:- }
	local line=$5
	if [ -z "$line" ]; then
		line=${BASH_LINENO[0]}
	fi

	local file="child.${pna_id}.u$uid"
	local spawn_args="-I $pna_id -u $uid"
	if [ $gid -ge 0 ]; then
		spawn_args+=" -g $gid"
		file+=".g$gid"
	fi
	if [ "$groups" != " " ]; then
		spawn_args+=" -G $groups"
		file+="._groups"
	fi

	echo "SPAWN: $file"
	${PERSONA_SPAWN} -v $spawn_args ${PERSONA_SPAWN} child -v -E > "${TMPDIR}/$file"
	check_return "child info: $file" $line

	# Grab the specified persona's info so we can
	# verify the child's info against it.
	# get_persona_info puts data into global variables, e.g. _UID, _LOGIN, etc.
	get_persona_info ${pna_id} " " $line
	if [ $uid -lt 0 ]; then
		uid=$_UID
	fi

	validate_child_info "${TMPDIR}/$file" "$pna_id" "$uid" "$gid" "$groups" $line

	## validate that the first child spawned into a persona *cannot* spawn
	## into a different persona...
	if [ $uid -eq 0 ]; then
		${PERSONA_SPAWN} -v $spawn_args ${PERSONA_SPAWN_UNENTITLED} child -v -E -R spawn -v $spawn_args -I ${TEST_DEFAULT_PERSONA} /bin/echo "This is running in the system persona"
		expect_failure "Spawned child that re-execs into non-default persona" $line
	fi
	return 0
}

## get_created_id [output_file]
#
# Parse the output of the 'persona_mgr' command to determine the ID
# of the newly created persona.
#
function get_created_id() {
	local file=$1
	local o=$(cat "$file" | grep "Created persona" | sed 's/.*Created persona \([0-9][0-9]*\):/\1/')
	echo $o
	return 0
}

## create_persona [login_name] [persona_type] {persona_id}
#
# Create a new persona with given parameters.
#
# Returns: the newly created persona ID via the global variable, $_ID
#
function create_persona() {
	local name=${1}
	local type=${2}
	local pna_id=${3:--1}
	local line=$6
	if [ -z "$line" ]; then
		line=${BASH_LINENO[0]}
	fi

	if [ -z "$name" -o -z "$type" ]; then
		bail "Invalid arguments to create_persona '$name' '$type'" $line
	fi

	local file="persona.at${line}"
	# persona ID of '-1' is auto-assigned
	local spawn_args="-v -l $name -i $pna_id"
	if [ $pna_id -eq -1 ]; then
		file+=".auto"
	else
		file+=".${pna_id}"
	fi

	spawn_args+=" -t $type"
	file+=".$type"

	echo "CREATE: $file"
	${PERSONA_MGR} create ${spawn_args} > "${TMPDIR}/${file}"
	check_return "persona creation: ${file}" $line
	# test output should include persona creation output for later debugging
	cat "${TMPDIR}/${file}"

	# validate the output of the persona_mgr tool (what we think we created)
	_ID=`get_created_id "${TMPDIR}/${file}"`
	test_num "persona_id for $file" "$_ID" $line
	if [ ${pna_id} -gt 0 ]; then
		if [ $_ID -ne ${pna_id} ]; then
			bail "Created persona doesn't have expected ID $_ID != ${pna_id}" $line
		fi
	fi

	# validate the entire persona information (what a kpersona_lookup says we created)
	# This function puts data into global variables, e.g. _ID, _LOGIN, etc.
	echo "VALIDATE: ${file}"
	get_persona_info ${pna_id} "$name" $line
	if [ "$name" != "$_LOGIN" ]; then
		bail "${file}: unexpected login '$_LOGIN' != '$name'" $line
	fi
	if [ "$type" != "$_TYPE" ]; then
		bail "${file}: unexpected type '$_TYPE' != '$type'" $line
	fi
	if [ ${pna_id} -gt 0 ]; then
		if [ ${pna_id} -ne $_ID ]; then
			bail "${file}: unexpected ID '$_ID' != '${pna_id}'" $line
		fi
	fi

	return 0
}

## destroy_persona [persona_id]
#
# Destroy the given persona.
#
function destroy_persona() {
	local pna_id=$1
	local line=$2
	if [ -z "$line" ]; then
		line=${BASH_LINENO[0]}
	fi

	echo "DESTROY: ${pna_id}"
	${PERSONA_MGR} destroy -v -i ${pna_id}
	check_return "destruction of ${pna_id}" $line
}

#
#
# Begin Tests!
#
#
echo "Running persona tests [$LINENO] ($TMPDIR)"

##
## Test Group 0: basic creation + spawn tests
##

create_persona "test_default_persona" "guest" 99999
TEST_DEFAULT_PERSONA=$_ID

# default group, specific ID
create_persona "test0_1" "guest" 10001
P0ID=$_ID

spawn_child $P0ID 1100
spawn_child $P0ID 0
spawn_child $P0ID 1100 1101
spawn_child $P0ID 1100 1101 1000,2000,3000
spawn_child $P0ID 1100 -1 1000,2000,3000
destroy_persona $P0ID

##
## Test Group 1: persona creation / re-creation
##

# Create 3 personas with auto-assigned IDs
create_persona "test1_1" "guest"
P1ID=$_ID
create_persona "test1_2" "managed"
P2ID=$_ID
create_persona "test1_3" "priv"
P3ID=$_ID
create_persona "test1_4" "guest"
P4ID=$_ID

D1=$(($P2ID - $P1ID))
D2=$(($P3ID - $P2ID))
D3=$(($P4ID - $P3ID))
if [ $D1 -ne $D2 -o $D1 -ne $D3 -o $D2 -ne $D3 ]; then
	bail "inconsistent automatic Persona ID increment: $D1,$D2,$D3 ($P1ID,$P2ID,$P3ID,$P4ID)"
fi

# make sure we can't re-allocate the same name / ID
${PERSONA_MGR} create -v -l test1_1 -t guest -i -1 && expect_failure "re-create same name:test1_1 type:guest"
${PERSONA_MGR} create -v -l test1_1 -t managed -i -1 && expect_failure "re-create same name:test1_1 type:managed"
${PERSONA_MGR} create -v -l test1_1_new -t managed -i $P1ID && expect_failure "re-create $P1ID with new name:test1_1_new type:managed"

##
## Test Group 2: auto-assigned ID tricks
##

# Notice the difference in IDs, then try to create a persona by
# specifying an ID that will match the next auto-assigned ID
# (should succeed)
P5ID_REQ=$(($P4ID + $D2))
create_persona "test2_1" "guest" ${P5ID_REQ}
P5ID=$_ID
if [ ! $P5ID -eq ${P5ID_REQ} ]; then
	bail "test2_1: ${P5ID_REQ} != $P5ID"
fi

# try to create a persona with auto-assigned ID
# (resulting persona should have ID != P5ID)
create_persona "test2_2" "guest"
P6ID=$_ID
if [ $P6ID -eq $P5ID ]; then
	bail "created duplicate persona IDs: $P6ID == $P5ID"
fi

##
## Test Group 3: persona destruction
##

destroy_persona $P1ID
destroy_persona $P2ID
destroy_persona $P3ID
destroy_persona $P4ID
destroy_persona $P5ID
destroy_persona $P6ID

# try to re-destroy the personas
# (should fail)
${PERSONA_MGR} destroy -v -i $P1ID && expect_failure "re-destroy (1/2) $P1ID"
${PERSONA_MGR} destroy -v -i $P1ID && expect_failure "re-destroy (2/2) $P1ID"
${PERSONA_MGR} destroy -v -i $P2ID && expect_failure "re-destroy $P2ID"
${PERSONA_MGR} destroy -v -i $P3ID && expect_failure "re-destroy $P3ID"
${PERSONA_MGR} destroy -v -i $P4ID && expect_failure "re-destroy $P4ID"
${PERSONA_MGR} destroy -v -i $P5ID && expect_failure "re-destroy $P5ID"
${PERSONA_MGR} destroy -v -i $P6ID && expect_failure "re-destroy $P6ID"

destroy_persona ${TEST_DEFAULT_PERSONA}

# cleanup
rm -rf "${TMPDIR}"

echo ""
echo "${0##/}: SUCCESS"
exit 0
