14

I want to find files that a particular user will not be able to read.

Assume the username is "user123" and they are in a group called "user123". I want to find files that, if they are owned by user123 have u+r on; failing that if the file is group user123 it should have g+r on; failing that it can have o+r on.

Since GNU find has "-readable", I could do this:

sudo -u user123 find /start ! -readable -ls

However the process has to run by a user that does not have sudo access. Therefore I I tried this: (it doesn't check o+r but that isn't important at this point)

find /start \( -user user123 ! -perm -u=r  \) -o \( -group user123 ! -perm -g=r  \) -ls

but it lists this file:

272118    4 -rw-------   1 user123   user123       3243 Jul  3 19:50 /start/blah/blah/file.txt

This file is the only file under /start that is owned by user123 with g=r off. It is as if find is interpreting the -u=r as -g=r.

I decided to try reversing the logic and instead test not ( truth ) instead:

find /etc/puppet ! \( \( -user puppet -perm -u=r  \) -o \( -group puppet -perm -g=r \) -o \( -perm -o=r \) \)  -ls

That works!

Why did the original find fail? Is it a bug in find (unlikely) or is the logic wrong?

Update: I had the logic wrong. As pointed out below, since ! ( A || B || C ) == ( !A && !B && !C ) these are the two equivalent statements:

find /start ! \( \( -user user123 -perm -u=r \) -o \( -group user123 -perm -g=r \) -o \( ! \( -user user123 -o -group user123 \) -perm -o=r \) \) -ls
find /start ! \( -user user123 -perm -u=r \) ! \( -group user123 -perm -g=r \) ! \( ! \( -user user123 -o -group user123 \) -perm -o=r \) -ls

My goal was not to have to test user/group twice. What I really need is a more complicated if-then-else structure, which would probably only be possible if there was an -xor operator. I could build an xor out of and/or/not but it would be more complex than the two solutions above.

TomOnTime
  • 945

3 Answers3

9

There are far many more things to take into consideration to check whether a user has access to a file via a given path:

  • The owner of the file
  • the group of the file
  • the ACLs in the file
  • the uid, gid, and supplementary gids of the user
  • search access to any path component leading to that file.
  • whether the file is a symlink
  • permissions apply differently for users of id 0.
  • possibly more security features like SELinux...

Short of actually switching all the uids and gids to those of the user and check, it's very difficult to implement the same logic as what the system does.

With zsh, you could do (as root):

readable() (
  USERNAME=$u
  [ -r "$REPLY" ]
)
u=some-user
print -rl -- **/*(DoN^+readable)

Or with perl:

find . -print0 | sudo -u some-user perl -Mfiletest=access -l -0ne '
  print unless -r'

That is in both cases, descend the directory tree as root but test for file access as the corresponding user.

Running find -readable as some-user won't in cases as it won't be able to go past the directories for which the user has no access or no read permission (but possibly access).

Even when only considering the permission and ownership of the file itself (and not ACLs or path components...), you need at least (here GNU syntax):

u=some-user; g=$(id -G "$u" | sed 's/ / -o -group /g'); IFS=" "
find . ! \( -user "$u" -perm -u=r -o \
          ! -user "$u" \( -group $g \) -perm -g=r -o \
          ! -user "$u" ! \( -group $g \) -perm -o=r \)

The idea being that if the file is owned by the user, all other permissions are irrelevant. If not, then if the file is group-owned by any of the user's groups, then the "other" permission is irrelevant.

  • 1
    Good point about ACLs and other factors. The only 100% correct assessment is access() since it uses the same kernel code as open(). Thus sudo -u user123 find /start -readable is the best solution if sudo is an option. – TomOnTime Aug 29 '13 at 00:13
  • 1
    @TomOnTime. Well no, if you use sudo -u user123 find -readable, it won't report files in directories you can't enter, or in directories you can't read (so there will be false negatives and false positives). That's why I suggest using zsh for descending the directory tree as root and do access() ([ -r ... ]) as the actual user (setting $USERNAME in zsh changes all the uids and gids like sudo would). – Stéphane Chazelas Aug 29 '13 at 06:59
7

The logic is wrong. You're thinking this file shouldn't have been listed because it's owned by user123 and has the user's r bit set. However, it's listed because it matches the second criterion (it's owned by group user123 and has the group's r bit unset).

Your second version works because of one of de Morgan's laws: negating the logical ORing of a group of statements is logically equivalent to ANDing the negation of the individual statements. In other words:

 ! ( A || B || C ) == ( !A && !B && !C )

So the working find is looking for a file that

  • Is not (owned by user user123 and readable by said user) AND
  • Is not (owned by group user123 and readable by said group) AND
  • Is not world-readable.

while the first find is looking for a file that

  • Is owned by user user123 and not readable by said user OR
  • Is owned by group user123 and not readable by said group OR (if you had completed it)
  • Is not world-readable

So a file matching ANY of the above 3 criteria (and not necessarily all) would be listed as you have seen.

Edit

Incidentally (after viewing your profile), I'm a big fan of your O'Reilly book :)

Joseph R.
  • 39,549
  • Thanks for the analysis. Yes, it was a misapplication of Morgan's law. I was trying to do ( !A && !B && !C ) but I moved the ! into the interior of each part, which isn't valid. Thanks! – TomOnTime Aug 29 '13 at 00:16
  • P.S. I'm glad you are a fan of my book! I'm curious which language you read it in. – TomOnTime Aug 29 '13 at 00:20
  • @TomOnTime English, of course. I try to read any book in its original language if I can help it. – Joseph R. Aug 29 '13 at 00:29
0

Test environment:

$ sudo tree -fp /tmp/del
/tmp/del
├── [drwxr-xr-x]  /tmp/del/1
│   └── [-rw-r--r--]  /tmp/del/1/f1
├── [d---------]  /tmp/del/2
│   └── [-rw-------]  /tmp/del/2/f1  <-- this filepath is not accessible for ! root
└── [drwxr-xr-x]  /tmp/del/3
    └── [-rw-r--r--]  /tmp/del/3/f1

Full otput:

$ sudo find /tmp/del/ -type f -exec sudo -u user123 ls {} \;
/tmp/del/3/f1
/tmp/del/1/f1
ls: cannot access '/tmp/del/2/f1': Permission denied  <-- required filepath

"Dirty" required output:

# Just redirect stdout
>/dev/null sudo find /tmp/del/ -type f -exec sudo -u user123 ls {} \; 
ls: cannot access '/tmp/del/2/f1': Permission denied

Reusable code

Just hide stdout and show stderr instead it:

function xfind() { 2>&1 >/dev/null sudo find "${2}" -type f -exec sudo -u "${1}" ls {} \; | grep -o "cannot access .*$"; }

"Production" usage example: what I can't see in root's home:

$ xfind $USER /root
cannot access '/root/.cache/dconf/user': Permission denied
cannot access '/root/.cache/gstreamer-1.0/registry.x86_64.bin': Permission denied
...
viktorkho
  • 121