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 Answers
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:
- search access to
/
(no need for read) - search access to
/foo
(no need for read) - 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:
- $user may not have read access to
/a/b/file
but if he ownsfile
(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 thefile
and grant himself access. - Same thing if he owns
/a/b
but doesn't have search access to it. - $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. - 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 openfile
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. Withfind
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 needperl-5.6.0
or above for-Mfiletest=access
.zsh
is not a POSIX-specified command. Thatzsh
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 runningperl
as the given user.

- 544,893
-
1this is possibly the best answer in terms of format and completeness if seen on any stack exchange site! – Dirk Hartzer Waldeck Jul 29 '15 at 10:06
-
@DirkHartzerWaldeck. Thanks. Note that this answer was copy-pasted, adapted and simplified from this other answer that dealt with write access which you may like as well ;-). – Stéphane Chazelas Jul 29 '15 at 11:06
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

- 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