52

We can test if a directory is writable by the uid of the current process:

if [ -w $directory ] ; then echo 'Eureka!' ; fi

But can anyone suggest a way to test if a directory is writable by some other uid?

My scenario is that I am administering a MySQL Server instance, and I want to change the location of the slow-query log file temporarily. I can do this by executing a MySQL command SET GLOBAL slow_query_log_file='$new_log_filename' and then disable & enable query logging to make mysqld start using that file.

But I'd like my script to check that the uid of the mysqld process has permissions to create that new log file. So I'd like to do something like (pseudocode):

$ if [ -w-as-mysql-uid `basename $new_log_filename` ] ; then echo 'Eureka!' ; fi

But of course that's an imaginary test predicate.

Clarification: I would like a solution that doesn't rely on su because I can't assume the user of my script has su privilege.

codeforester
  • 39,467
  • 16
  • 112
  • 140
Bill Karwin
  • 538,548
  • 86
  • 673
  • 828
  • Related Ruby question: http://stackoverflow.com/questions/13369667/programmatically-test-whether-an-arbitrary-user-has-access-to-an-arbitrary-file – Todd A. Jacobs Dec 31 '12 at 17:04
  • Interestingly, Server Admin.app on OS X has a "permissions inspector" onto which one can drop a user, then select any file, and it will display all the file operations that user can and can not do. No idea how it works, however. You'd think it would be easy. – Phil Frost Dec 31 '12 at 17:14
  • Do you really need such a general solution? Why not require that the directory be owned by mysql and have owner write permission, then just check for that specific case? – Barmar Jan 01 '13 at 10:03
  • @Bamar, I'm trying to develop a tool that will be of general use by the MySQL community, and not everyone runs MySQL as a literal user 'mysql' (even if they should). – Bill Karwin Jan 01 '13 at 19:03

8 Answers8

32

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
codeforester
  • 39,467
  • 16
  • 112
  • 140
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thanks, this is verbose but it would work and it would be more portable to different user environments. – Bill Karwin Dec 31 '12 at 18:32
  • I accepted this answer if only because it's a shell-only solution that doesn't require su or suid. – Bill Karwin Dec 31 '12 at 23:18
  • 3
    Warning about symlinks, as they could be `lrwxrwxrwx` while target is not. – F. Hauri - Give Up GitHub Jan 01 '13 at 09:51
  • @F.Hauri, that's a good thought, `stat -L` can account for that. – Bill Karwin Jan 01 '13 at 19:04
  • stat not available on some UNIX systems (AIX) – Felipe Alvarez Jun 05 '13 at 05:39
  • 3
    I found that I had to change the order of the `stat` options, `stat -Lc "fmt"` pro `stat -cL`. – tripleee Apr 28 '14 at 09:20
  • 5
    Also I bumped into [``unexpected token `&': conditional binary operator expected``](http://stackoverflow.com/questions/14318451/how-to-use-bitwise-operators-in-if-statements) – tripleee Apr 28 '14 at 09:30
  • 1
    And finally `stat -c '%a'` did not print a leading zero for me, so I had to change it to `'0%a'` in order for the arithmetic to work correctly. – tripleee Apr 28 '14 at 09:35
  • @tripleee I edited the script, option `-L` is now before `-c ` – galath Jan 25 '17 at 13:32
  • @galath I could easily have edited that myself, but code edits are discouraged on Stack Overflow, so I left comments instead. But thanks. – tripleee Jan 25 '17 at 13:38
  • Belatedly, here is a reference: https://meta.stackoverflow.com/questions/260245/when-should-i-make-edits-to-code but it's much more lenient about editing code in *anwers* than I remembered. – tripleee Jul 02 '17 at 05:46
  • While this answer was helpful to me, we should note that according to https://unix.stackexchange.com/a/134376/181606 , the actual precedence in which these things are checked is the opposite (owner, then group, world), and only one set of rwx perms will actually get used, the first one applicable. So the answer fails in some weird cases like people who have world access but don't have owner access - they would have to chmod before actually getting access, contrary to what this script will tell you. If I understand all this correctly (which I often doubt!) – pgr Nov 09 '21 at 12:39
10

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!

F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
  • +1 Yes that would work, so this is a correct answer, but I was hoping to avoid `su` because I can't assume the user has su privileges. – Bill Karwin Dec 31 '12 at 17:10
  • So have a look at `stat -c "%U %G %a" $directory` or `stat -c "%u %g %A" $directory` or any variant... Happy new year! – F. Hauri - Give Up GitHub Dec 31 '12 at 17:37
  • What about ACLs? And, wouldn't `$wantedGroup` have to iterate over all the groups `$wantedUser` is in? – Phil Frost Dec 31 '12 at 18:04
  • 1
    @PhilFrost sorry, I was stressed due to the New Year... There is a better work. – F. Hauri - Give Up GitHub Jan 01 '13 at 09:19
  • +1 cool, though you should check in user, group, other order, instead of user, other, group, because a file more "other" permissions than "group" permissions (extremely rare, I know) will fail. For example: `touch foo; chmod 044; [ -w foo ] || echo "I can't write to a file where I don't have permissions but others have"` – Carlos Campderrós Jul 24 '13 at 10:22
  • +1 and thanks, works for me. But one issue: it fails on my `/tmp` (1777) for users different from current user, did not check further, just want to note. – mviereck May 10 '17 at 18:06
8

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).

Community
  • 1
  • 1
newfurniturey
  • 37,556
  • 9
  • 94
  • 102
  • +1 Yes that would work, so this is a correct answer, but I was hoping to avoid `su` because I can't assume the user has su privileges. – Bill Karwin Dec 31 '12 at 17:11
  • what's with the -H, --set-home ? – papo Nov 25 '18 at 12:37
  • I am doing something similar `if ! sudo -u [App_xx] --shell [ -r "$path" -a -w "$path" ]; then ...` then I can take an action/fix permissions as root. – papo Nov 25 '18 at 12:39
7

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.

tripleee
  • 175,061
  • 34
  • 275
  • 318
3

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.

gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
  • LOL thanks for the suggestion but the point of writing my tool as a bash script is to avoid deploying C code! :-) – Bill Karwin Dec 31 '12 at 18:31
  • @BillKarwin Apart from this, I'm pretty sure there are no bash solutions to this problem. There are a few hacks possible, but nothing that will work in 100% of cases (think of ACLs and all the different possibilities to manage groups). Now, the solution I gave is not really a huge C code deployment! it's a trivial wrapper around the `eaccess` call. And it's funny `;-)`. – gniourf_gniourf Dec 31 '12 at 18:48
2

I've written a function can_user_write_to_file which will return 1 if the user passed to it either is the owner of the file/directory, or is member of a group which has write access to that file/directory. If not, the method returns 0.

## Method which returns 1 if the user can write to the file or
## directory.
##
## $1 :: user name
## $2 :: file
function can_user_write_to_file() {
  if [[ $# -lt 2 || ! -r $2 ]]; then
    echo 0
    return
  fi

  local user_id=$(id -u ${1} 2>/dev/null)
  local file_owner_id=$(stat -c "%u" $2)
  if [[ ${user_id} == ${file_owner_id} ]]; then
    echo 1
    return
  fi

  local file_access=$(stat -c "%a" $2)
  local file_group_access=${file_access:1:1}
  local file_group_name=$(stat -c "%G" $2)
  local user_group_list=$(groups $1 2>/dev/null)

  if [ ${file_group_access} -ge 6 ]; then
    for el in ${user_group_list-nop}; do
      if [[ "${el}" == ${file_group_name} ]]; then
        echo 1
        return
      fi
    done
  fi

  echo 0
}

To test it, I wrote a wee test function:

function test_can_user_write_to_file() {
  echo "The file is: $(ls -l $2)"
  echo "User is:" $(groups $1 2>/dev/null)
  echo "User" $1 "can write to" $2 ":" $(can_user_write_to_file $1 $2)
  echo ""
}

test_can_user_write_to_file root /etc/fstab
test_can_user_write_to_file invaliduser /etc/motd
test_can_user_write_to_file torstein /home/torstein/.xsession
test_can_user_write_to_file torstein /tmp/file-with-only-group-write-access

At least from these tests, the method works as intended considering file ownership and group write access :-)

skybert
  • 300
  • 2
  • 6
2

Why not just do something simple like TRY a mkdir on the folder in question. It's more reliable....

    mkdir your_directory/
    [[ $? -ne 0 ]] && echo "fatal" || echo "winner winner chicken dinner.."

OR ?

    # -- run the following commands as the_User_ID
    sudo su - the_User_ID << BASH

    mkdir your_directory/
    [[ $? -ne 0 ]] && echo "fatal" || echo "winner winner chicken dinner.."

    BASH
Mike Q
  • 6,716
  • 5
  • 55
  • 62
  • Creative solution, but I don't need to know if the directory is writeable by the uid of the current process, I need to know if the directory is writeable by a different specific uid. – Bill Karwin Jul 18 '14 at 18:38
  • your question at the top doesn't seem to infer that (in my mind) sorry for confusion. I included a way to run as a different user. Sure there are a few ways to do this, such as have the forked off process report an error. – Mike Q Jul 18 '14 at 19:26
  • Thanks, but a solution that requires sudo is not the solution I'm looking for. In many environments, that's not permitted. – Bill Karwin Jul 18 '14 at 20:26
  • The explicit examination of `$?` is superfluous, albeit a common antipattern. Anything which looks like `command; [[ $? -ne 0 ]] && failure || success` is better and more idiomatically written `command && success || failure` (assuming `success` itself always succeeds). – tripleee Nov 10 '18 at 16:06
2

alias wbyu='_(){ local -i FND=0; if [[ $# -eq 2 ]]; then for each in $(groups "$1" | awk "{\$1=\"\";\$2=\"\"; print \$0}"); do (($(find "${2}" \( -perm /220 -o -group "$each" -a -perm /g+w \) 2>/dev/null | wc -l))) && FND=1; done; else echo "Usage: wbyu <user> <file|dir>"; fi; (($FND)) && echo "Eureka!"; }; _'

I put it into an alias it takes two arguments, the first is the user and the second is the directory to check. It looks for permissions writable by anyone and also loops over the groups of the specified user to check if the directory is in the user group and writable - if either gets a hit it sets a found flag and prints Eureka! at the end.

IOW:

FND=0
USER=user1
DIR=/tmp/test
for each in $(groups "$USER" | awk '{$1="";$2=""; print $0}'); do 
(($(find "$DIR" \( -perm /220 -o -group "$each" -a -perm /g+w \)\ 
   2>/dev/null | wc -l))) && FND=1 
done
(($FND)) && echo 'Eureka!'