#!/bin/sh -efu
# Command format:
# <Target> <Action> <Arguments>
#
# <Target> := Package|@Group
# <Action> := add|del|leader|replace
#
# Superuser additional actions:
# <Action> := create|delete
#

. girar-sh-functions

. shell-error
. shell-quote

enable -f /usr/lib/bash/lockf lockf
builtin lockf -v "$GIRAR_ACL_CONF_DIR"
builtin lockf -v "$GIRAR_ACL_STATE_DIR"

workdir=
cleanup()
{
	trap - EXIT
	[ -z "$workdir" ] || rm -rf -- "$workdir"
	exit "$@"
}

exit_handler()
{
	cleanup $?
}

signal_handler()
{
	cleanup 143
}

make_workdir()
{
	[ -z "$workdir" ] || return 0
	trap exit_handler EXIT
	trap signal_handler HUP PIPE INT QUIT TERM
	workdir="$(mktemp -dt "$PROG.XXXXXXXX")" || exit 1
}

realname()
{
	if [ -n "$1" -a -z "${1##@*}" ]; then
		printf %s "${1#@} group"
	else
		local n
		n="$(getent passwd ${USER_PREFIX}$1 |cut -d: -f5 |tr -d '"')"
		printf %s "${n:-$1}"
	fi
}

email_address()
{
	if [ -n "$1" -a -z "${1##@*}" ]; then
		printf '<%s@%s>' "${1#@}" "packages.$EMAIL_DOMAIN"
	else
		printf '<%s@%s>' "$1" "$EMAIL_DOMAIN"
	fi
}

writelog()
{
	[ "$#" -eq 1 ] || return 0
	printf '< %s\n' "$(printf %s "$cmd_info" |tr -s '[:space:]' ' ')" >>"$logfile"
	printf '> %s\n' "$(printf %s "$1" |tr -s '[:space:]' ' ')" >>"$logfile"
}

remove_dups()
{
	local list=
	for o; do
		[ -n "$list" -a -z "${list##* $o *}" ] ||
			list="$list $o "
	done
	list="${list# }"
	list="${list% }"
	printf %s "$list" |tr -s '[:space:]' ' '
}

change()
{
	local owners
	owners="$(remove_dups "$@")"

	if [ "$owners" = "$prev_owners" ]; then
		writelog "IGNORE: Nothing to change"
		return
	fi

	local qitem
	qitem="$(quote_sed_regexp "$item")"
	sed -i "s/^\($qitem[[:space:]]\).*/\1$owners/" "$listfile" &&
		writelog "OK: $item: $owners" ||
		writelog "ERROR: $item: $owners"
}

show_cmd_usage()
{
	local msg="USAGE: <package|group> $action"
	case "$action" in
		create|*add|*del) msg="$msg <owners ...>" ;;
		replace) msg="$msg <old-owner> <new-owner>" ;;
		leader) msg="$msg <owner>" ;;
	esac
	writelog "$msg"
}

check_usage()
{
	if [ -z "$prev_owners" ]; then
		writelog "ERROR: $item: $item_type not found in acl file"
		return 1
	fi

	if [ -z "$new_owners" ]; then
		show_cmd_usage
		return 1
	fi
}

check_new_members()
{
	local i qi
	for i; do
		if [ "${i#@}" = "$i" ]; then
			if [ ! -d "$GIRAR_PEOPLE_QUEUE/${i##*/}" ]; then
				writelog "ERROR: $i: Login name not found"
				return 1
			fi
		elif [ "$i" != '@everybody' ]; then
			qi="$(quote_sed_regexp "$i")"
			if [ -z "$(sed -n "/^$qi[[:space:]]/p" "list/list.groups.$repository")" ]; then
				writelog "ERROR: $i: Group not found in acl file"
				return 1
			fi
		fi
	done
}

pkgadd()
{
	check_usage && check_new_members $new_owners ||
		return 1

	local o owners=

	# Merge old and new owners, remove @nobody if any.
	for o in $prev_owners $new_owners; do
		[ "$o" = '@nobody' ] ||
			owners="$owners $o"
	done

	change ${owners:-@nobody}
}

pkgdel()
{
	check_usage ||
		return 1

	local o owners=

	if [ "$prev_owners" != "@nobody" ]; then
		new_owners=" $new_owners "
		# Filter out new owners from old owners.
		for o in $prev_owners; do
			if [ -n "${new_owners##* $o *}" ]; then
				owners="$owners $o"
			fi
		done
	fi

	set -- $owners
	while [ $# -gt 0 ]; do
		case "${1-}" in
			@qa|@everybody) shift ;;
			*) break ;;
		esac
	done

	change ${*:-@nobody}
}

grpadd()
{
	check_usage && check_new_members $new_owners ||
		return 1

	local o owners=

	# Check new owners for nested groups.
	for o in $new_owners; do
		if [ -z "${o##@*}" ]; then
			writelog "ERROR: Nested group \`$o' detected"
			return 1
		fi
	done

	# Merge old and new owners, remove @nobody if any.
	for o in $prev_owners $new_owners; do
		[ "$o" = '@nobody' ] ||
			owners="$owners $o"
	done

	change $owners
}

grpdel()
{
	check_usage ||
		return 1

	local o owners=

	new_owners=" $new_owners "
	# Filter out new owners from old owners.
	for o in $prev_owners; do
		[ -z "${new_owners##* $o *}" ] ||
			owners="$owners $o"
	done

	set -- $owners
	while [ $# -gt 0 ]; do
		case "${1-}" in
			@qa|@everybody) shift ;;
			*) break ;;
		esac
	done

	if [ -z "${*-}" ]; then
		writelog "ERROR: Group list cannot be made empty"
		return 1
	fi

	change $*
}

leader()
{
	set -- $new_owners
	if [ $# -ne 1 ]; then
		show_cmd_usage
		return 1
	fi

	check_usage && check_new_members $new_owners ||
		return 1

	local new_leader="$1"; shift
	if [ "$leader" = "$new_leader" ]; then
		writelog "IGNORE: Nothing to change"
		return
	fi

	change $new_leader $prev_owners
}

replace()
{
	set -- $new_owners
	if [ $# -ne 2 ]; then
		show_cmd_usage
		return 1
	fi
	local old new
	old="$1"; shift
	new="$1"; shift

	check_usage && check_new_members $new ||
		return 1

	if [ "$old" = "$new" ]; then
		writelog "IGNORE: Nothing to change"
		return
	fi

	local o found= owners=
	for o in $prev_owners; do
		case "$o" in
			$old)
				found=1
				owners="$owners $new"
				;;
			*)
				owners="$owners $o"
				;;
		esac
	done

	if [ -z "$found" ]; then
		writelog "ERROR: Owner \`$old' not found"
		return 1
	fi

	change $owners
}

create()
{
	if [ -n "$prev_owners" ]; then
		writelog "IGNORE: $item: $item_type already exist"
		return
	fi

	if [ -z "$new_owners" ]; then
		show_cmd_usage
		return 1
	fi

	check_new_members $new_owners ||
		return 1

	printf '%s\t%s\n' "$item" "$new_owners" >>"$listfile" &&
		sort -u -o "$listfile" "$listfile" &&
		writelog "OK: $item: $new_owners" ||
		writelog "ERROR: $item: Failed to create"
}

delete()
{
	if [ -z "$prev_owners" ]; then
		writelog "IGNORE: $item: $item_type not found in acl file"
		return
	fi

	if [ -n "$new_owners" ]; then
		show_cmd_usage
		return 1
	fi

	local qitem
	qitem="$(quote_sed_regexp "$item")"
	sed -i "/^$qitem[[:space:]]/d" "$listfile" &&
		writelog "OK: $item: Removed" ||
		writelog "ERROR: $item: Failed to remove"
}

nmuadd()
{
	local changed= p b s e
	while read p b s e; do
		if [ "$item" != "$p" -o "$login" != "$b" ]; then
			printf '%s\t%s\t%s\t%s\n' \
				"$p" "$b" "$s" "$e"
			continue
		fi

		[ $start_time -lt $s ] || start_time="$s"
		[ $end_time -gt $e ]   || end_time="$e"

		printf '%s\t%s\t%s\t%s\n' \
			"$item" "$login" "$start_time" "$end_time"
		changed=1

	done <"list/list.nmu.$repository" >new

	[ -n "$changed" ] ||
		printf '%s\t%s\t%s\t%s\n' \
			"$item" "$login" "$start_time" "$end_time" >> new

	sort -uo "list/list.nmu.$repository" new &&
		writelog "OK: $item $login $start_time $end_time" ||
		writelog "ERROR: Failed to add NMU"
}

nmudel()
{
	local qitem qlogin
	qitem="$(quote_sed_regexp "$item")"
	qlogin="$(quote_sed_regexp "$login")"

	if [ -z "$(sed -n "/^$qitem[[:space:]]$qlogin[[:space:]]/p" \
		"list/list.nmu.$repository")" ]; then
		writelog "IGNORE: NMU not found in acl file"
		return
	fi

	sed -i "/^$qitem[[:space:]]$qlogin[[:space:]]/d" \
		"list/list.nmu.$repository" &&
		writelog "OK: NMU removed" ||
		writelog "ERROR: Failed to remove NMU"
}

pkgnmu()
{
	set -- $new_owners

	local cmd="$1"; shift
	case "$cmd" in
		add|del) func_name="nmu${cmd}" ;;
		*)
			writelog "ERROR: nmu $cmd: Invalid action"
			return 1
			;;
	esac

	new_owners="$*"
	check_usage ||
		return 1

	local login="$1"; shift
	if [ "$login" = '_' ]; then
		login='*'
	else
		check_new_members "$login" ||
			return 1
	fi
	local start_time="$1"; shift
	local end_time="$1"; shift
	$func_name
}

grpnmu()
{
	writelog "ERROR: @item: Group is not allowed here"
	return 1
}

process_user_rules()
{
	local rulesfile="$1"; shift

	local item action new_owners
	while read item action new_owners; do
		local item_type action_type listfile

		case "$item" in
			''|\#*)
				continue
				;;
			@*)
				item_type='Group'
				action_type='grp'
				listfile="list/list.groups.$repository"
				;;
			*)
				item_type='Package'
				action_type='pkg'
				listfile="list/list.packages.$repository"
				;;
		esac

		# Handle command aliases.
		case "$action" in
			rem) action=del ;;
		esac

		local cmd_info
		cmd_info="$item $action $new_owners"

		# Check action name.
		local func_name
		case "$action" in
			add|del|nmu)	func_name="${action_type}${action}" ;;
			replace|leader)	func_name="$action" ;;

			# privileged actions
			create|delete)
				if ! GIRAR_USER="$person" girar-check-superuser "$repository"; then
					writelog "ERROR: $item $action: Permission denied"
					return 1
				fi
				func_name="$action"
				;;
			*)
				writelog "ERROR: $item $action: Invalid action"
				return 1
				;;
		esac

		# Check new_owners
		if [ -n "$(printf %s "$new_owners" |LANG=C tr -d '[@a-z_0-9[:space:]]')" ]; then
			writelog "ERROR: $item $action: $new_owners: Invalid argument(s)"
			return 1
		fi
		local a
		for a in $new_owners; do
			printf %s "$a" |egrep -qs '^@?[a-z_0-9]+$' ||
			{
				writelog "ERROR: $item $action: $new_owners: Invalid argument(s)"
				return 1
			}
		done

		# Check perms
		local msg
		if [ del = "$action" -a "$person" = "$new_owners" ]; then
			# $person is allowed to del self from the list (ALT#19215)
			msg="$(girar-check-acl-item "$item" \
				"list/list.packages.$repository" \
				"list/list.groups.$repository")"
		else
			msg="$(girar-check-acl-leader "$person" "$item" \
				"$repository" list)"
		fi ||
		{
			writelog "ERROR: $item $action: $msg"
			return 1
		}

		local qitem prev_owners
		qitem="$(quote_sed_regexp "$item")"
		prev_owners="$(sed -n "s/^$qitem[[:space:]]\+//p" "$listfile")"

		# Get current leader
		local leader
		leader="${prev_owners%% *}"

		# First failure breaks the loop
		$func_name ||
			return 1

	done <"$rulesfile"
}

make_workdir
cd "$workdir"

mkdir -- list mails status diff notify

# Copy all active acl files to workdir.
find "$GIRAR_ACL_CONF_DIR" -type f -name 'list.*' \
	-exec cp -at list -- \{\} \+

# Save old acl files for later analysis.
cp -a list list.orig

# Handle new spooled acl files in ascending modification time order.
find "$GIRAR_ACL_STATE_DIR" -mindepth 2 -maxdepth 2 -type f \
	-newer "$GIRAR_ACL_STATE_DIR/.stamp" -name '*.acl' -printf '%T@\t%p\n' |
	sort |
	cut -f2 |
while read rulesfile; do

	person="${rulesfile##*/}"
	person="${person%.acl}"

	repository="${rulesfile%/*}"
	repository="${repository##*/}"

	logfile="status/$person.$repository."

	if process_user_rules "$rulesfile"; then
		# Install all working acl files as active.
		find list -type f -name 'list.*' \
			-exec cp -at "$GIRAR_ACL_CONF_DIR" -- \{\} \+
		newlogfile="${logfile}COMPLETE"
	else
		# Copy all active acl files to workdir.
		find "$GIRAR_ACL_CONF_DIR" -type f -name 'list.*' \
			-exec cp -at list -- \{\} \+
		newlogfile="${logfile}ABORTED"
	fi

	mv "$logfile" "$newlogfile"
	rm -f -- "$rulesfile"

	# Store information required for sending emails at the end of processing.
	printf '%s %s %s\n' "$person" "$repository" "$newlogfile" >>mails/info
done

# Update timestamp.
touch -- "$GIRAR_ACL_STATE_DIR"/.stamp
sleep 1

[ -s mails/info ] ||
	exit 0

rsync -rlt -- "$GIRAR_ACL_CONF_DIR/" "$GIRAR_ACL_PUB_DIR/"

while read person repository logfile; do
	status="${logfile##*.}"
	realname="$(realname "$person")"
	cat >mails/msg <<EOF
From: Girar ACL robot <girar-acl@$EMAIL_DOMAIN>
To: "$realname" $(email_address "$person")
Cc: Girar ACL robot <girar-acl@$EMAIL_DOMAIN>
X-Incominger: acl
Subject: [girar-acl] $status $repository
Content-Type: text/plain; charset=us-ascii

Dear $realname!

Result of your acl command(s) is listed below:

EOF
	cat -- "$logfile" >>mails/msg
	cat >>mails/msg <<EOF

Summary: ACL change transaction $status.


-- 
Rgrds, your Girar ACL robot

EOF
	/usr/sbin/sendmail -i -t <mails/msg
done <mails/info

# Now lets have a look what have been changed.
set +f
for f in "$GIRAR_ACL_CONF_DIR"/list.*; do
	t=diff/"${f##*/}"
	sort -- list.orig/"${f##*/}" "$f" |
		uniq -u >"$t"
	[ -s "$t" ] || rm -f "$t"
done

for f in diff/list.*; do
	[ -f "$f" ] || continue
	repo="${f##*/}"
	repo="${repo#list.*.}"
	mkdir -p notify/"$repo"

	list_old="list.orig/${f##*/}"
	list_new="$GIRAR_ACL_CONF_DIR/${f##*/}"
	engaged="$(cut -f2- "$f"|
		tr ' ' '\n' |
		sed 's/^@nobody$/@everybody/' |
		sort -u)"
	# Find items for each engaged person.
	for person in $engaged; do
		qperson="$(quote_sed_regexp "$person")"
		if [ "$person" = '@everybody' ]; then
			qperson="\\(@nobody\\|@everybody\\)\\>"
		elif [ -z "${person##@*}" ]; then
			qperson="$qperson\\>"
		else
			qperson="\\<$qperson\\>"
		fi
		items=$(grep "^[^[:space:]]\\+[[:space:]].*$qperson" "$f" |
			cut -f1 |sort -u)
		for item in $items; do
			qitem="$(quote_sed_regexp "$item")"
			old=$(sed -n "s/^\\($qitem[[:space:]]\\)//p" "$list_old")
			new=$(sed -n "s/^\\($qitem[[:space:]]\\)//p" "$list_new")
			if [ -z "$old" ]; then
				printf '%s: CREATED -> "%s"\n' "$item" "$new"
			elif [ -z "$new" ]; then
				printf '%s: "%s" -> DELETED\n' "$item" "$old"
			else
				printf '%s: "%s" -> "%s"\n' "$item" "$old" "$new"
			fi
		done >>"notify/$repo/$person"
	done
done

for d in notify/*; do
	[ -d "$d" ] || continue
	repository="${d##*/}"
	for f in "$d"/*; do
		[ -f "$f" ] || continue
		person="${f##*/}"
		realname="$(realname "$person")"
		cat >mails/msg <<EOF
From: Girar ACL robot <girar-acl@$EMAIL_DOMAIN>
To: "$realname" $(email_address "$person")
X-Incominger: acl
X-Incominger-Reason: acl-change-notification
Subject: [girar-acl] $repository changes summary
Content-Type: text/plain; charset=us-ascii

Dear $realname!

You have been engaged in ACL change(s) listed below:

EOF
	cat -- "$f" >>mails/msg
	cat >>mails/msg <<EOF


-- 
Rgrds, your Girar ACL robot

EOF
	/usr/sbin/sendmail -i -t <mails/msg
	done
done
