Last active
June 29, 2026 17:50
-
-
Save pgorod/f58b8c8c9a4707ce1d9465d7dd2a74ca to your computer and use it in GitHub Desktop.
Permsplainer explains ownerships and permissions for specific files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| #use this to log debug, enclose blocks you want to examine in 'set -x' and 'set +x' | |
| exec 19>lixo.txt # any unused file descriptor number will do | |
| BASH_XTRACEFD=19 | |
| # USAGE ./permsplainer.sh FILE USER | |
| # Default values: | |
| [ -z $1 ] && FILE=/var/www/html/suitecrm.log || FILE=`realpath "$1"` | |
| [ -z $2 ] && USER=`whoami` || USER=$2 | |
| # Colors | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[0;33m' | |
| NC='\033[0m' # No Color | |
| # $1 is Text to echo, $2 is optional color variable name, $3 is optional width in number of chars | |
| cecho(){ | |
| COLOR=${2} | |
| TABS=${3} | |
| if [[ ! ${!2} ]]; then { COLOR="NC"; TABS=${#1}; } fi | |
| if [ "${3}" -eq "" ] 2>/dev/null; then { TABS=${#1}; } fi | |
| printf "${!COLOR}%-*s%s${NC}" "$TABS" "${1}" | |
| unset COLOR | |
| unset TABS | |
| } | |
| function red { printf "${RED}$@${NC}"; } | |
| function yellow { printf "${YELLOW}$@${NC}"; } | |
| function green { if [ $2 = true ]; then printf "${GREEN}$1${NC}"; else printf "$1"; fi } | |
| function dirTexts(){ | |
| PERMS=$1 | |
| CAN="" | |
| if [ "${PERMS:0:1}" = "r" ]; then CAN="${GREEN}Enumerate${NC}"; else CAN="${RED}not Enumerate${NC}"; fi | |
| if [ "${PERMS:1:1}" = "w" ]; then CAN="$CAN, ${GREEN}Create/Delete${NC}"; else CAN="$CAN, ${RED}not Create/Delete${NC}"; fi | |
| if [ ! "${PERMS:2:1}" = "-" ]; then CAN="$CAN, ${GREEN}Traverse${NC}"; else CAN="$CAN, ${RED}not Traverse${NC}"; fi | |
| printf "$CAN" | |
| } | |
| function fileTexts(){ | |
| PERMS=$1 | |
| CAN="" | |
| if [ "${PERMS:0:1}" = "r" ]; then CAN="${GREEN}Read${NC}"; else CAN="${RED}not Read${NC}"; fi | |
| if [ "${PERMS:1:1}" = "w" ]; then CAN="$CAN, ${GREEN}Write${NC}"; else CAN="$CAN, ${RED}not Write${NC}"; fi | |
| if [ ! "${PERMS:2:1}" = "-" ]; then CAN="$CAN, ${GREEN}Execute${NC}"; else CAN="$CAN, ${RED}not Execute${NC}"; fi | |
| printf "$CAN" | |
| } | |
| function WhichAccess(){ | |
| FILEORDIR=$( [ $DIRPERMSTR = "d" ] && echo -n "dirTexts" || echo -n "fileTexts" ) | |
| [[ "$OWNER_MATCH" = "GREEN" ]] && # [[ ! "${OWNERPERMSTR:2:1}" = "-" ]] && | |
| { WHICH="Owner (${OWNERPERMSTR:0:3}), can $($FILEORDIR ${OWNERPERMSTR:0:3})" ; return; } | |
| [[ "$GROUP_MATCH" = "GREEN" ]] && [[ ! "${GROUPPERMSTR:2:1}" = "-" ]] && | |
| { WHICH="Group member (${GROUPPERMSTR:0:3}), can $($FILEORDIR ${GROUPPERMSTR:0:3})"; return; } | |
| [[ ! "${WORLDPERMSTR:2:1}" = "-" ]] && { WHICH="Other (${WORLDPERMSTR:0:3}), can $($FILEORDIR ${WORLDPERMSTR:0:3})"; return; } | |
| false | |
| } | |
| printf "\nPermsplainer, please explain exactly how user ${RED}$USER${NC} can access ${YELLOW}$FILE${NC}!\n\n" | |
| echo '-------------------------------------------------------------------------------------------------------------' | |
| printf "These are dir/file properties, in all levels leading down to the file, with ${GREEN}relevant${NC} accesses for that user:\n" | |
| echo '-------------------------------------------------------------------------------------------------------------' | |
| echo | |
| IFS=/ | |
| CUR_FILE="" | |
| for item in $FILE; do # iterate directory parts, separated by the IFS which is '/' in this () subshell | |
| CUR_FILE="$CUR_FILE/$item" | |
| CUR_FILE=${CUR_FILE/\/\//\/} # replace two backslashes with one | |
| TAB=$(( ${#FILE} + 2 )) # length of filename + 2 | |
| if [ "$CUR_FILE" = "/" ]; then # for some crazy reason I need a special case to print '/' manually...! | |
| printf "/%-*s" $(( ${#FILE}+1 )) | |
| else | |
| printf "%-*s%s" $TAB $CUR_FILE # equivalent to something like printf '%-50s' $CUR_FILE | |
| fi | |
| # Use -L to get information about the target of a symlink, not the link itself | |
| IFS=' ' | |
| EXTRA_TABS=0 | |
| INFO=( $(stat -c "%a %A %U %G" "$CUR_FILE") ) | |
| PERMS=${INFO[0]} # array separation depends on IFS also, has to be space in this case | |
| PERMS=`printf "%04d" $PERMS` # adjust 755 to 0755 | |
| PERMSTR=${INFO[1]} # drwxr-xr-x | |
| OWNER=${INFO[2]} # username such as root | |
| GROUP=${INFO[3]} # groupname such as www-data | |
| DIRPERMS=${PERMS:0:1} # typically 0, 1 or 2 on dirs for setgid etc | |
| OWNERPERMS=${PERMS:1:1} # 0 to 7 | |
| GROUPPERMS=${PERMS:2:1} # 0 to 7 | |
| WORLDPERMS=${PERMS:3:1} # 0 to 7 | |
| DIRPERMSTR=${PERMSTR:0:1} # d | |
| OWNERPERMSTR=${PERMSTR:1:3} # rwx | |
| GROUPPERMSTR=${PERMSTR:4:3} # rwx | |
| WORLDPERMSTR=${PERMSTR:7:3} # rwx | |
| OWNER_MATCH="NC" | |
| GROUP_MATCH="NC" | |
| WORLD_MATCH="NC" | |
| # User owns file? | |
| OWNER_MATCH=$( [[ "$OWNER" = "$USER" ]] && echo -n "GREEN" || echo -n "NC" ) | |
| if [ "$OWNER_MATCH" = "NC" ]; then | |
| # User belongs to group? | |
| if id -nGz "$USER" | grep -qzxF "$GROUP"; then | |
| GROUP_MATCH="GREEN" | |
| WORLD_MATCH="NC" | |
| EXTRA_TABS=4 | |
| else | |
| WORLD_MATCH="GREEN" | |
| EXTRA_TABS=8 | |
| fi | |
| fi | |
| # for dirs: | |
| if [ $DIRPERMSTR = "d" ] && WhichAccess; then ACCESS="Access as $WHICH."; else ACCESS=""; fi | |
| # for files: | |
| if [ $DIRPERMSTR = "-" ] && WhichAccess; then ACCESS="Access as $WHICH."; fi | |
| printf "Perms: $DIRPERMS" | |
| cecho "$OWNERPERMS" $OWNER_MATCH | |
| cecho "$GROUPPERMS" $GROUP_MATCH | |
| cecho "$WORLDPERMS" $WORLD_MATCH | |
| cecho "|$DIRPERMSTR" | |
| cecho "|$OWNERPERMSTR" $OWNER_MATCH | |
| cecho "|$GROUPPERMSTR" $GROUP_MATCH | |
| cecho "|$WORLDPERMSTR|" $WORLD_MATCH | |
| printf ',' | |
| cecho " Owner "; cecho $OWNER $OWNER_MATCH 9 | |
| cecho " Group "; cecho $GROUP $GROUP_MATCH 9 | |
| printf "\n%-*s---" $(($TAB+14+$EXTRA_TABS)) | |
| printf "\n%-*s\\ " $(($TAB+16+$EXTRA_TABS)) | |
| if [[ $(getfacl -s -p $CUR_FILE) ]] | |
| then | |
| printf "${RED}Has ACL!${NC} Sorry, I don't know how to interpret ACL's..." | |
| else | |
| cecho "$ACCESS" "YELLOW" | |
| fi | |
| printf '\n\n' | |
| done | |
| echo '---------------------------------------------------------------------------------------------------------------------' | |
| printf "Effective permissions, obtained with actual access test, using full path (requires traverse right at all dir levels):\n" | |
| echo '---------------------------------------------------------------------------------------------------------------------' | |
| sudo -u $USER test -r $FILE && printf "User $USER ${GREEN}can read${NC} $FILE\n" || printf "User $USER ${RED}cannot read${NC} $FILE\n" | |
| sudo -u $USER test -w $FILE && printf "User $USER ${GREEN}can write${NC} $FILE\n" || printf "User $USER ${RED}cannot write${NC} $FILE\n" | |
| sudo -u $USER test -x $FILE && printf "User $USER ${GREEN}can execute${NC} $FILE\n" || printf "User $USER ${RED}cannot execute${NC} $FILE\n" | |
| printf '\n' | |
| if [ ! "$1" = "$FILE" ] # check if arg was a relative path, so that realpath used above changed it | |
| then | |
| printf "Since I was given a relative path ($1), I'll try another set of tests from where I am running:\n" | |
| sudo -u $USER test -r $1 && printf "User $USER ${GREEN}can read${NC} $FILE\n" || printf "User $USER ${RED}cannot read${NC} $FILE\n" | |
| sudo -u $USER test -w $1 && printf "User $USER ${GREEN}can write${NC} $FILE\n" || printf "User $USER ${RED}cannot write${NC} $FILE\n" | |
| sudo -u $USER test -x $1 && printf "User $USER ${GREEN}can execute${NC} $FILE\n" || printf "User $USER ${RED}cannot execute${NC} $FILE\n" | |
| printf '\n' | |
| fi | |
| printf '\n' |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's a nice PHP version that Claude just burped out