Test if a directory is writable by a given UID?

Solution 1:

Here's a long, roundabout way of checking.

USER=johndoe
DIR=/path/to/somewhere

# Use -L to get information about the target of a symlink,
# not the link itself, as pointed out in the comments
INFO=( $(stat -L -c "%a %G %U" "$DIR") )
PERM=${INFO[0]}
GROUP=${INFO[1]}
OWNER=${INFO[2]}

ACCESS=no
if (( ($PERM & 0002) != 0 )); then
    # Everyone has write access
    ACCESS=yes
elif (( ($PERM & 0020) != 0 )); then
    # Some group has write access.
    # Is user in that group?
    gs=( $(groups $USER) )
    for g in "${gs[@]}"; do
        if [[ $GROUP == $g ]]; then
            ACCESS=yes
            break
        fi
    done
elif (( ($PERM & 0200) != 0 )); then
    # The owner has write access.
    # Does the user own the file?
    [[ $USER == $OWNER ]] && ACCESS=yes
fi

Solution 2:

That could do the test:

if read -a dirVals < <(stat -Lc "%U %G %A" $directory) && (
    ( [ "$dirVals" == "$wantedUser" ] && [ "${dirVals[2]:2:1}" == "w" ] ) ||
    ( [ "${dirVals[2]:8:1}" == "w" ] ) ||
    ( [ "${dirVals[2]:5:1}" == "w" ] && (
        gMember=($(groups $wantedUser)) &&
        [[ "${gMember[*]:2}" =~ ^(.* |)${dirVals[1]}( .*|)$ ]]
    ) ) )
  then
    echo 'Happy new year!!!'
  fi

Explanations:

There is only one test (if), no loop and no fork.

+ Nota: as I'v used stat -Lc instead of stat -c, this will work for symlinks too!

So condition is if,

  • I could successfully read stats of $directory and assign them to dirVals,
  • And (
    • ( Owner match And Flag UserWriteable is present )
    • or flag Other Writeable is present
    • or ( Flag GroupWriteabe is present AND
      • I could successfully assing member list of $wantedUser to gMember AND
      • A string built by merging fields 2 to last of $gMember will match beginOfSting-Or-something-followed-by-a-space, immediately followed by target's group (${dirVals[1]}), immediately followed by a-space-followed-by-something-Or-endOfString. )

then echo Happy new year!

As the group's test implie a second fork (And I love to reduce as possible such calls), this is the last test to be done.

Old:

Simply:

su - mysql -c "test -w '$directory'" && echo yes
yes

or:

if su - mysql -s /bin/sh -c "test -w '$directory'" ; then 
    echo 'Eureka!'
  fi

Nota: Warn to enclose first with double-quotes for having $directory developped!

Solution 3:

You can use sudo to execute the test in your script. For instance:

sudo -u mysql -H sh -c "if [ -w $directory ] ; then echo 'Eureka' ; fi"

To do this, the user executing the script will need sudo privileges of course.

If you explicitly need the uid instead of the username, you can also use:

sudo -u \#42 -H sh -c "if [ -w $directory ] ; then echo 'Eureka' ; fi"

In this case, 42 is the uid of the mysql user. Substitute your own value if needed.

UPDATE (to support non-sudo-priviledged users)
To get a bash script to change-users without sudu would be to require the ability to suid ("switch user id"). This, as pointed out by this answer, is a security restriction that requires a hack to work around. Check this blog for an example of "how to" work around it (I haven't tested/tried it, so I can't confirm it's success).

My recommendation, if possible, would be to write a script in C that is given permission to suid (try chmod 4755 file-name). Then, you can call setuid(#) from the C script to set the current user's id and either continue code-execution from the C application, or have it execute a separate bash script that runs whatever commands you need/want. This is also a pretty hacky method, but as far as non-sudo alternatives it's probably one of the easiest (in my opinion).

Solution 4:

Because I had to make some changes to @chepner's answer in order to get it to work, I'm posting my ad-hoc script here for easy copy & paste. It's a minor refactoring only, and I have upvoted chepner's answer. I'll delete mine if the accepted answer is updated with these fixes. I have already left comments on that answer pointing out the things I had trouble with.

I wanted to do away with the Bashisms so that's why I'm not using arrays at all. The ((arithmetic evaluation)) is still a Bash-only feature, so I'm stuck on Bash after all.

for f; do
    set -- $(stat -Lc "0%a %G %U" "$f")
    (("$1" & 0002)) && continue
    if (("$1" & 0020)); then
        case " "$(groups "$USER")" " in *" "$2" "*) continue ;; esac
    elif (("$1" & 0200)); then
        [ "$3" = "$USER" ] && continue
    fi
    echo "$0: Wrong permissions" "$@" "$f" >&2
done

Without the comments, this is even fairly compact.

Solution 5:

One funny possibility (but it's not bash anymore) is to make a C program with the suid flag, owned by mysql.

Step 1.

Create this wonderful C source file, and call it caniwrite.c (sorry, I've always sucked at choosing names):

#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc,char* argv[]) {
   int i;
   for(i=1;i<argc;++i) {
      if(eaccess(argv[i],W_OK)) {
         return EXIT_FAILURE;
      }
   }
   return EXIT_SUCCESS;
}

Step 2.

Compile:

gcc -Wall -ocaniwrite caniwrite.c

Step 3.

Move it in whatever folder you like, /usr/local/bin/ being a good choice, change it's ownership and set the suid flag: (do this as root)

# mv -nv caniwrite /usr/local/bin
# chown mysql:mysql /usr/local/bin/caniwrite
# chmod +s /usr/local/bin/caniwrite

Done!

Just call it as:

if caniwrite folder1; then
    echo "folder1 is writable"
else
    echo "folder1 is not writable"
fi

In fact, you can call caniwrite with as many arguments as you wish. If all the directories (or files) are writable, then the return code is true, otherwise the return code is false.