Skip to content

Commit

Permalink
cockpit: support access attributes in fsinfo
Browse files Browse the repository at this point in the history
For cockpit-files it is useful to know if the current watched directory
or for example a text file is editable for the current user. Doing this
based on the existing file permissions doesn't take ACL's into account.

The `access` syscall only handles one access check (read/write/execute)
per call making it rather inefficient to check for multiple scenario's,
so that's why there are separate attrs depending on what the user want
so in worst case we only add 1 extra syscall.

As Python does not support AT_EMPTY_PATH there is a workaround to read
the file from /proc/self this is only required for reading the access
bits of the current watched directory.

Closes: cockpit-project#21596
  • Loading branch information
jelly committed Feb 10, 2025
1 parent 54f87df commit ca10789
Showing 1 changed file with 23 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/cockpit/channels/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,20 @@ def get_group(gid: int) -> 'str | int':
except KeyError:
return gid

def get_access(name: str, fd: int, mode: int, *, follow_symlinks: bool = False) -> 'bool | None':
if not name:
# HACK: Python's os.access() does not support passing "AT_EMPTY_PATH"
# so we need to resolve the name of the watched directory.
try:
name = os.readlink(f'/proc/self/fd/{fd}')
return os.access(name, mode, follow_symlinks=follow_symlinks)
except OSError:
return None
try:
return os.access(name, mode, dir_fd=fd, follow_symlinks=follow_symlinks)
except OSError:
return None

stat_types = {stat.S_IFREG: 'reg', stat.S_IFDIR: 'dir', stat.S_IFLNK: 'lnk', stat.S_IFCHR: 'chr',
stat.S_IFBLK: 'blk', stat.S_IFIFO: 'fifo', stat.S_IFSOCK: 'sock'}
available_stat_getters = {
Expand All @@ -393,6 +407,11 @@ def get_group(gid: int) -> 'str | int':
}
stat_getters = tuple((key, available_stat_getters.get(key, lambda _: None)) for key in attrs)

available_access_getters = {
'r-ok': lambda name, fd, follow: get_access(name, fd, os.R_OK, follow_symlinks=follow),
'w-ok': lambda name, fd, follow: get_access(name, fd, os.W_OK, follow_symlinks=follow),
}

def get_attrs(fd: int, name: str, follow: Follow) -> 'JsonDict | None':
try:
buf = os.stat(name, follow_symlinks=follow.value, dir_fd=fd) if name else os.fstat(fd)
Expand All @@ -407,6 +426,10 @@ def get_attrs(fd: int, name: str, follow: Follow) -> 'JsonDict | None':
with contextlib.suppress(OSError):
result['target'] = os.readlink(name, dir_fd=fd)

for attr, getter in available_access_getters.items():
if attr in result:
result[attr] = getter(name, fd, follow.value)

return result

return get_attrs
Expand Down

0 comments on commit ca10789

Please sign in to comment.