#!/bin/bash
#
# Set the appropriate permissions and security contexts on a Noggin Integration
# Framework account (project) directory and it's contents
#
# TODO: Clean all selinux contexts?
#
# Dry-run helper to print instead of run (eg via xargs)
if [ "$1" == "--print" ]; then
	shift
	printf "  %s " "$1"
	shift
	printf "%s " "$@"
	echo
	exit
fi


TMPFS=/dev/shm
DRYRUN=0
# Add sbin to the path for compatibility
if env PATH=$PATH:/usr/sbin:/sbin selinuxenabled 2>/dev/null; then
        SELINUX=1
else
        SELINUX=0
fi

declare -A CMD
CMD[setfacl]=setfacl
CMD[chmod]=chmod
CMD[chown]=chown
CMD[chcon]=chcon

# Print usage / help then exit
function usage {
	local ERROR="$1"
	cat <<-EOD >&2
		  ${ERROR:+$(echo -e "\n  ERROR: $ERROR\n ")}
		  Usage: $(basename "$0") [<options>] </path/to/account/home>

		         -t | --dry-run           Just print the commands to be run, don't actually run them

		         -h | --help | --usage    Show this usage message

	EOD
	exit 1
}

# Argument flag processing, save args for possible sudo call
ARGS=( "$@" )
while true; do
	case "$1" in

		-t | --dry-run ) DRYRUN=1; shift;;

		-h | --help | --usage) usage ;;
		-? | --* ) echo -e "\n  ERROR: Unknown flag '$1'" 2>&1; usage;;

		-- ) shift; break;;
		*) break;
	esac
done

# This should not be run as the root user
# Validate the path argument
if [ $# -eq 0 ]; then
	usage "You must specify an account path"
elif [ $# -gt 1 ]; then
	usage "Only a single account path can be specified"
elif [ ! -d "$1" ]; then
	usage "'$1' is not a directory"
elif [ ! -d "$1/code" ] && [ ! -d "$1/accounts" ]; then
        usage "'$1' does not appear to be a NIF install (either $1/code or $1/accounts is not a directory)"
else
	ACCTHOME=$(readlink -f "${1}")
	ACCTUSER=$(stat -c "%U" ${ACCTHOME})
	shift
fi


#-------------------- If necessary sudo to the account owner -------------------------
#if [ $(id -un) != ${ACCTUSER} ]; then
#		# Nest this condition so we don't hit when running as the ACCTUSER
#		if [ $(id -un) == "root" ]; then
#			# Fork into process run by the ACCTUSER
#			exec sudo -u ${ACCTUSER} $(which "$0") "${ARGS[@]}"
#	 	else
#	 		usage "This script must be executed by root or ${ACCTUSER}"
#			exit 1
#		fi
#fi

# Log only if in dry-run mode
function verbose {
	[ "$DRYRUN" == "1" ] && echo -e "\033[1m$1\033[0m"
}


#
# Warn if there are absolute symlinks detected in the code directory
# because that will probably cause this script to get stuck in a loop and/or fail
#
ABSOLUTELINKS=($(find ${ACCTHOME}/accounts -type l -lname '/*'))
if [[ ${#ABSOLUTELINKS[@]} -gt 0 ]]; then
        echo "WARN: Symlinks using an absolute path were detected (may cause the script to get stuck or fail)"
        printf '===>Detected absolute link %s\n' "${ABSOLUTELINKS[@]}" 2>/dev/null
        echo
fi


#------------------------- Now the actual permission management ----------------------
#
# Because we need to take multiple steps including stripping existing ACLs we work in
# a temporary clone of the account and then use getfacl+setfacl to copy all of the
# uids, gids, modes, flags and ACLs back into the account and getfattr+setfattr to copy
# all of the selinux security contexts back
#
# This still isn't atomic, but it should prevent ever reducing the permissions below
# the target list, even temporarily

# Create a working directory (on tmpfs for performance)
ACCTTEMP=$(mktemp -d $TMPFS/$(basename $0).$$.${ACCTUSER}.XXXXXXXXXX)
if [ -z "$ACCTTEMP" ] || [ ! -d "$ACCTTEMP" ]; then
  echo "ERROR: Failed to create temporary directory" >&2
  exit 1;
fi
TARGET=${ACCTTEMP}/$(basename ${ACCTHOME})

# Copy all the files, but not thirr contents into $ACCTTEMP/<dirname>
cp -ar --attributes-only ${ACCTHOME} ${TARGET}

#------- Now set everything to the target values ------

# Start by cleaning everything to minimal permissions
${CMD[setfacl]} --physical --recursive -b -m u::rwX,g::rX,o::0 ${TARGET}
find ${TARGET} -type f -print0 | xargs -0 -r ${CMD[chmod]} a-x
${CMD[chmod]} -R a-st ${TARGET}

# Let apache and the ocapublic user enter the account directory
${CMD[setfacl]} -m u::rwx,g::rx,o::0,u:apache:x,u:${ACCTUSER}:rx ${TARGET}
if [ $SELINUX -eq 1 ]; then
  ${CMD[chcon]} -R -h -t user_home_t ${TARGET}
  ${CMD[chcon]} -h -t user_home_dir_t ${TARGET}
fi

#
# Paths that httpd needs to traverse do a find, but limit it to dirs matching
# Match:
#   code/*/dist
#   code/vendors/*
#
# We'll also need to g:apache:x all the parents of these
# Finally this doesn't account for any single file symlinks
# We should also warn on things we find linked from the docroot
# but not on the permitted access pattern list
# We need to consider symlinks for parent access only

shopt -s nullglob

IFS=$'\n'

###############################################################################
# Find all of the web content directories, these directories and their children
# should all be g:apache:rX and httpd_user_content_t
#
# We start at our known web entry points, resolve ala realpath() then filter
# based on permitted patterns, any changes to the permitted pattern list must be
# discussed with the Infra team before proceeding
#
WEBDIRS=(
  $(
    find -L ${TARGET}/accounts/*/code/dist -type d -print0 \
     | xargs -0 -r  readlink -f | sort -u \
     | grep -E \
            -e 'code/[^/]+/dist$' \
            -e 'code/[^/]+/vendor/[^/]+$' \
  )
)

# Find anything symlinked from outside a permitted location and warn about it
NOTWEBDIRS=(
  $(
    find -L ${TARGET}/accounts/*/code/dist -print0 \
     | xargs -0 -r readlink -f | sort -u \
     | grep -vE \
            -e 'code/[^/]+/dist(/.*)?$' \
            -e 'code/[^/]+/vendor/(/.*)?$' \
  )
)

if [ ${#NOTWEBDIRS[@]} -gt 0 ]; then
    printf 'You should better not link to that from a web context: %s\n' "${NOTWEBDIRS[@]}" >&2
fi

###############################################################################
# All of webdirs, plus all of the web entry symlinks should have g:apache:x on
# parents, up as far as ${TARGET}
#
# Start with the list of web dirs + the main entry symlinks
WEBPARENTS=( "${WEBDIRS[@]}" ${TARGET}/accounts/*/code/dist )
# Strip the last component off since we only want parents
WEBPARENTS=( $(dirname "${WEBPARENTS[@]}" | sort -u) )
# Expand each path into a directory list
WEBPARENTS=( $(printf '%s\n' "${WEBPARENTS[@]}" | gawk -vTARGET="${TARGET}" '{ while ($0 != TARGET) { print $0; gsub("/[^/]+$", ""); }; }') )


###############################################################################
# The contents of all of the binary / script directories need to be marked as
# executable
#
# TODO: Alternative names, eg 'scripts' ?
# Find all of the directories containing CLI type scripts, the *contents* of these should
# have u::+x set
BINDIRS=( $(find ${TARGET}/code ${TARGET}/accounts -type d -name bin) )


###############################################################################
# Note that apache should *never* be granted write permissions, all writes
# happen as the php-fpm-pool / script owner. NEVER under apache's uid
# TODO: Understand how symlinks work here (eg bbcurrent on dev!)
#WRITEDIRS=( $(find ${TARGET}/[c]gdata ${TARGET}/accounts/ngdata/*) )
WRITEDIRS=()

DATADIRS=()

# Make the webdirs and thier contents accessible to apache
if [ ${#WEBDIRS[@]} -ge 1 ]; then
  if [ $SELINUX -eq 1 ]; then
    printf '%s\0' "${WEBDIRS[@]}" | xargs -0 -r ${CMD[chcon]} -R -h -t httpd_user_content_t
  fi
  printf '%s\0' "${WEBDIRS[@]}" | xargs -0 -r ${CMD[setfacl]} -RP -m g:apache:rX,default:g:apache:rX
fi

# Make the web parents _traversable_ by apache
if [  ${#WEBPARENTS[@]} -ge 1 ]; then
  printf '%s\0' "${WEBPARENTS[@]}" | xargs -0 -r ${CMD[setfacl]} -P -m g:apache:X
fi

# Mark the contents of bin dirs as executable
if [  ${#BINDIRS[@]} -ge 1 ]; then
  printf '%s\0' "${BINDIRS[@]}" | xargs -0 -r ${CMD[setfacl]} -RP -m u::rx
fi

# Mark the contents of data dirs as writeable
if [  ${#DATADIRS[@]} -ge 1 ]; then
  printf '%s\0' "${DATADIRS[@]}" | xargs -0 -r ${CMD[setfacl]} -RP -m u::rwX,default:u::rwX
fi


###############################################################################
# Defense in depth
# mark all ini files as read only
find ${TARGET} -maxdepth 1 -type f -name '*.ini' -print0 | xargs -0 -r ${CMD[chmod]} a-w

# Mark any pem or key files as read only by the user
find ${TARGET} -maxdepth 1 -type f '(' -name '*.pem' -or -name '*.key' ')'  -print0 | xargs -0 -r ${CMD[chmod]} 0400

if [ $DRYRUN -eq 0 ]; then
  # Copy materialized ACLs from tmpfs clone to the actual directory
  pushd ${ACCTHOME} >/dev/null || exit
  setfacl --restore=<(cd ${TARGET}; getfacl -R .)
  if [ $SELINUX -eq 1 ]; then
    # Copy materialized SELinux context from tmpfs clone to the actual directory
    setfattr -h --restore=<(cd ${TARGET}; getfattr --dump --match=security\\.selinux -R .)
  fi
  # Remove our temp directory
  if [ -n "${ACCTTEMP}" ] && [ -d "${ACCTTEMP}" ] && [ "${ACCTTEMP}" != "/" ]; then
    chmod -R u+w ${ACCTTEMP}
    rm -rf ${ACCTTEMP}
  fi
else
  echo "# Copy ACLs from ${TARGET} ==> ${ACCTHOME}"
  echo "setfacl --restore=<(cd ${TARGET}; getfacl -R .)"
  if [ $SELINUX -eq 1 ]; then
    echo "# Copy SELinux context from ${TARGET} ==> ${ACCTHOME}"
    echo "setfattr --restore=<(cd ${TARGET}; getfattr --dump --match=security\\\\.selinux -R .)"
    echo "# ${TARGET} has been preserved for review, please delete it once it is no longer needed"
  fi
fi


