#!/bin/bash
# Copyright 2018, 2019 eomanis
#
# This file is part of inherit-acl.
#
# inherit-acl is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# inherit-acl is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with inherit-acl. If not, see <http://www.gnu.org/licenses/>.
# Set shell options
set -o pipefail
set -o noclobber
set -o nounset
set -o errexit
# Globals
# The absolute paths to some programs so that the caller hopefully
# cannot spoof them
declare -r programSed="/usr/bin/sed"
declare -r programChown="/usr/bin/chown"
declare -r programChgrp="/usr/bin/chgrp"
declare -r programChmod="/usr/bin/chmod"
declare -r programGetfacl="/usr/bin/getfacl"
declare -r programSetfacl="/usr/bin/setfacl"
declare -r programRealpath="/usr/bin/realpath"
# getDefaultAcl directory
getDefaultAcl () {
local directory="$1"; shift
local defaultAcl
# Read the directory's default ACL
defaultAcl="$("$programGetfacl" --default --omit-header --no-effective --skip-base -- "$directory" 2> /dev/null)" || return 1
if test "" = "$defaultAcl" ; then
# No ACL present
return 0
fi
# Echo the default ACLs in "regular ACL" notation, i.e. without "default:"
# prepended
echo "$defaultAcl"
# Echo the default ACLs in "default ACL" notation, i.e. with "default:"
# prepended
echo "$defaultAcl" | "$programSed" -re 's/^(.+)$/default:\1/'
}
# isWhitelisted userLogin path
#
# Returns with code 0 if the given path is whitelisted by the given
# user's parent directory whitelist
isWhitelisted () {
local userLogin="$1"; shift
local parentDir
parentDir="$(echo "$1" | sed -re 's,/+$,,')"; shift
local whitelistedParentDir
# Return false immediately if the associative array does not contain
# a mapping for the user login
test -v parentDirWhitelist[$userLogin] || return 1
# Return false immediately if the given path is the empty string
# Since trailing slashes are stripped from the path, this also
# denies root
test "" = "$parentDir" && return 1
# Walk upwards the given path's chain of parent directories and test
# if any of them is among the user's whitelisted parent directories
while parentDir="$("$programRealpath" --canonicalize-missing -- "${parentDir}/..")"; do
while read -rs whitelistedParentDir; do
test "" = "$whitelistedParentDir" && continue
test "$whitelistedParentDir" = "$parentDir" && return 0
done < <( echo "${parentDirWhitelist[$userLogin]}" )
# Do not attempt to walk upwards beyond root
test "$parentDir" = "/" && break
done
return 1
}
# No arguments: Print a help message
if test $# -eq 0; then
echo -n \
"Usage:
inherit-acl-run userLogin path [path]...
For each given path, recursively applies the path's parent directory's
- owning user
- owning group
- permissions
- default ACL
Must be run as root and is supposed to be called from its companion
launcher application \"inherit-acl\"; the calling user's login name is
given as the 1st argument.
For more information run \"inherit-acl\" without arguments.
"
exit 1
fi
# Make sure the required programs are available
type "$programSed" &> /dev/null || { echo "ERROR Required program \"${programSed}\" is not available" >&2; exit 1; }
type "$programChown" &> /dev/null || { echo "ERROR Required program \"${programChown}\" is not available" >&2; exit 1; }
type "$programChgrp" &> /dev/null || { echo "ERROR Required program \"${programChgrp}\" is not available" >&2; exit 1; }
type "$programChmod" &> /dev/null || { echo "ERROR Required program \"${programChmod}\" is not available" >&2; exit 1; }
type "$programGetfacl" &> /dev/null || { echo "ERROR Required program \"${programGetfacl}\" is not available" >&2; exit 1; }
type "$programSetfacl" &> /dev/null || { echo "ERROR Required program \"${programSetfacl}\" is not available" >&2; exit 1; }
type "$programRealpath" &> /dev/null || { echo "ERROR Required program \"${programRealpath}\" is not available" >&2; exit 1; }
# Declare some more global variables
configFile="/etc/inherit-acl.conf"
# Set up the configuration
declare -A parentDirWhitelist
# Root may do anything
parentDirWhitelist["root"]='/'
# Source the configuration file if it is available
if test -f "$configFile"; then
# shellcheck source=/dev/null
source "$configFile"
else
echo " INFO Configuration file \"$configFile\" not found or not a file, only root may use this application" >&2
fi
# Read arguments
userLogin="$1"; shift
# Validate arguments
# The user login is only a lookup key for the parent directories
# whitelist map, so no further validation is required
if test "root" != "$userLogin"; then
# Non-root user: Make sure hardlink protection is active
protectedHardlinks="$(< "/proc/sys/fs/protected_hardlinks")"
if test "1" != "$protectedHardlinks"; then
echo "ERROR Hardlink protection is not active on this system, only root may use this application" >&2
exit 1
fi
fi
# Process the given paths
declare -i exitCode=0
declare defaultAcl
while test $# -gt 0; do
exitCode=0
# Get the absolute canonicalized path of the current item
# This also resolves any symlinks involved in the path
item="$("$programRealpath" --canonicalize-missing -- "$1")"; shift
# Some basic sanity checks
if ! test -e "$item"; then
echo " WARN Target does not exist, skipping: \"$item\"" >&2
exitCode=2
continue
fi
# Test if the item is whitelisted for the user
if ! isWhitelisted "$userLogin" "$item"; then
echo " WARN Path not whitelisted for user \"$userLogin\", skipping: \"$item\"" >&2
exitCode=2
continue
fi
# Get the absolute canonicalized path of the item's parent directory
if ! parentDir="$("$programRealpath" --canonicalize-missing -- "${item}/..")" || test "" = "$parentDir"; then
echo " WARN Failed to determine the parent directory, skipping: \"$item\"" >&2
exitCode=2
continue
fi
# Apply the parent's user, group, permissions and ACL
# If the parent does not have ACL, remove them from the item
echo " INFO Processing \"$item\"" >&2
if ! "$programChown" --reference "$parentDir" --recursive --no-dereference -- "$item"; then
echo " WARN There were problems recursively setting the owning user" >&2
exitCode=$(( exitCode | 4 ))
fi
if ! "$programChgrp" --reference "$parentDir" --recursive --no-dereference -- "$item"; then
echo " WARN There were problems recursively setting the owning group" >&2
exitCode=$(( exitCode | 8 ))
fi
if ! "$programChmod" --reference "$parentDir" --recursive -- "$item"; then
echo " WARN There were problems recursively setting the permissions" >&2
exitCode=$(( exitCode | 16 ))
fi
if defaultAcl="$(getDefaultAcl "$parentDir")"; then
if test "$defaultAcl" != ""; then
if ! echo "$defaultAcl" | "$programSetfacl" --recursive --physical --set-file - -- "$item"; then
echo " WARN There were problems recursively setting the ACL" >&2
exitCode=$(( exitCode | 32 ))
fi
else
if ! "$programSetfacl" --recursive --physical --remove-all -- "$item"; then
echo " WARN There were problems recursively removing the ACL" >&2
exitCode=$(( exitCode | 32 ))
fi
fi
else
echo " WARN There were problems reading the default ACL from parent directory \"${parentDir}\"" >&2
exitCode=$(( exitCode | 32 ))
fi
done
exit "$exitCode"