#!/bin/bash

# update apparmor profile sniplet or selinux policy based on samba
# configuration
#
# For apparmor this script creates and updates a profile sniplet with
# permissions for all
# samba shares, except
# - paths with variables (anything containing a % sign)
# - "/" - if someone is insane enough to share his complete filesystem, he'll have
#   to modify the apparmor profile himself
#
# For selinux this scripts creates and updates a record of directories
# to be labled as samba_share_t corresponding to configured shares
# in a special file. Subsequent executions of this script compare the
# list of shares configured against the shares configured by the previous
# execution to determine the paths of shares that have been added or
# deleted.
#
# The directories labeled correspond
# to the shares configured in smb.conf.
# except those with
# - paths with variables (anything containing a % sign)
# - "/" - if someone is insane enough to share his complete filesystem
# selinux boolean 'samba_enable_home_dirs is enabled or disabled depending
# on if a share such as the special '[users]' share has path '/home'
#
# (c) Christian Boltz 2011-2022
# (c) Noel Power
# This script is licensed under the GPL v2 or, at your choice, any later version.

# exit silently - used if no profile update is needed
silentexit() {
	# echo "$@"
	exit 0
}

# exit with an error message
verboseexit() {
	echo "$@" >&2
	exit 1
}

# if you change this script, _always_ update the version to force an update of the profile sniplet
versionstring="${0##*/} 1.5"

aastatus="/usr/sbin/aa-status"
aaparser="/sbin/apparmor_parser"
loadedprofiles="/sys/kernel/security/apparmor/profiles"

smbconf="/etc/samba/smb.conf"
smbd_profile="/etc/apparmor.d/usr.sbin.smbd"
profilesniplet="/etc/apparmor.d/local/usr.sbin.smbd-shares"
tmp_profilesniplet="/etc/apparmor.d/local/usr.sbin.smbd-shares.new"

selinux_enabled="/sbin/selinuxenabled"

# holds the list of share paths that have been labeled
# for selinux from the last run, this script determines
# which share paths have been added or removed since the last
# run so the selinux labels can be updated as needed
selinuxshares="/etc/samba/selinux-shares"
tmp_selinuxshares="/etc/samba/selinux-shares.new"

set -o pipefail

widelinks=$(testparm -s --parameter-name "wide links" 2>/dev/null)

update_policy_forapparmor() {
	# test -x "$aastatus" || silentexit "apparmor not installed"
	# # "$aastatus" --enabled || silentexit "apparmor not loaded (or not running as root)"
	test -r "$loadedprofiles" || verboseexit "no read permissions for $loadedprofiles - not running as root?"

	test "$widelinks" == "Yes" && {
		echo "[$(date '+%Y/%m/%d %T')] $(basename $0)"
		echo '  WARNING: "wide links" enabled. You might need to modify the smbd apparmor profile manually.'
	} >> /var/log/samba/log.smbd

	test -e "$profilesniplet" || touch "$profilesniplet" # prevent error messages (from grep and diff later in this script) if the sniplet file doesn't exist

	grep -q "$versionstring" "$profilesniplet" && {
		test "$smbconf" -nt "$profilesniplet" || silentexit "smb.conf is older than the AppArmor profile sniplet"
	}

	{
		echo "# autogenerated by $versionstring at samba start - do not edit!"
		echo ""
		testparm -s 2>/dev/null |sed -n '/^[ \t]*path[ \t]*=[ \t]*[^% \t]\{2,\}/ s;^[ \t]*path[ \t]*=[ \t]*\([^%]*\)$;"\1/"   rk,\n"\1/**" rwkl,;p' || verboseexit "generating profile sniplet failed"
} > "$tmp_profilesniplet"

		diff "$profilesniplet" "$tmp_profilesniplet" >/dev/null && {
			rm -f "$tmp_profilesniplet"
			touch "$profilesniplet" # update timestamp - otherwise we'll have to check again on the next run
			silentexit "profile sniplet unchanged"
		}

	mv -f "$tmp_profilesniplet" "$profilesniplet"

	grep -q '^/usr/sbin/smbd (\|^smbd (' /sys/kernel/security/apparmor/profiles || silentexit "smbd profile not loaded"

	echo "Reloading updated AppArmor profile for Samba..."

	# reload profile
	"$aaparser" -r "$smbd_profile"
}

update_policy_forselinux() {
	test -e "$selinuxshares" || touch "$selinuxshares" # prevent error messages (from grep and diff later in this script) if the sniplet file doesn't exist

	# find any 'path' definition ending with /home or /home/
	out=$(testparm -s 2>/dev/null | grep "^["$'\t'" ]*path.*=.*/home/*$")
	res=$?

	if [ $res -eq 0 ]; then
		set_home_bool=true
	else
		set_home_bool=false
	fi

	out=$(testparm -s 2>/dev/null | grep -v  '^.*path.*=.*/home/*$' |sed -n '/^[ \t]*path[ \t]*=[ \t]*[^% \t]\{2,\}/ s;^[ \t]*path[ \t]*=[ \t]*\([^%]*\)$;\1;p')
	OLD_IFS=$IFS
	IFS=$'\n'

	{
		echo "# autogenerated by $versionstring at samba start - do not edit!"
		echo ""
		# only add paths that exist now (to prevent error)
		for share in $out; do
			if [ -d "$share" ]; then
				echo "$share"
			fi
		done
} > "$tmp_selinuxshares"

	IFS=$OLD_IFS

	getsebool samba_enable_home_dirs | grep " on$" > /dev/null
        res=$?

	# first check if we need to enable/disable home dirs

	if [ $res -eq 0 ]; then
		current_home_bool=true
	else
		current_home_bool=false
	fi

	# if we are exporting home dirs then set appropriate selinux boolean
	if [ "${current_home_bool}" != "${set_home_bool}" ]; then
		if [ "${set_home_bool}" = true ]; then
			echo "enabling selinux boolean samba_enable_home_dirs"
			setsebool -P samba_enable_home_dirs 1
		else
			echo "disabling selinux boolean samba_enable_home_dirs"
			setsebool -P samba_enable_home_dirs 0
		fi
	fi

	diff "$selinuxshares" "$tmp_selinuxshares" >/dev/null && {
		rm -f "$tmp_selinuxshares"
		touch "$selinuxshares" # update timestamp - otherwise we'll have to check again on the next run
		silentexit "profile sniplet unchanged"
}

	# discover what shares have been removed since last run
	removed_shares=$(diff --new-line-format="" --unchanged-line-format="" <(sort -u $selinuxshares) <(sort -u $tmp_selinuxshares))
	# discover what shares have been added since last run
	added_shares=$(diff --unchanged-line-format="" --old-line-format="" <(sort -u $selinuxshares) <(sort -u $tmp_selinuxshares))

	OLD_IFS=$IFS
	IFS=$'\n'

	echo "Reloading updated selinux profile for Samba..."

	# semanage fcontext -at | -dt doesn't handle spaces !!!
	# so, it you want to add a path with a space in it you
	# need to create some regrex to match that.
	# see https://bugzilla.redhat.com/show_bug.cgi?id=522087
	#
	# #TODO should think about clearing out all samba_share_t
	# definitions if the match below is changed because
	# a version change might mean the regrex we generate is
	# different and attempts to remove regrex associated with
	# shares we have detected to be removed could fail

	match='[\\s].*'

	if [ "${removed_shares}" ]; then
		for share in $(echo "$removed_shares" | grep -v "^#"); do
			regrex_share=$(echo "$share" | sed "s/[ ] */$match/g")
			semanage fcontext -dt samba_share_t "$regrex_share(/.*)?"
			restorecon -R "$share"
		done
	fi

	# re-label added and deleted share directories
	if [ "${added_shares}" ]; then
		for share in $(echo "$added_shares" | grep -v "^#"); do
			regrex_share=$(echo "$share" | sed "s/[ ] */$match/g")
			out=$(semanage fcontext --list | grep "^$share/.*" | grep "all files")
			res=$?
			if [ $res -eq 0 ]; then
				echo "WARNING: $share has some existing labeling rules that might interfere with newly applied selinux rule"
			fi

			semanage fcontext -at samba_share_t "$regrex_share(/.*)?"
			restorecon -R "$share"
		done
	fi

	IFS=$OLD_IFS

	# move working record of shares whos directories we labeled
	# to current
	mv "$tmp_selinuxshares" "$selinuxshares"
}

$selinux_enabled || if [ -e "$loadedprofiles" ]; then
	update_policy_forapparmor
else
	silentexit "selinux not enabled or apparmor not loaded"
fi
update_policy_forselinux
