Skip to content

Commit d4d5e10

Browse files
committed
Correctly handle file permissions for different classes
- always consider the current user group while evaluating file permissions for user/group/other classes
1 parent dd5d1af commit d4d5e10

9 files changed

+206
-39
lines changed

CHANGES.md

+8
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ The released versions correspond to PyPI releases.
33

44
## Unreleased
55

6+
### Changes
7+
* The handling of file permissions under Posix is should now mostly match the behavior
8+
of the real filesystem, which may change the behavior of some tests
9+
610
### Fixes
711
* Fixed a specific problem on reloading a pandas-related module (see [#947](../../issues/947)),
812
added possibility for unload hooks for specific modules
913
* Use this also to reload django views (see [#932](../../issues/932))
1014
* Fixed `EncodingWarning` for Python >= 3.11 (see [#957](../../issues/957))
15+
* Consider directory ownership while adding or removing directory entries
16+
(see [#959](../../issues/959))
17+
* Fixed handling of directory enumeration and search permissions under Posix systems
18+
(see [#960](../../issues/960))
1119

1220
## [Version 5.3.5](https://pypi.python.org/pypi/pyfakefs/5.3.5) (2024-01-30)
1321
Fixes a regression.

pyfakefs/fake_file.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,21 @@ def __setattr__(self, key: str, value: Any) -> None:
391391
def __str__(self) -> str:
392392
return "%r(%o)" % (self.name, self.st_mode)
393393

394+
def has_permission(self, permission_bits: int) -> bool:
395+
"""Checks if the given permissions are set in the fake file.
396+
397+
Args:
398+
permission_bits: The permission bits as set for the user.
399+
400+
Returns:
401+
True if the permissions are set in the correct class (user/group/other).
402+
"""
403+
if helpers.get_uid() == self.stat_result.st_uid:
404+
return self.st_mode & permission_bits == permission_bits
405+
if helpers.get_gid() == self.stat_result.st_gid:
406+
return self.st_mode & (permission_bits >> 3) == permission_bits >> 3
407+
return self.st_mode & (permission_bits >> 6) == permission_bits >> 6
408+
394409

395410
class FakeNullFile(FakeFile):
396411
def __init__(self, filesystem: "FakeFilesystem") -> None:
@@ -504,8 +519,8 @@ def add_entry(self, path_object: FakeFile) -> None:
504519
"""
505520
if (
506521
not helpers.is_root()
507-
and not self.st_mode & helpers.PERM_WRITE
508522
and not self.filesystem.is_windows_fs
523+
and not self.has_permission(helpers.PERM_WRITE)
509524
):
510525
raise OSError(errno.EACCES, "Permission Denied", self.path)
511526

@@ -572,9 +587,8 @@ def remove_entry(self, pathname_name: str, recursive: bool = True) -> None:
572587
if self.filesystem.has_open_file(entry):
573588
self.filesystem.raise_os_error(errno.EACCES, pathname_name)
574589
else:
575-
if not helpers.is_root() and (
576-
self.st_mode & (helpers.PERM_WRITE | helpers.PERM_EXE)
577-
!= helpers.PERM_WRITE | helpers.PERM_EXE
590+
if not helpers.is_root() and not self.has_permission(
591+
helpers.PERM_WRITE | helpers.PERM_EXE
578592
):
579593
self.filesystem.raise_os_error(errno.EACCES, pathname_name)
580594

pyfakefs/fake_filesystem.py

+43-16
Original file line numberDiff line numberDiff line change
@@ -674,14 +674,15 @@ def stat(self, entry_path: AnyStr, follow_symlinks: bool = True):
674674
follow_symlinks,
675675
allow_fd=True,
676676
check_read_perm=False,
677+
check_exe_perm=False,
677678
)
678679
except TypeError:
679680
file_object = self.resolve(entry_path)
680681
if not is_root():
681682
# make sure stat raises if a parent dir is not readable
682683
parent_dir = file_object.parent_dir
683684
if parent_dir:
684-
self.get_object(parent_dir.path) # type: ignore[arg-type]
685+
self.get_object(parent_dir.path, check_read_perm=False) # type: ignore[arg-type]
685686

686687
self.raise_for_filepath_ending_with_separator(
687688
entry_path, file_object, follow_symlinks
@@ -1597,6 +1598,7 @@ def get_object_from_normpath(
15971598
self,
15981599
file_path: AnyPath,
15991600
check_read_perm: bool = True,
1601+
check_exe_perm: bool = True,
16001602
check_owner: bool = False,
16011603
) -> AnyFile:
16021604
"""Search for the specified filesystem object within the fake
@@ -1607,6 +1609,8 @@ def get_object_from_normpath(
16071609
path that has already been normalized/resolved.
16081610
check_read_perm: If True, raises OSError if a parent directory
16091611
does not have read permission
1612+
check_exe_perm: If True, raises OSError if a parent directory
1613+
does not have execute (e.g. search) permission
16101614
check_owner: If True, and check_read_perm is also True,
16111615
only checks read permission if the current user id is
16121616
different from the file object user id
@@ -1638,24 +1642,27 @@ def get_object_from_normpath(
16381642
target = target.get_entry(component) # type: ignore
16391643
if (
16401644
not is_root()
1641-
and check_read_perm
1645+
and (check_read_perm or check_exe_perm)
16421646
and target
1643-
and not self._can_read(target, check_owner)
1647+
and not self._can_read(
1648+
target, check_read_perm, check_exe_perm, check_owner
1649+
)
16441650
):
16451651
self.raise_os_error(errno.EACCES, target.path)
16461652
except KeyError:
16471653
self.raise_os_error(errno.ENOENT, path)
16481654
return target
16491655

16501656
@staticmethod
1651-
def _can_read(target, owner_can_read):
1652-
if target.st_uid == helpers.get_uid():
1653-
if owner_can_read or target.st_mode & 0o400:
1654-
return True
1655-
if target.st_gid == get_gid():
1656-
if target.st_mode & 0o040:
1657-
return True
1658-
return target.st_mode & 0o004
1657+
def _can_read(target, check_read_perm, check_exe_perm, owner_can_read):
1658+
if owner_can_read and target.st_uid == helpers.get_uid():
1659+
return True
1660+
permission = helpers.PERM_READ if check_read_perm else 0
1661+
if S_ISDIR(target.st_mode) and check_exe_perm:
1662+
permission |= helpers.PERM_EXE
1663+
if not permission:
1664+
return True
1665+
return target.has_permission(permission)
16591666

16601667
def get_object(self, file_path: AnyPath, check_read_perm: bool = True) -> FakeFile:
16611668
"""Search for the specified filesystem object within the fake
@@ -1684,6 +1691,7 @@ def resolve(
16841691
follow_symlinks: bool = True,
16851692
allow_fd: bool = False,
16861693
check_read_perm: bool = True,
1694+
check_exe_perm: bool = True,
16871695
check_owner: bool = False,
16881696
) -> FakeFile:
16891697
"""Search for the specified filesystem object, resolving all links.
@@ -1695,6 +1703,8 @@ def resolve(
16951703
allow_fd: If `True`, `file_path` may be an open file descriptor
16961704
check_read_perm: If True, raises OSError if a parent directory
16971705
does not have read permission
1706+
check_read_perm: If True, raises OSError if a parent directory
1707+
does not have execute permission
16981708
check_owner: If True, and check_read_perm is also True,
16991709
only checks read permission if the current user id is
17001710
different from the file object user id
@@ -1714,6 +1724,7 @@ def resolve(
17141724
return self.get_object_from_normpath(
17151725
self.resolve_path(file_path, allow_fd),
17161726
check_read_perm,
1727+
check_exe_perm,
17171728
check_owner,
17181729
)
17191730
return self.lresolve(file_path)
@@ -1756,7 +1767,7 @@ def lresolve(self, path: AnyPath) -> FakeFile:
17561767
if not self.is_windows_fs and isinstance(parent_obj, FakeFile):
17571768
self.raise_os_error(errno.ENOTDIR, path_str)
17581769
self.raise_os_error(errno.ENOENT, path_str)
1759-
if not parent_obj.st_mode & helpers.PERM_READ:
1770+
if not parent_obj.has_permission(helpers.PERM_READ):
17601771
self.raise_os_error(errno.EACCES, parent_directory)
17611772
return (
17621773
parent_obj.get_entry(to_string(child_name))
@@ -1781,7 +1792,10 @@ def add_object(self, file_path: AnyStr, file_object: AnyFile) -> None:
17811792
if not file_path:
17821793
target_directory = self.root_dir
17831794
else:
1784-
target_directory = cast(FakeDirectory, self.resolve(file_path))
1795+
target_directory = cast(
1796+
FakeDirectory,
1797+
self.resolve(file_path, check_read_perm=False, check_exe_perm=True),
1798+
)
17851799
if not S_ISDIR(target_directory.st_mode):
17861800
error = errno.ENOENT if self.is_windows_fs else errno.ENOTDIR
17871801
self.raise_os_error(error, file_path)
@@ -2859,14 +2873,22 @@ def isjunction(self, path: AnyPath) -> bool:
28592873
return False
28602874

28612875
def confirmdir(
2862-
self, target_directory: AnyStr, check_owner: bool = False
2876+
self,
2877+
target_directory: AnyStr,
2878+
check_read_perm: bool = True,
2879+
check_exe_perm: bool = True,
2880+
check_owner: bool = False,
28632881
) -> FakeDirectory:
28642882
"""Test that the target is actually a directory, raising OSError
28652883
if not.
28662884
28672885
Args:
28682886
target_directory: Path to the target directory within the fake
28692887
filesystem.
2888+
check_read_perm: If True, raises OSError if the directory
2889+
does not have read permission
2890+
check_exe_perm: If True, raises OSError if the directory
2891+
does not have execute (e.g. search) permission
28702892
check_owner: If True, only checks read permission if the current
28712893
user id is different from the file object user id
28722894
@@ -2878,7 +2900,12 @@ def confirmdir(
28782900
"""
28792901
directory = cast(
28802902
FakeDirectory,
2881-
self.resolve(target_directory, check_owner=check_owner),
2903+
self.resolve(
2904+
target_directory,
2905+
check_read_perm=check_read_perm,
2906+
check_exe_perm=check_exe_perm,
2907+
check_owner=check_owner,
2908+
),
28822909
)
28832910
if not directory.st_mode & S_IFDIR:
28842911
self.raise_os_error(errno.ENOTDIR, target_directory, 267)
@@ -2972,7 +2999,7 @@ def listdir(self, target_directory: AnyStr) -> List[AnyStr]:
29722999
OSError: if the target is not a directory.
29733000
"""
29743001
target_directory = self.resolve_path(target_directory, allow_fd=True)
2975-
directory = self.confirmdir(target_directory)
3002+
directory = self.confirmdir(target_directory, check_exe_perm=False)
29763003
directory_contents = list(directory.entries.keys())
29773004
if self.shuffle_listdir_results:
29783005
random.shuffle(directory_contents)

pyfakefs/fake_open.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,8 @@ def _init_file_object(
270270
) -> FakeFile:
271271
if file_object:
272272
if not is_root() and (
273-
(open_modes.can_read and not file_object.st_mode & PERM_READ)
274-
or (open_modes.can_write and not file_object.st_mode & PERM_WRITE)
273+
(open_modes.can_read and not file_object.has_permission(PERM_READ))
274+
or (open_modes.can_write and not file_object.has_permission(PERM_WRITE))
275275
):
276276
self.filesystem.raise_os_error(errno.EACCES, file_path)
277277
if open_modes.can_write:

pyfakefs/fake_os.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ def chdir(self, path: AnyStr) -> None:
422422
directory = self.filesystem.resolve(path)
423423
# A full implementation would check permissions all the way
424424
# up the tree.
425-
if not is_root() and not directory.st_mode | PERM_EXE:
425+
if not is_root() and not directory.has_permission(PERM_EXE):
426426
self.filesystem.raise_os_error(errno.EACCES, directory.name)
427427
self.filesystem.cwd = path # type: ignore[assignment]
428428

pyfakefs/fake_scandir.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def __init__(self, filesystem, path):
146146
path = make_string_path(path)
147147
self.abspath = self.filesystem.absnormpath(path)
148148
self.path = to_string(path)
149-
entries = self.filesystem.confirmdir(self.abspath).entries
149+
entries = self.filesystem.confirmdir(self.abspath, check_exe_perm=False).entries
150150
self.entry_iter = iter(entries)
151151

152152
def __iter__(self):

0 commit comments

Comments
 (0)