Skip to content

Commit 667acf4

Browse files
Inherit HTTP cache file read/write permissions from cache directory (#13070)
The NamedTemporaryFile class used to create HTTP cache files is hard-coded to use file mode 600 (owner read/write only). This makes it impossible to share a pip cache with other users. With this patch, once a cache file is committed, its permissions are updated to inherit the read/write permissions of the cache directory. As before, the owner read/write permissions will always be set to avoid a completely unusable cache. --------- Co-authored-by: Richard Si <[email protected]>
1 parent 2324303 commit 667acf4

File tree

3 files changed

+46
-0
lines changed

3 files changed

+46
-0
lines changed

news/11012.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Files in the network cache will inherit the read/write permissions of pip's cache
2+
directory (in addition to the current user retaining read/write access). This
3+
enables a single cache to be shared among multiple users.

src/pip/_internal/network/cache.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ def _write(self, path: str, data: bytes) -> None:
7676

7777
with adjacent_tmp_file(path) as f:
7878
f.write(data)
79+
# Inherit the read/write permissions of the cache directory
80+
# to enable multi-user cache use-cases.
81+
mode = (
82+
os.stat(self.directory).st_mode
83+
& 0o666 # select read/write permissions of cache directory
84+
| 0o600 # set owner read/write permissions
85+
)
86+
# Change permissions only if there is no risk of following a symlink.
87+
if os.chmod in os.supports_fd:
88+
os.chmod(f.fileno(), mode)
89+
elif os.chmod in os.supports_follow_symlinks:
90+
os.chmod(f.name, mode, follow_symlinks=False)
7991

8092
replace(f.name, path)
8193

tests/unit/test_network_cache.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,34 @@ def test_cache_hashes_are_same(self, cache_tmpdir: Path) -> None:
8282
key = "test key"
8383
fake_cache = Mock(FileCache, directory=cache.directory, encode=FileCache.encode)
8484
assert cache._get_cache_path(key) == FileCache._fn(fake_cache, key)
85+
86+
@pytest.mark.skipif("sys.platform == 'win32'")
87+
@pytest.mark.skipif(
88+
os.chmod not in os.supports_fd and os.chmod not in os.supports_follow_symlinks,
89+
reason="requires os.chmod to support file descriptors or not follow symlinks",
90+
)
91+
@pytest.mark.parametrize(
92+
"perms, expected_perms", [(0o300, 0o600), (0o700, 0o600), (0o777, 0o666)]
93+
)
94+
def test_cache_inherit_perms(
95+
self, cache_tmpdir: Path, perms: int, expected_perms: int
96+
) -> None:
97+
key = "foo"
98+
with chmod(cache_tmpdir, perms):
99+
cache = SafeFileCache(os.fspath(cache_tmpdir))
100+
cache.set(key, b"bar")
101+
assert (os.stat(cache._get_cache_path(key)).st_mode & 0o777) == expected_perms
102+
103+
@pytest.mark.skipif("sys.platform == 'win32'")
104+
def test_cache_not_inherit_perms(
105+
self, cache_tmpdir: Path, monkeypatch: pytest.MonkeyPatch
106+
) -> None:
107+
monkeypatch.setattr(os, "supports_fd", os.supports_fd - {os.chmod})
108+
monkeypatch.setattr(
109+
os, "supports_follow_symlinks", os.supports_follow_symlinks - {os.chmod}
110+
)
111+
key = "foo"
112+
with chmod(cache_tmpdir, 0o777):
113+
cache = SafeFileCache(os.fspath(cache_tmpdir))
114+
cache.set(key, b"bar")
115+
assert (os.stat(cache._get_cache_path(key)).st_mode & 0o777) == 0o600

0 commit comments

Comments
 (0)