Skip to content

Commit 6ffaa9c

Browse files
jellytomasmatus
authored andcommitted
cockpit: support access attributes in fsinfo
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, for the case where `name` is an empty string we read the access bits from `/proc/self/fd/$fd`. Closes: #21596
1 parent e663b16 commit 6ffaa9c

File tree

2 files changed

+51
-1
lines changed

2 files changed

+51
-1
lines changed

src/cockpit/channels/filesystem.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,16 @@ def get_group(gid: int) -> 'str | int':
440440
except KeyError:
441441
return gid
442442

443+
def get_access(dir_fd: int, name: str, mode: int, *, follow_symlinks: bool = False) -> 'bool | None':
444+
try:
445+
if name:
446+
return os.access(name, mode, dir_fd=dir_fd, follow_symlinks=follow_symlinks)
447+
else:
448+
# This is the given path for which systemd_ctypes has already resolved the symlink
449+
return os.access(f'/proc/self/fd/{dir_fd}', mode, follow_symlinks=True)
450+
except OSError:
451+
return None
452+
443453
stat_types = {stat.S_IFREG: 'reg', stat.S_IFDIR: 'dir', stat.S_IFLNK: 'lnk', stat.S_IFCHR: 'chr',
444454
stat.S_IFBLK: 'blk', stat.S_IFIFO: 'fifo', stat.S_IFSOCK: 'sock'}
445455
available_stat_getters = {
@@ -469,6 +479,10 @@ def get_attrs(fd: int, name: str, follow: Follow) -> 'JsonDict | None':
469479
with contextlib.suppress(OSError):
470480
result['target'] = os.readlink(name, dir_fd=fd)
471481

482+
for (attr, mask) in [('r-ok', os.R_OK), ('w-ok', os.W_OK), ('x-ok', os.X_OK)]:
483+
if attr in result:
484+
result[attr] = get_access(fd, name, mask, follow_symlinks=follow.value)
485+
472486
return result
473487

474488
return get_attrs

test/pytest/test_bridge.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1359,7 +1359,7 @@ async def test_fsinfo_onlydir(transport: MockTransport, fsinfo_test_cases: 'dict
13591359

13601360
# with '/' appended, this should only open dirs
13611361
client = await FsInfoClient.open(transport, str(path) + '/')
1362-
assert await client.wait() == expected_state
1362+
assert await client.wait() == expected_state, f'for {path}'
13631363

13641364

13651365
@pytest.mark.asyncio
@@ -1587,3 +1587,39 @@ async def test_fsinfo_targets(transport: MockTransport, tmp_path: Path) -> None:
15871587
# double-check with the non-watch variant
15881588
client = await FsInfoClient.open(transport, tmp_path, ['type', 'target', 'targets'], fnmatch='l*')
15891589
assert await client.wait() == state
1590+
1591+
1592+
@pytest.mark.asyncio
1593+
async def test_fsinfo_access_attrs(transport: MockTransport, fsinfo_test_cases: 'dict[Path, JsonObject]') -> None:
1594+
for path, expected_state in fsinfo_test_cases.items():
1595+
# these are errors
1596+
if path.name == 'dangling' or path.name == 'loopy':
1597+
continue
1598+
1599+
read_ok = True
1600+
write_ok = True
1601+
exec_ok = path.is_dir()
1602+
1603+
if path.name == 'no-r-dir':
1604+
read_ok = False
1605+
elif path.name == 'no-x-dir':
1606+
exec_ok = False
1607+
elif path.name == 'no-r-file':
1608+
read_ok = False
1609+
write_ok = False
1610+
exec_ok = False
1611+
1612+
expected_state = {'info': {'r-ok': read_ok, 'w-ok': write_ok, 'x-ok': exec_ok}}
1613+
1614+
# fnmatch='' to not include entries
1615+
client = await FsInfoClient.open(transport, path, attrs=['w-ok', 'r-ok', 'x-ok'], fnmatch='')
1616+
assert await client.wait() == expected_state, f'for path={path.name}'
1617+
1618+
# Symlink access bits are always allowed
1619+
for path, expected_state in fsinfo_test_cases.items():
1620+
if path.name != 'dev':
1621+
continue
1622+
1623+
expected_state = {'info': {'r-ok': True, 'w-ok': True, 'x-ok': True}}
1624+
client = await FsInfoClient.open(transport, path, attrs=['w-ok', 'r-ok', 'x-ok'], follow=False)
1625+
assert await client.wait() == expected_state, f'for path={path.name}'

0 commit comments

Comments
 (0)