Skip to content

Instantly share code, notes, and snippets.

@QNimbus
Last active February 20, 2020 15:04
Show Gist options
  • Save QNimbus/8680cade5ac68e87f6308e4c72463a03 to your computer and use it in GitHub Desktop.
Save QNimbus/8680cade5ac68e87f6308e4c72463a03 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
##
## Title: acl_util.sh
## Description: Shell script to recursively set ACE's on FreeNAS ZFS
## Author: B. van wetten
## Created date: 17-02-2020
## Updated date: 20-02-2020
## Version: 0.4.0
## GitHub Gist: https://gist.github.com/QNimbus/8680cade5ac68e87f6308e4c72463a03
##
## To do: - Bug fix: Problem with interpreting globbing
## Possible resolution is to disable globbing in the future as it's not essential
## To reproduce 'acl_util.sh -d -r -p /mnt/media'
## This results in '/usr/bin/find /mnt -type f -name media' & '/usr/bin/find /mnt -type d -name media'
## - Implement import/export function to easily save and restore existing ACL's
##
## Usage: acl_util.sh
# Shell utilities
ID=$(which id); [[ $? != 0 ]] && echo "Command 'id' not found" >&2 && exit 1
ECHO=$(which echo); [[ $? != 0 ]] && echo "Command 'echo' not found" >&2 && exit 1
FIND=$(which find); [[ $? != 0 ]] && echo "Command 'find' not found" >&2 && exit 1
GREP=$(which grep); [[ $? != 0 ]] && echo "Command 'grep' not found" >&2 && exit 1
CHGRP=$(which chgrp); [[ $? != 0 ]] && echo "Command 'chgrp' not found" >&2 && exit 1
CHOWN=$(which chown); [[ $? != 0 ]] && echo "Command 'chown' not found" >&2 && exit 1
CHMOD=$(which chmod); [[ $? != 0 ]] && echo "Command 'chmod' not found" >&2 && exit 1
GETENT=$(which getent); [[ $? != 0 ]] && echo "Command 'getent' not found" >&2 && exit 1
GETOPT=$(which getopt); [[ $? != 0 ]] && echo "Command 'getopt' not found" >&2 && exit 1
SETFACL=$(which setfacl); [[ $? != 0 ]] && echo "Command 'setfacl' not found" >&2 && exit 1
# Initialize variables
_ME=$(basename "${0}")
_PWD=$(pwd)
_SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
_WIDTH=${COLUMNS:-150}
_USERS=()
_GROUPS=()
# Option strings
SHORT=hvxugsptdraq
LONG=help,verbose,strip-executable,user,group,strip,path,template,dry-run,recursive,append,quiet
# Get command line arguments
if [[ "${#}" -gt 0 ]]
then
getopt -T > /dev/null
if [ $? -eq 4 ]; then
# GNU enhanced getopt is available
_ARGS=$(${GETOPT} --name "$_ME" --long ${LONG} --options ${SHORT} -- "$@")
else
# Original getopt is available (no long option names, no whitespace, no sorting)
_ARGS=$(${GETOPT} ${SHORT} "$@")
fi
if [ $? -ne 0 ]; then
echo "$_ME: usage error (use -h for help)" >&2
exit 2
fi
fi
###############################################################################
# ACL Templates #
###############################################################################
declare -A TEMPLATES_DIR
declare -A TEMPLATES_FILE
# Template names are case-insensitive (e.g. '-t HOME' is the same as '-t Home' or '-t home')
TEMPLATES_DIR=(
# 0755
[DEFAULT_OWNER]="rwxpDdaARWcCos"
[DEFAULT_GROUP]="r-x---a-R-c---"
[DEFAULT_OTHER]="r-x---a-R-c---"
# 0777 - Similar to FreeNAS ACL preset 'OPEN'
[OPEN_OWNER]="rwxpDdaARWcCos"
[OPEN_GROUP]="rwxpDdaARWc---"
[OPEN_OTHER]="rwxpDdaARWc---"
# 0770 - Similar to FreeNAS ACL preset 'RESTRICTED'
[RESTRICTED_OWNER]="rwxpDdaARWcCos"
[RESTRICTED_GROUP]="rwxpDdaARWc---"
[RESTRICTED_OTHER]="------a-R-c---"
# 0700 - Similar to FreeNAS ACL preset 'HOME'
[HOME_OWNER]="rwxpDdaARWcCos"
[HOME_GROUP]="------a-R-c---"
[HOME_OTHER]="------a-R-c---"
# 0700 - Invisible to anyone but owner
[PRIVATE_OWNER]="rwxpDdaARWcCos"
[PRIVATE_GROUP]="--------------"
[PRIVATE_OTHER]="--------------"
# 0750 - Invisible to anyone but owner and group
[GROUP_PRIVATE_OWNER]="rwxpDdaARWcCos"
[GROUP_PRIVATE_GROUP]="r-x---a-R-c---"
[GROUP_PRIVATE_OTHER]="--------------"
)
TEMPLATES_FILE=(
# 0644
[DEFAULT_OWNER]="rw-pDdaARWcCos"
[DEFAULT_GROUP]="r-----a-R-c---"
[DEFAULT_OTHER]="r-----a-R-c---"
# 0666 - Similar to FreeNAS ACL preset 'OPEN'
[OPEN_OWNER]="rw-pDdaARWcCos"
[OPEN_GROUP]="rw-pDdaARWc---"
[OPEN_OTHER]="rw-pDdaARWc---"
# 0660 - Similar to FreeNAS ACL preset 'RESTRICTED'
[RESTRICTED_OWNER]="rw-pDdaARWcCos"
[RESTRICTED_GROUP]="rw-pDdaARWc---"
[RESTRICTED_OTHER]="------a-R-c---"
# 0600 - Similar to FreeNAS ACL preset 'HOME'
[HOME_OWNER]="rw-pDdaARWcCos"
[HOME_GROUP]="------a-R-c---"
[HOME_OTHER]="------a-R-c---"
# 0600 - Invisible to anyone but owner
[PRIVATE_OWNER]="rw-pDdaARWcCos"
[PRIVATE_GROUP]="--------------"
[PRIVATE_OTHER]="--------------"
# 0640 - Invisible to anyone but owner and group
[GROUP_PRIVATE_OWNER]="rw-pDdaARWcCos"
[GROUP_PRIVATE_GROUP]="r-----a-R-c---"
[GROUP_PRIVATE_OTHER]="--------------"
)
###############################################################################
# Functions #
###############################################################################
# _print_usage()
#
# Usage:
# _print_usage
#
# Print the program usage information.
function _print_usage() {
cat <<HEREDOC
____ ____ _ _ _ ___ _ _
|__| | | | | | | |
| | |___ |___ ___ |__| | | |___
Usage:
${_ME} -h
${_ME} -a [-u user1 user2 user3] [-g group1 group2]
${_ME} [-d] [-r] [-p path] [-t template]
${_ME} [-d] [-s] [-p path]
${_ME} [-v] [-p path] [-t template]
Example:
${_ME} -d -r -u root -g wheel -t DEFAULT -p /root
${_ME} -d -r -a -u bas -g users -t HOME -p /mnt/media
Options:
-h Show this screen
-p Path to process (defaults to current directory)
File globs are possible when used with the '-r' flag e.g.
'/home/user/*.log'
'/etc/*.conf'
-a Append user(s)/group(s) to ACL instead of chown/chgp
-u user Change user ownership to user
Used with '-a' will append to ACL
-g group Change group ownership to group
Used with '-a' will append to ACL
-r Perform actions recursively
-x Strip file executable permissions
-s Strip all ACL's
-v Verbose output
-d Dry-run with verbose output
-q Quiet mode - no ouput or confirmation
-t Use ACL template (case-insensitive)
Available templates:
- default: Default ACL (0755 & 0644)
- open: Public ACL
- restricted: Restricted ACL user and group writable (0775 & 0664)
- home: Home ACL for use in home directories (0700 & 0600)
- private: Private ACL files are invisible to anyone but user
- group_private: Private ACL files are invisible to anyone but user & group
HEREDOC
exit ${1:-0}
}
# _set_variable()
#
# Usage:
# _set_variable variable value
#
# Sets the variable to a value if not already set. Otherwise exits with an error message
function _set_variable() {
local varname="$1"
shift
if [ -z "${!varname:-}" ]; then
eval "$varname=\"$@\""
else
echo "Error: $varname already set"
usage
fi
}
# _user_exists()
#
# Usage:
# _user_exists <uid or username>
#
# Returns true or false depending on whether the user exists
function _user_exists() {
# Check if $_USER is set and exist
local _USER="${1:-}"
if [[ -n "${_USER}" ]]
then
${ID} -u "${_USER}" > /dev/null 2>&1
return $?
else
# Return true
return 0
fi
}
# _group_exists()
#
# Usage:
# _group_exists <gid or groupname>
#
# Returns true or false depending on whether the group exists
function _group_exists() {
# Check if $_GROUP is set and exist
local _GROUP="${1:-}"
if [[ -n "${_GROUP}" ]]
then
${GETENT} group "${_GROUP}" > /dev/null 2>&1
return $?
else
# Return true
return 0
fi
}
# _parse_commandline_arguments()
#
# Usage:
# _parse_commandline_arguments
#
# Parses and validates commandline arguments and populates appropriate variables.
function _parse_commandline_arguments() {
while true;
do
case "${1:-}" in
-x|--strip-executable)
_STRIP_EXECUTABLE=true
_SET_EXECUTABLE=false
shift 1
;;
-r|--recursive)
_RECURSIVE=true
shift 1
;;
-a|--append)
_APPEND_USER_GROUP=true
shift 1
;;
-s|--strip)
_STRIP=true
shift 1
;;
-d|--dry-run)
_DRYRUN=true
_VERBOSE=true
shift 1
;;
-v|--verbose)
_VERBOSE=true
shift 1
;;
-q|--quiet)
_QUIET=true
shift 1
;;
-p|--path)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable _PATH "${2}"
_PFLAG=true
shift 2
;;
-t|--template)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable _TEMPLATE $(${ECHO} "${2^^}")
shift 2
;;
-u|--user)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
while
if ! [[ -z "${2}" || "${2}" =~ ^-{1,2} ]]
then
_USERS+=("${2}")
shift 1
fi
! [[ -z "${2}" || "${2}" =~ ^-{1,2} ]]
do
:
done
shift 1
;;
-g|--group)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
while
if ! [[ -z "${2}" || "${2}" =~ ^-{1,2} ]]
then
_GROUPS+=("${2}")
shift 1
fi
! [[ -z "${2}" || "${2}" =~ ^-{1,2} ]]
do
:
done
shift 1
;;
\?) echo "$_ME: Unknown option -$1" >&2;
exit 1
;;
:) echo "$_ME: -$1 needs a value" >&2;
exit 1
;;
*) shift
break
;;
esac
done
# Set defaults:
_DRYRUN="${_DRYRUN:-false}"
_PFLAG="${_PFLAG:-false}"
_STRIP="${_STRIP:-false}"
_QUIET="${_QUIET:-false}"
_VERBOSE="${_VERBOSE:-false}"
_RECURSIVE="${_RECURSIVE:-false}"
_SET_EXECUTABLE="${_SET_EXECUTABLE:-true}"
_STRIP_EXECUTABLE="${_STRIP_EXECUTABLE:-false}"
_APPEND_USER_GROUP="${_APPEND_USER_GROUP:-false}"
}
# _validate_parameters()
#
# Usage:
# _validate_parameters
#
# Performs several checks on the supplied command line arguments
function _validate_parameters() {
# User/Group validation
if [[ "${_APPEND_USER_GROUP}" = true ]]
then
# Verify that at least one user or one group was passed as parameter when appending
[[ ${#_USERS[@]} -lt 1 && ${#_GROUPS[@]} -lt 1 ]] && echo -e "$_ME: Append user and/or group requires at least 1 user or 1 group parameter\n" && _print_usage 1;
# Clear variables since we won't be using them
unset _USER
unset _GROUP
# Verifiy that user(s) is/are valid
for user in "${_USERS[@]}"
do
! _user_exists ${user} && echo -e "$_ME: User '${user}' does not exist\n" && _print_usage 1;
done
# Verifiy that group(s) is/are valid
for group in "${_GROUPS[@]}"
do
! _group_exists ${group} && echo -e "$_ME: Group '${group}' does not exist\n" && _print_usage 1;
done
else
# Verify that at most one user or one group was passed as parameter when changing ownership
[[ ${#_USERS[@]} -gt 1 ]] && echo -e "$_ME: -u expects exactly 1 argument. Maybe you want to run this command recursively with the '-a' option?'\n" && _print_usage 1;
[[ ${#_GROUPS[@]} -gt 1 ]] && echo -e "$_ME: -g expects exactly 1 argument. Maybe you want to run this command recursively with the '-a' option?'\n" && _print_usage 1;
# Set _USER & _GROUP variables
_USER=${_USERS[0]:-}
_GROUP=${_GROUPS[0]:-}
# Verify that user is valid
! _user_exists ${_USER} && echo -e "$_ME: User '${_USER}' does not exist\n" && _print_usage 1;
# Verify that group is valid
! _group_exists ${_GROUP} && echo -e "$_ME: Group '${_GROUP}' does not exist\n" && _print_usage 1;
fi
# Ensure that '-v' and '-q' flag are not both set
[[ "${_VERBOSE}" == true && "${_QUIET}" == true ]] && echo -e "$_ME: Cannot set '-v' and '-q' flag simultaneously\n" && _print_usage 1;
# Check if path was given as argument otherwise set path to current working directory
_PATH=${_PATH:-"${_PWD}"}
# Check if path is a glob
if [[ "${_PATH}" ]]
then
# Get last part (glob) into seperate variable
_GLOB="${_PATH##*/}"
if [[ ! -z "${_PATH##*/*}" ]]
then
# If path does not contain a '/' character assume current folder
_PATH="."
else
# Otherwise get everything up to and including the last '/' character
_PATH="${_PATH%/*}"
fi
fi
# Remove trailing slash unless root
case "${_PATH}" in
*[!/]*/)
_PATH=${_PATH%"${_PATH##*[!/]}"}
;;
*[/])
_PATH="/"
;;
esac
# Check if path argument exists as a directory
[[ ! -d "${_PATH}" || ! -w "${_PATH}" ]] && echo -e "Error: '${_PATH}${_GLOB:+/$_GLOB}' does not exist or current user does not have writing permissions\n" && _print_usage 1;
# If path is relative, make it absolute
[[ ! "${_PATH}" =~ ^\/ ]] && _PATH=$(cd "${_PWD}/${_PATH}"; pwd)
# Check if templates existst
TEMPLATE="${_TEMPLATE:-DEFAULT}"
[[ -z ${TEMPLATES_DIR["${TEMPLATE}_OWNER"]+_} ]] && echo -e "Directory template '${TEMPLATE}_OWNER' not found\n" && _print_usage 1;
[[ -z ${TEMPLATES_DIR["${TEMPLATE}_GROUP"]+_} ]] && echo -e "Directory template '${TEMPLATE}_GROUP' not found\n" && _print_usage 1;
[[ -z ${TEMPLATES_DIR["${TEMPLATE}_OTHER"]+_} ]] && echo -e "Directory template '${TEMPLATE}_OTHER' not found\n" && _print_usage 1;
[[ -z ${TEMPLATES_FILE["${TEMPLATE}_OWNER"]+_} ]] && echo -e "File template '${TEMPLATE}_OWNER' not found\n" && _print_usage 1;
[[ -z ${TEMPLATES_FILE["${TEMPLATE}_GROUP"]+_} ]] && echo -e "File template '${TEMPLATE}_GROUP' not found\n" && _print_usage 1;
[[ -z ${TEMPLATES_FILE["${TEMPLATE}_OTHER"]+_} ]] && echo -e "File template '${TEMPLATE}_OTHER' not found\n" && _print_usage 1;
# If we're doing a dry-run replace the commands
if [[ "${_DRYRUN}" == true ]]
then
SETFACL="${ECHO} |--> ${SETFACL}"
CHOWN="${ECHO} |--> ${CHOWN}"
CHMOD="${ECHO} |--> ${CHMOD}"
CHGRP="${ECHO} |--> ${CHGRP}"
fi
}
# _is_file_executable()
#
# Usage:
# _is_file_executable [user|group|other|any] <file>
#
# Check if a file is readable and executable
function _is_file_executable() {
local is_executable
case "${1:-}" in
owner|user|u)
is_executable="$(${FIND} -L "${2:-}" -type f -perm -u=rx 2> /dev/null)"
[[ $? -eq 0 ]] || exit 1
# Return false
[[ -z "${is_executable}" ]] && return 1
# Return true
return 0
;;
group|g)
is_executable="$(${FIND} -L "${2:-}" -type f -perm -g=rx 2> /dev/null)"
[[ $? -eq 0 ]] || exit 1
# Return false
[[ -z "${is_executable}" ]] && return 1
# Return true
return 0
;;
other|o)
is_executable="$(${FIND} -L "${2:-}" -type f -perm -o=rx 2> /dev/null)"
[[ $? -eq 0 ]] || exit 1
# Return false
[[ -z "${is_executable}" ]] && return 1
# Return true
return 0
;;
any|a|*)
is_executable="$(${FIND} -L "${2:-}" -type f \( -perm -u=rx -o -perm -g=rx -o -perm -o=rx \) 2> /dev/null)"
[[ $? -eq 0 ]] || exit 1
# Return false
[[ -z "${is_executable}" ]] && return 1
# Return true
return 0
;;
esac
}
# _print_separator()
#
# Usage:
# _print_separator
#
# Prints a line separator to the output
function _print_separator() {
local _row
printf -v _row "%${_WIDTH}s"; echo ${_row// /-}
}
# _progress_bar()
#
# Usage:
# _progress_bar <current_value> <total_value> [label]
#
# Prints a progress bar
# e.g. Progress : [##############--------------------------] 35%
function _progress_bar() {
local _progress
local _done
local _left
local _label=${3:-Progress:}
let _progress=(${1}*100/${2}*100)/100
let _done=(${_progress}*4)/10
let _left=40-$_done
local _fill=$(printf "%${_done}s")
local _empty=$(printf "%${_left}s")
# Clear line
printf "\r%${_WIDTH}s"
# Print progress bar
printf "\r%-25s [${_fill// /#}${_empty// /-}] ${_progress}%%" "${_label}"
}
###############################################################################
# Main #
###############################################################################
# _main()
#
# Usage:
# _main [<options>] [<arguments>]
#
# Description:
# Entry point for the program, handling basic option parsing and dispatching.
_main() {
# Avoid complex option parsing when only one program option is expected.
if [[ "${@:-}" =~ -h|--help ]]
then
_print_usage
else
_parse_commandline_arguments "$@"
_validate_parameters
if [[ "${_RECURSIVE}" == true ]]
then
_FILES=$(${FIND} "${_PATH}" -type f -name "${_GLOB:-*}")
_FOLDERS=$(${FIND} "${_PATH}" -type d -name "${_GLOB:-*}")
else
[[ -f "${_PATH}/${_GLOB:-}" ]] && _FILES="${_PATH}/${_GLOB:-}"
[[ -d "${_PATH}/${_GLOB:-}" ]] && _FOLDERS="${_PATH}/${_GLOB:-}"
fi
_FILE_COUNT=$([[ ! -z "${_FILES}" ]] && printf '%s\0' "${_FILES}" | grep -c '^')
_FOLDER_COUNT=$([[ ! -z "${_FOLDERS}" ]] && printf '%s\0' "${_FOLDERS}" | grep -c '^')
_FILE_COUNT=${_FILE_COUNT:-0}
_FOLDER_COUNT=${_FOLDER_COUNT:-0}
# If no directories of files matched display message and exit
if [[ "${_FILE_COUNT}" -eq 0 && "${_FOLDER_COUNT}" -eq 0 ]]
then
[[ "${_RECURSIVE}" == true && "${_VERBOSE}" == true ]] && echo -e "No directories or files matched '${_PATH}/${_GLOB}'\n"
[[ "${_RECURSIVE}" == false && "${_VERBOSE}" == true ]] && echo -e "File or directory '${_PATH}/${_GLOB}' not found. Maybe you want to run this command recursively with the '-r' option?'\n"
exitcode=1
return
fi
# No explicit path or glob was given - display prompt for confirmation
if [[ "${_DRYRUN}" == false && "${_QUIET}" == false ]]
then
[[ "${_PFLAG:-false}" == false ]] && echo -e "*** Warning ***: No explicit path provided. Double-check path below!\n"
echo -e "Effective path: ${_PATH}/${_GLOB}"
echo -e "${_FILE_COUNT} files will be modified"
echo -e "${_FOLDER_COUNT} folders will be modified\n"
[[ "${_RECURSIVE}" == true ]] && read -p "This will recursively modify ACL's in '${_PATH}/${_GLOB}' directory. Do you want to continue? " -n 1 -r REPLY
[[ "${_RECURSIVE}" == false ]] && read -p "This will modify the ACL of '${_PATH}/${_GLOB}'. Do you want to continue? " -n 1 -r REPLY
echo
if [[ ! "${REPLY:-n}" =~ ^[Yy]$ ]]
then
exitcode=0
return
fi
fi
# Output line separator unless in quiet mode
[[ "${_QUIET}" == false ]] && _print_separator
# Counter for ACL rule index
counter=0
[[ ! -z "${_FOLDERS}" ]] && while read FOLDER
do
# Output progress bar
if [[ "${_VERBOSE}" == false && "${_QUIET}" == false ]]
then
_progress_bar $((counter=counter+1)) "${_FOLDER_COUNT}" "Processing folders:"
fi
# Double check we are not following symlinked folders
if [[ -L "${FOLDER}" ]]
then
[[ "${_VERBOSE}" == true ]] && printf "%-32s %s\n" "Skipping directory (no symlinks)" "${FOLDER}"
continue
fi
[[ "${_VERBOSE}" == true ]] && printf "%-32s %s\n" "Processing directory" "${FOLDER}"
# Clear custom ACE's
${SETFACL} -bn "${FOLDER}"
# Only apply ACL's if '-s' option was not set
if [[ "${_STRIP}" == false ]]
then
# Counter for ACL rule index
rule_index=-1
# Remove existing entries
while [[ "${_DRYRUN}" == false ]] && ${SETFACL} -x 0 "${FOLDER}" 2> /dev/null
do
continue
done
# Owner permissions
${SETFACL} -a $((rule_index=rule_index+1)) owner@:"${TEMPLATES_DIR["${TEMPLATE}_OWNER"]}":d:allow "${FOLDER}"
${SETFACL} -a $((rule_index=rule_index+1)) owner@:"${TEMPLATES_FILE["${TEMPLATE}_OWNER"]}":f:allow "${FOLDER}"
# Do we need to add users?
if [[ "${_APPEND_USER_GROUP}" == true ]]
then
# Loop through users that need to be appended
for user in "${_USERS[@]}"
do
${SETFACL} -a $((rule_index=rule_index+1)) user:"${user}":"${TEMPLATES_DIR["${TEMPLATE}_OWNER"]}":d:allow "${FOLDER}"
${SETFACL} -a $((rule_index=rule_index+1)) user:"${user}":"${TEMPLATES_FILE["${TEMPLATE}_OWNER"]}":f:allow "${FOLDER}"
done
fi
# Group permissions
${SETFACL} -a $((rule_index=rule_index+1)) group@:"${TEMPLATES_DIR["${TEMPLATE}_GROUP"]}":d:allow "${FOLDER}"
${SETFACL} -a $((rule_index=rule_index+1)) group@:"${TEMPLATES_FILE["${TEMPLATE}_GROUP"]}":f:allow "${FOLDER}"
# Do we need to add groups?
if [[ "${_APPEND_USER_GROUP}" == true ]]
then
# Loop through groups that need to be appended
for group in "${_GROUPS[@]}"
do
${SETFACL} -a $((rule_index=rule_index+1)) group:"${group}":"${TEMPLATES_DIR["${TEMPLATE}_GROUP"]}":d:allow "${FOLDER}"
${SETFACL} -a $((rule_index=rule_index+1)) group:"${group}":"${TEMPLATES_FILE["${TEMPLATE}_GROUP"]}":f:allow "${FOLDER}"
done
fi
# Other permissions
${SETFACL} -a $((rule_index=rule_index+1)) everyone@:"${TEMPLATES_DIR["${TEMPLATE}_OTHER"]}":d:allow "${FOLDER}"
${SETFACL} -a $((rule_index=rule_index+1)) everyone@:"${TEMPLATES_FILE["${TEMPLATE}_OTHER"]}":f:allow "${FOLDER}"
# Cleanup remaining entries
while [[ "${_DRYRUN}" == false ]] && ${SETFACL} -x $((rule_index=rule_index+1)) "${FOLDER}" 2> /dev/null
do
continue
done
fi
# Change ownership if user and/or group parameter were used
[[ -n "${_USER_ID}" ]] && ${CHOWN} "${_USER_ID}" "${FOLDER}"
[[ -n "${_GROUP_ID}" ]] && ${CHGRP} "${_GROUP_ID}" "${FOLDER}"
done <<< "${_FOLDERS}"
# Continue on next line
[[ ! -z "${_FOLDERS}" && "${_QUIET}" == false ]] && printf "\n"
# Counter for ACL rule index
counter=0
[[ ! -z "${_FILES}" ]] && while read FILE
do
# Output progress bar
if [[ "${_VERBOSE}" == false && "${_QUIET}" == false ]]
then
_progress_bar $((counter=counter+1)) "${_FILE_COUNT}" "Processing files:"
fi
# Double check we are not following symlinked files
if [[ -L "${FILE}" ]]
then
[[ "${_VERBOSE}" == true ]] && printf "%-32s %s\n" "Skipping file (no symlinks)" "${FILE}"
continue
fi
# Do not modify this scripts ACL's by accident
# True if $FILE and $_SELF refer to the same device and inode numbers.
if [[ "${FILE}" -ef "${_SELF:-}" ]]
then
[[ "${_VERBOSE}" == true ]] && printf "%-32s %s\n" "Skipping file (self)" "${FILE}"
continue
fi
[[ "${_VERBOSE}" == true ]] && printf "%-32s %s\n" "Processing file" "${FILE}"
# Reset values
_OWNER_EXECUTABLE=false
_GROUP_EXECUTABLE=false
_OTHER_EXECUTABLE=false
_ANY_EXECUTABLE=false
# Logic checks whether file is originaly executable (i.e. has 'r+x' attribute set for respective permission group)
# If the '-x' flag is used then file is considered executable if ANY permission group had 'r+x' set
[[ "${_STRIP_EXECUTABLE}" == false ]] && _is_file_executable owner "${FILE}" && _OWNER_EXECUTABLE=true
[[ "${_STRIP_EXECUTABLE}" == false ]] && _is_file_executable group "${FILE}" && _GROUP_EXECUTABLE=true
[[ "${_STRIP_EXECUTABLE}" == false ]] && _is_file_executable other "${FILE}" && _OTHER_EXECUTABLE=true
[[ "${_SET_EXECUTABLE}" == true ]] && _is_file_executable any "${FILE}" && _ANY_EXECUTABLE=true
# Clear custom ACE's
${SETFACL} -bn "${FILE}"
# Only apply ACL's if '-s' option was not set
if [[ "${_STRIP}" == false ]]
then
# Counter for ACL rule index
rule_index=-1
# Remove existing entries
while [[ "${_DRYRUN}" == false ]] && ${SETFACL} -x 0 "${FILE}" 2> /dev/null
do
continue
done
# Owner permissions
if [[ "${_ANY_EXECUTABLE}" == true || "${_OWNER_EXECUTABLE}" == true ]] && [[ ${TEMPLATES_FILE["${TEMPLATE}_OWNER"]:0:1} =~ ^r$ ]]
then
${SETFACL} -a $((rule_index=rule_index+1)) owner@:"${TEMPLATES_FILE["${TEMPLATE}_OWNER"]:0:2}x${TEMPLATES_FILE["${TEMPLATE}_OWNER"]:4}"::allow "${FILE}"
# Do we need to add users?
if [[ "${_APPEND_USER_GROUP}" == true ]]
then
# Loop through users that need to be appended
for user in "${_USERS[@]}"
do
${SETFACL} -a $((rule_index=rule_index+1)) user:"${user}":"${TEMPLATES_FILE["${TEMPLATE}_OWNER"]:0:2}x${TEMPLATES_FILE["${TEMPLATE}_OWNER"]:4}"::allow "${FILE}"
done
fi
else
${SETFACL} -a $((rule_index=rule_index+1)) owner@:"${TEMPLATES_FILE["${TEMPLATE}_OWNER"]}"::allow "${FILE}"
# Do we need to add users?
if [[ "${_APPEND_USER_GROUP}" == true ]]
then
# Loop through users that need to be appended
for user in "${_USERS[@]}"
do
${SETFACL} -a $((rule_index=rule_index+1)) user:"${user}":"${TEMPLATES_FILE["${TEMPLATE}_OWNER"]}"::allow "${FILE}"
done
fi
fi
# Group permissions
if [[ "${_ANY_EXECUTABLE}" == true || "${_GROUP_EXECUTABLE}" == true ]] && [[ ${TEMPLATES_FILE["${TEMPLATE}_GROUP"]:0:1} =~ ^r$ ]]
then
${SETFACL} -a $((rule_index=rule_index+1)) group@:"${TEMPLATES_FILE["${TEMPLATE}_GROUP"]:0:2}x${TEMPLATES_FILE["${TEMPLATE}_GROUP"]:4}"::allow "${FILE}"
# Do we need to add groups?
if [[ "${_APPEND_USER_GROUP}" == true ]]
then
# Loop through groups that need to be appended
for group in "${_GROUPS[@]}"
do
${SETFACL} -a $((rule_index=rule_index+1)) group:"${group}":"${TEMPLATES_FILE["${TEMPLATE}_GROUP"]:0:2}x${TEMPLATES_FILE["${TEMPLATE}_GROUP"]:4}"::allow "${FILE}"
done
fi
else
${SETFACL} -a $((rule_index=rule_index+1)) group@:"${TEMPLATES_FILE["${TEMPLATE}_GROUP"]}"::allow "${FILE}"
# Do we need to add groups?
if [[ "${_APPEND_USER_GROUP}" == true ]]
then
# Loop through groups that need to be appended
for group in "${_GROUPS[@]}"
do
${SETFACL} -a $((rule_index=rule_index+1)) group:"${group}":"${TEMPLATES_FILE["${TEMPLATE}_GROUP"]}"::allow "${FILE}"
done
fi
fi
# Other permissions
if [[ "${_ANY_EXECUTABLE}" == true || "${_OTHER_EXECUTABLE}" == true ]] && [[ ${TEMPLATES_FILE["${TEMPLATE}_OTHER"]:0:1} =~ ^r$ ]]
then
${SETFACL} -a $((rule_index=rule_index+1)) everyone@:"${TEMPLATES_FILE["${TEMPLATE}_OTHER"]:0:2}x${TEMPLATES_FILE["${TEMPLATE}_OTHER"]:4}"::allow "${FILE}"
else
${SETFACL} -a $((rule_index=rule_index+1)) everyone@:"${TEMPLATES_FILE["${TEMPLATE}_OTHER"]}"::allow "${FILE}"
fi
# Cleanup remaining entries
while [[ "${_DRYRUN}" == false ]] && ${SETFACL} -x $((rule_index=rule_index+1)) "${FILE}" 2> /dev/null
do
continue
done
fi
# Change ownership if user and/or group parameter were used
[[ -n "${_USER_ID}" ]] && ${CHOWN} "${_USER_ID}" "${FILE}"
[[ -n "${_GROUP_ID}" ]] && ${CHGRP} "${_GROUP_ID}" "${FILE}"
done <<< "${_FILES}"
# Continue on next line
[[ ! -z "${_FILES}" && "${_QUIET}" == false ]] && printf "\n"
# Output summary in verbose mode
[[ "${_QUIET}" == false ]] && _print_separator
[[ "${_QUIET}" == false ]] && printf "%-22s %11s\n" "Files processed:" "${_FILE_COUNT}"
[[ "${_QUIET}" == false ]] && printf "%-22s %11s\n\n" "Directories processed:" "${_FOLDER_COUNT}"
fi
}
# Call `_main` after everything has been defined.
_main "$@"
exit ${exitcode:-0}
@QNimbus
Copy link
Author

QNimbus commented Feb 19, 2020

NFSv4 ACL helper script

This script aims to make managing NFSv4 ACL permissions easier using the CLI. I use this script on my FreeNAS (FreeBSD) server to quickly and easily manage my permissions.

Usage information is displayed with the -h flag:

$ ./acl_util.sh -h

____ ____ _        _  _ ___ _ _
|__| |    |        |  |  |  | |
|  | |___ |___ ___ |__|  |  | |___

Usage:
  acl_util.sh -h
  acl_util.sh -a [-u user1 user2 user3] [-g group1 group2]
  acl_util.sh [-d] [-r] [-p path] [-t template]
  acl_util.sh [-d] [-s] [-p path]
  acl_util.sh [-v] [-p path] [-t template]

Example:
  acl_util.sh -d -r -u root -g wheel -t DEFAULT -p /root
  acl_util.sh -d -r -a -u bas -g users -t HOME -p /mnt/media

Options:
  -h        Show this screen
  -p        Path to process (defaults to current directory)
            File globs are possible when used with the '-r' flag e.g.
            '/home/user/*.log'
            '/etc/*.conf'
  -a        Append user(s)/group(s) to ACL instead of chown/chgp
  -u user   Change user ownership to user
            Used with '-a' will append to ACL
  -g group  Change group ownership to group
            Used with '-a' will append to ACL
  -r        Perform actions recursively
  -x        Strip file executable permissions
  -s        Strip all ACL's
  -v        Verbose output
  -d        Dry-run with verbose output
  -t        Use ACL template (case-insensitive)

  Available templates:

    - default:        Default ACL (0755 & 0644)
    - open:           Public ACL
    - restricted:     Restricted ACL user and group writable (0775 & 0664)
    - home:           Home ACL for use in home directories (0700 & 0600)
    - private:        Private ACL files are invisible to anyone but user
    - group_private:  Private ACL files are invisible to anyone but user & group

⚠️ To prevent accidental ACL changes: Always perform dry-run before actual run to verify correctness of command!

⚠️ Bug/Issue: At the moment path globbing is not working exactly as intended - to (recursively) select a directory you need to specify the path with a trailing slash (e.g. /mnt/myfolder/)

Common commands:

$ ./acl_util.sh -d -p /mnt/storage/home -t HOME

Command will perform a dry-run. Will output commands to set permission of /mnt/storage/home according to the HOME template.

$ ./acl_util.sh -d -r -p /mnt/storage/home -t HOME

The same command as above only this will run recursively for the entire /mnt/storage/home directory

$ ./acl_util.sh -d -r -x -p /mnt/storage/scripts -t DEFAULT

This command will perform a dry-run. The -x flag is used to remove the executable bit on files that are executable.

$ ./acl_util.sh -d -r -s -p /mnt/storage/scripts

This command will perform a dry-run. This will strip all existing ACL's recursively.

$ ./acl_util.sh -d -u bas -g users

This command will perform a dry-run. This will set the ownership of the current directory to bas:users

$ ./acl_util.sh -d -a -u bas www-data

This command will perform a dry-run. Appends the users bas and www-data to the ACL with the same permissions as the owner of the file/directory.

$ ./acl_util.sh -d -a -g users www-data

This command will perform a dry-run. Appends the groups users and www-data to the ACL with the same permissions as the group of the file/directory.

$ ./acl_util.sh -d -a -u bas smbuser -g users

Combination of the previous two commands - adds both users bas and smbuser and group users to the ACL with respective permissions.

To actually perform the above commands remove the -d flag or replace it with the verbose (-v) flag for a little more verbosity.

@QNimbus
Copy link
Author

QNimbus commented Feb 20, 2020

Example commands for personal use

The public SMB share is configured like this:

$ /mnt/storage/scripts/utils/acl_util.sh -r -u nobody -g nogroup -p /mnt/vault/smb/public -t OPEN

The dev SMB share is configured like this:

$ /mnt/storage/scripts/utils/acl_util.sh -r -a -u smbuser -p /mnt/vault/smb/dev/ -t RESTRICTED

The media SMB share is configured like this:

$ /mnt/storage/scripts/utils/acl_util.sh -r -a -u smbuser -p /mnt/vault/smb/media/ -t RESTRICTED

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment