5

I need to pass a user and a directory to a script and have it spit out a list of what folders/files in that directory that the user has read access to. MS has a tool called AccessChk for Windows that does this but does something like this exist on the Unix side? I found some code that will do this for a specific folder or file but I need it to traverse a directory.

2 Answers2

8

TL;DR

find "$dir" ! -type l -print0 |
  sudo -u "$user" perl -Mfiletest=access -l -0ne 'print if -r'

You need to ask the system if the user has read permission. The only reliable way is to switch the effective uid, effective gid and supplementation gids to that of the user and use the access(R_OK) system call (even that has some limitations on some systems/configurations).

The longer story

Let's consider what it takes for instance for a $user to have read access to /foo/file.txt (assuming none of /foo and /foo/file.txt are symlinks)?

He needs:

  1. search access to / (no need for read)
  2. search access to /foo (no need for read)
  3. read access to /foo/file.txt

You can see already that approaches that check only the permission of file.txt won't work because they could say file.txt is readable even if the user doesn't have search permission to / or /foo.

And an approach like:

sudo -u "$user" find / -readable

Won't work either because it won't report the files in directories the user doesn't have read access (as find running as $user can't list their content) even if he can read them.

If we forget about ACLs or other security measures (apparmor, SELinux...) and only focus on traditional permission and ownership attributes, to get a given (search or read) permission, that's already quite complicated and hard to express with find.

You need:

  • if the file is owned by you, you need that permission for the owner (or have uid 0)
  • if the file is not owned by you, but the group is one of yours, then you need that permission for the group (or have uid 0).
  • if it's not owned by you, and not in any of your groups, then the other permissions apply (unless your uid is 0).

In find syntax, here as an example with a user of uid 1 and gids 1 and 2, that would be:

find / -type d \
  \( \
    -user 1 \( -perm -u=x -o -prune \) -o \
    \( -group 1 -o -group 2 \) \( -perm -g=x -o -prune \) -o \
    -perm -o=x -o -prune \
  \) ! -type d -o -type l -o \
    -user 1 \( ! -perm -u=r -o -print \) -o \
    \( -group 1 -o -group 2 \) \( ! -perm -g=r -o -print \) -o \
    ! -perm -o=r -o -print

That one prunes the directories that user doesn't have search right for and for other types of files (symlinks excluded as they're not relevant), checks for read access.

Or for an arbitrary $user and its group membership retrieved from the user database:

groups=$(id -G "$user" | sed 's/ / -o -group /g'); IFS=" "
find / -type d \
  \( \
    -user "$user" \( -perm -u=x -o -prune \) -o \
    \( -group $groups \) \( -perm -g=x -o -prune \) -o \
    -perm -o=x -o -prune \
  \) ! -type d -o -type l -o \
    -user "$user" \( ! -perm -u=r -o -print \) -o \
    \( -group $groups \) \( ! -perm -g=r -o -print \) -o \
    ! -perm -o=r -o -print

The best here would be to descend the tree as root and check the permissions as the user for each file.

 find / ! -type l -exec sudo -u "$user" sh -c '
   for file do
     [ -r "$file" ] && printf "%s\n" "$file"
   done' sh {} +

Or with perl:

find / ! -type l -print0 |
  sudo -u "$user" perl -Mfiletest=access -l -0ne 'print if -r'

Or with zsh:

files=(/**/*(D^@))
USERNAME=$user
for f ($files) {
  [ -r $f ] && print -r -- $f
}

Those solutions rely on the access(2) system call. That is instead of reproducing the algorithm the system uses to check for access permission, we're asking the system to do that check with the same algorithm (which takes into account permissions, ACLs...) it would use would you try to open the file for reading, so is the closest you're going to get to a reliable solution.

Now, all those solutions try to identify the paths of files that the user may open for reading, that's different from the paths where the user may be able to read the content. To answer that more generic question, there are several things to take into account:

  1. $user may not have read access to /a/b/file but if he owns file (and has search access to /a/b, and he's got shell access to the system), then he would be able to change the permissions of the file and grant himself access.
  2. Same thing if he owns /a/b but doesn't have search access to it.
  3. $user may not have access to /a/b/file because he doesn't have search access to /a or /a/b, but that file may have a hard link at /b/c/file for instance, in which case he may be able to read the content of /a/b/file by opening it via its /b/c/file path.
  4. Same thing with bind-mounts. He may not have search access to /a, but /a/b may be bind-mounted in /c, so he could open file for reading via its /c/file other path.

To find the paths that $user would be able to read. To address 1 or 2, we can't rely on the access(2) system call anymore. We could adjust our find -perm approach to assume search access to directories, or read access to files as soon as you're the owner:

groups=$(id -G "$user" | sed 's/ / -o -group /g'); IFS=" "
find / -type d \
  \( \
    -user "$user" -o \
    \( -group $groups \) \( -perm -g=x -o -prune \) -o \
    -perm -o=x -o -prune \
  \) ! -type d -o -type l -o \
    -user "$user" -print -o \
    \( -group $groups \) \( ! -perm -g=r -o -print \) -o \
    ! -perm -o=r -o -print

We could address 3 and 4, by recording the device and inode numbers or all the files $user has read permission for and report all the file paths that have those dev+inode numbers. This time, we can use the more reliable access(2)-based approaches:

Something like:

find / ! -type l -print0 |
  sudo -u "$user" perl -Mfiletest=access -0lne 'print 0+-r,$_' |
  perl -l -0ne '
    ($w,$p) = /(.)(.*)/;
    ($dev,$ino) = stat$p or next;
    $writable{"$dev,$ino"} = 1 if $w;
    push @{$p{"$dev,$ino"}}, $p;
    END {
      for $i (keys %writable) {
        for $p (@{$p{$i}}) {
          print $p;
        }
      }
    }'

And merge both solutions with:

{ solution1; solution2
} | perl -l -0ne 'print unless $seen{$_}++'

As should be clear if you've read everything thus far, part of it at least only deals with permissions and ownership, not the other features that may grant or restrict read access (ACLs, other security features...). And as we process it in several stages, some of that information may be wrong if the files/directories are being created/deleted/renamed or their permissions/ownership modified while that script is running, like on a busy file server with millions of files.

Portability notes

All that code is standard (POSIX, Unix for t bit) except:

  • -print0 is a GNU extension now also supported by a few other implementations. With find implementations that lack support for it, you can use -exec printf '%s\0' {} + instead, and replace -exec sh -c 'exec find "$@" -print0' sh {} + with -exec sh -c 'exec find "$@" -exec printf "%s\0" {\} +' sh {} +.
  • perl is not a POSIX-specified command but is widely available. You need perl-5.6.0 or above for -Mfiletest=access.
  • zsh is not a POSIX-specified command. That zsh code above should work with zsh-3 (1995) and above.
  • sudo is not a POSIX-specified command. The code should work with any version as long as the system configuration allows running perl as the given user.
-1

This is one easy route to your solution. Just substitute 'testuser' with your own user. Assuming you know how to use find and can substitute the '.' find argument with your relevant directory.

x=testuser; find . -type f |xargs ls -la | grep $x | awk '{print$9}'

I created three directory deep structure (dir1, dir2, dir3) with some files names test1-3.txt. I arbitrarily assigned user or group as testuser to only some of the files.

[root@myserver tmp]# x=testuser; find . -type f |xargs ls -la | grep $x | awk '{print$9}'

./dir1/dir2/dir3/test2.txt

./dir1/test2.txt

./dir1/test3.txt

The crux of it is the recursive option on 'ls'. Here is an example of the structure my test was run against:

[root@det1svn01n tmp]# ls -laR dir1

dir1:

total 12

drwxr-xr-x 3 root root 4096 Jun 17 14:55 .

drwxrwxrwt 4 root root 4096 Jun 17 14:54 ..

drwxr-xr-x 3 testuser testuser 4096 Jun 17 14:55 dir2

-rw-r--r-- 1 root root 0 Jun 17 14:55 test1.txt

-rw-r--r-- 1 root testuser 0 Jun 17 14:55 test2.txt

-rw-r--r-- 1 testuser testuser 0 Jun 17 14:55 test3.txt

dir1/dir2:

total 12

drwxr-xr-x 3 testuser testuser 4096 Jun 17 14:55 .

drwxr-xr-x 3 root root 4096 Jun 17 14:55 ..

drwxr-xr-x 2 root root 4096 Jun 17 14:55 dir3

-rw-r--r-- 1 root root 0 Jun 17 14:55 test1.txt

-rw-r--r-- 1 root root 0 Jun 17 14:55 test2.txt

-rw-r--r-- 1 root root 0 Jun 17 14:55 test3.txt

dir1/dir2/dir3:

total 8

drwxr-xr-x 2 root root 4096 Jun 17 14:55 .

drwxr-xr-x 3 testuser testuser 4096 Jun 17 14:55 ..

-rw-r--r-- 1 root root 0 Jun 17 14:55 test1.txt

-rw-r--r-- 1 root root 0 Jun 17 14:55 test2.txt

-rw-r--r-- 1 root root 0 Jun 17 14:55 test3.txt

Baazigar
  • 730
  • It is true, if a file was owned by someone else but named 'testuser' this script would return a false positive. That is some very minor tweaking to eliminate that so I think you can handle it! – Baazigar Jun 17 '15 at 19:08