Skip to content

Commit f49a2d9

Browse files
amol-ichard26
andauthored
Ensure selfcheck file inherits directory permissions (#13528)
When writing files to the cache, pip copies the permissions from the cache directory. This is an expected behaviour to ensure that any user that has access to the cache has also access to its content. Copying the permissions explicitly is necessary due to the use of adjacent_tmp_file() which by virtue of tempfile.mkstemp() (indirectly via NamedTemporaryFile) causes all files to be created with 600 permissions. This is not happening for the selfcheck cache, rendering it unaccessible by users different from the one that first created the cache. Signed-off-by: Alessandro Molina <[email protected]> Co-authored-by: Richard Si <[email protected]>
1 parent 5455edc commit f49a2d9

File tree

5 files changed

+35
-14
lines changed

5 files changed

+35
-14
lines changed

news/13528.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Ensure the self-check files in the cache has same permissions as the rest of the cache.

src/pip/_internal/network/cache.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
from pip._vendor.cachecontrol.caches import SeparateBodyFileCache
1414
from pip._vendor.requests.models import Response
1515

16-
from pip._internal.utils.filesystem import adjacent_tmp_file, replace
16+
from pip._internal.utils.filesystem import (
17+
adjacent_tmp_file,
18+
copy_directory_permissions,
19+
replace,
20+
)
1721
from pip._internal.utils.misc import ensure_dir
1822

1923

@@ -82,16 +86,7 @@ def _write_to_file(self, path: str, writer_func: Callable[[BinaryIO], Any]) -> N
8286
writer_func(f)
8387
# Inherit the read/write permissions of the cache directory
8488
# to enable multi-user cache use-cases.
85-
mode = (
86-
os.stat(self.directory).st_mode
87-
& 0o666 # select read/write permissions of cache directory
88-
| 0o600 # set owner read/write permissions
89-
)
90-
# Change permissions only if there is no risk of following a symlink.
91-
if os.chmod in os.supports_fd:
92-
os.chmod(f.fileno(), mode)
93-
elif os.chmod in os.supports_follow_symlinks:
94-
os.chmod(f.name, mode, follow_symlinks=False)
89+
copy_directory_permissions(self.directory, f)
9590

9691
replace(f.name, path)
9792

src/pip/_internal/self_outdated_check.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@
2727
get_best_invocation_for_this_pip,
2828
get_best_invocation_for_this_python,
2929
)
30-
from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace
30+
from pip._internal.utils.filesystem import (
31+
adjacent_tmp_file,
32+
check_path_owner,
33+
copy_directory_permissions,
34+
replace,
35+
)
3136
from pip._internal.utils.misc import (
3237
ExternallyManagedEnvironment,
3338
check_externally_managed,
@@ -100,13 +105,15 @@ def set(self, pypi_version: str, current_time: datetime.datetime) -> None:
100105
if not self._statefile_path:
101106
return
102107

108+
statefile_directory = os.path.dirname(self._statefile_path)
109+
103110
# Check to make sure that we own the directory
104-
if not check_path_owner(os.path.dirname(self._statefile_path)):
111+
if not check_path_owner(statefile_directory):
105112
return
106113

107114
# Now that we've ensured the directory is owned by this user, we'll go
108115
# ahead and make sure that all our directories are created.
109-
ensure_dir(os.path.dirname(self._statefile_path))
116+
ensure_dir(statefile_directory)
110117

111118
state = {
112119
# Include the key so it's easy to tell which pip wrote the
@@ -120,6 +127,7 @@ def set(self, pypi_version: str, current_time: datetime.datetime) -> None:
120127

121128
with adjacent_tmp_file(self._statefile_path) as f:
122129
f.write(text.encode())
130+
copy_directory_permissions(statefile_directory, f)
123131

124132
try:
125133
# Since we have a prefix-specific state file, we can just

src/pip/_internal/utils/filesystem.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,15 @@ def directory_size(path: str) -> int | float:
150150

151151
def format_directory_size(path: str) -> str:
152152
return format_size(directory_size(path))
153+
154+
155+
def copy_directory_permissions(directory: str, target_file: BinaryIO) -> None:
156+
mode = (
157+
os.stat(directory).st_mode & 0o666 # select read/write permissions of directory
158+
| 0o600 # set owner read/write permissions
159+
)
160+
# Change permissions only if there is no risk of following a symlink.
161+
if os.chmod in os.supports_fd:
162+
os.chmod(target_file.fileno(), mode)
163+
elif os.chmod in os.supports_follow_symlinks:
164+
os.chmod(target_file.name, mode, follow_symlinks=False)

tests/unit/test_self_check_outdated.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ def test_writes_expected_statefile(self, tmpdir: Path) -> None:
189189
"last_check": "2000-01-01T00:00:00+00:00",
190190
"pypi_version": "1.0.0",
191191
}
192+
# Check that the self-check cache entries inherit the root cache permissions.
193+
statefile_permissions = os.stat(expected_path).st_mode & 0o666
194+
selfcheckdir_permissions = os.stat(cache_dir / "selfcheck").st_mode & 0o666
195+
cache_permissions = os.stat(cache_dir).st_mode & 0o666
196+
assert statefile_permissions == selfcheckdir_permissions == cache_permissions
192197

193198

194199
@patch("pip._internal.self_outdated_check._self_version_check_logic")

0 commit comments

Comments
 (0)