Script to set ACLs recursively

PetrZ

Dabbler
Joined
Feb 23, 2018
Messages
20
I didn't found simple way to set ACLs using provided tools, so I wrote basic script.
Feel free to use it, but there is no guarantee. :)

It is using bash and I run it with redirected stderr:
./acl_set.sh 2>error.log

Code:
#!/usr/bin/env bash

export ROOT_DIR="/mnt/DATA"

ACL_RW_FILE='rw-p-daARWc---:-------:allow';
ACL_RW_DIR='rwxp-daARWc---:fd-----:allow';
ACL_RO_FILE='r-----a-R-c---:-------:allow';
ACL_RO_DIR='r-x---a-R-c---:fd-----:allow';

# ACLs set by winacl as default will be deleted
read -r -d '' ACL_DEL_FILE <<'ACL'
   group@:rwxpDdaARWcCos:------I:allow
everyone@:r-x---a-R-c---:------I:allow
ACL

read -r -d '' ACL_DEL_DIR <<'ACL'
   group@:rwxpDdaARWcCos:fd----I:allow
everyone@:r-x---a-R-c---:fd----I:allow
ACL

CUR_DIR=`pwd`

# In case of harld links, setfacl fails as already
# removed ACLs can not be removed again. Therefore
# it is needed to check if we can skip this error.
function acl_check {
    TYPE="${1}"
    OBJ="${2}"

    # check if delete was done already
    if [[ ! -z "$(getfacl "${OBJ}" | grep -E $(cut -d':' -f1 "/tmp/acl_delete_${TYPE}.def" | sed -e 's/^\(.*\)$/(\1)|/' | tr -d "\n" | sed -e 's/\(.*\)|/\1/'))" ]]; then
        return 1;
    fi
 
    # check if add was done already
    if [[ -z "$(getfacl "${OBJ}" | grep -Eoz $(sed -e 's/$/\\s*/g' /tmp/acl_add.def | tr -d "\n" | sed -e 's/\(.*\)\\s\*/\1/'))" ]]; then
        return 1;
    fi

    return 0
}

# Set ACL for single file / dir
function acl_set {
    TYPE="${1}"
    DIR="${2}"
    FILE="${3}"

    set +e
    { ERR=$(setfacl -h -M "/tmp/acl_add.def" -X "/tmp/acl_delete_${TYPE}.def" "${DIR}/${FILE}" 2>&1 1>&$out); } {out}>&1;
    STATUS=$?;
    set -e

    # Check if error is "skippable", log to stderr
    if [ $STATUS -ne 0 ]; then
        printf "Set ACL for %s failed, check ACL" "${FILE}"  >&2
        if acl_check "${TYPE}" "${DIR}/${FILE}"; then
            echo ... passed >&2
        else
            echo ... failed >&2
            echo Command: setfacl -h -M "/tmp/acl_add.def" -X "/tmp/acl_delete_${TYPE}.def" "${FILE}" >&2
            echo Result: "${ERR}" >&2
            ERR_COUNT=$(( ERR_COUNT + 1))
            return 1
        fi
    fi
}

# Set ACLs recursively. When single is not empty, apply without recursive.
# type d|f, acl rule, dir, single
function acl_walk {
    TYPE="${1}"
    ACL="${2}"
    DIR="${3}"
    SINGLE="${4}"

    # Write ACLs to be added
    printf "${ACL}" >/tmp/acl_add.def

    cd "${DIR}"
    if [[ "${TYPE}" == "d" ]]; then
        acl_set "${TYPE}" "${DIR}" ""
    fi
 
    if [[ ! -z "${SINGLE}" ]]; then
        return 0;
    fi

    TOTAL=$(find . -type "${TYPE}" ! -name . ! -name .windows | wc -l)
    COUNT=0

    find . -type "${TYPE}" ! -name . ! -name .windows -print0 | while IFS= read -r -d '' FILE; do
        COUNT=$(( COUNT + 1))
        printf '\r%9d / %d, Errors %d' "${COUNT}" "${TOTAL}" "${ERR_COUNT}"
        acl_set "${TYPE}" "${DIR}" "${FILE}"
    done
    echo
}

# Print count of detected errors on exit
function errors {
    printf '\nDetected %d errors\n' "${ERR_COUNT}"
}

set -e
ERR_COUNT=0
trap errors EXIT

echo "${ACL_DEL_FILE}" >/tmp/acl_delete_f.def
echo "${ACL_DEL_DIR}" >/tmp/acl_delete_d.def


# I use it this way
echo "Set owner and default for whole share"
winacl -O root -G wheel -a reset -r -p "${ROOT_DIR}/your_share"

echo "Set RO for Employer for your_share top level"
acl_walk d "group:Employer:${ACL_RO_DIR}" "${ROOT_DIR}/your_share" single

echo "Set RW for Employer for following subfolders"
for sub in Public General Misc; do
    echo "   /${sub} - directories"
    acl_walk d "group:Employer:${ACL_RW_DIR}" "${ROOT_DIR}/your_share/${sub}"
    echo "   /${sub} - files"
    acl_walk f "group:Employer:${ACL_RW_FILE}" "${ROOT_DIR}/your_share/${sub}"
done

echo "Set RW for Finances for Accounts"
echo "   - directories"
acl_walk d "group:Finances:${ACL_RW_DIR}" "${ROOT_DIR}/your_share/Accounts"
echo "   - files"
acl_walk f "group:Finances:${ACL_RW_FILE}" "${ROOT_DIR}/your_share/Accounts"

# Other share will have RO and RW for different groups
echo "Set owner and default for whole other_share"
winacl -O root -G wheel -a reset -r -p "${ROOT_DIR}/other_share"

echo "Set RO for Media and Mediamanagement for other_share top level"
acl_walk d "group:Media:${ACL_RO_DIR}\ngroup:Mediamanagement:${ACL_RO_DIR}" "${ROOT_DIR}/other_share" single

echo "Set RO for Media, RW for Mediamanagement for following subfolders"
for sub in Documents Video Photos; do
    echo "   /${sub} - directories"
    acl_walk d "group:Media:${ACL_RO_DIR}\ngroup:Mediamanagement:${ACL_RW_DIR}" "${ROOT_DIR}/other_share/${sub}"

    echo "   /${sub} - files"
    acl_walk f "group:Media:${ACL_RO_FILE}\ngroup:Mediamanagement:${ACL_RW_FILE}" "${ROOT_DIR}/other_share/${sub}"

done

echo "Set RW for Media and Mediamanagement for UPLOAD"
echo "   - directories"
acl_walk d "group:Media:${ACL_RW_DIR}\ngroup:Mediamanagement:${ACL_RW_DIR}" "${ROOT_DIR}/other_share/UPLOAD"

echo "   - files"
acl_walk f "group:Media:${ACL_RW_FILE}\ngroup:Mediamanagement:${ACL_RW_FILE}" "${ROOT_DIR}/other_share/UPLOAD"

cd "${CUR_DIR}"
 
Last edited:

m0nkey_

MVP
Joined
Oct 27, 2015
Messages
2,739
#!/usr/local/bin/bash .. hmm, your script is not portable (See: https://rainbowhackerhorse.github.io/bash-Is-Not-sh/) .. would prefer it if you used #!/usr/bin/env bash :)

Also, hopefully with the next release of FreeNAS these kinds of scripts won't be required. Latest release of FreeBSD 11.2 and 12.0 have the recursive option for setfacl.
 

PetrZ

Dabbler
Joined
Feb 23, 2018
Messages
20
Good point. I didn't plan to run it somewhere else, but it's better to use env, agree. I updated post.
I am also looking forward to see recursive option in new releases. I just needed that now. :smile:
I migrated from QNAP and I had to reset different ACLs for more than 500000 files.
 
Top