Skip to content

Commit

Permalink
Inherit HTTP cache file read/write permissions from cache directory (#…
Browse files Browse the repository at this point in the history
…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 <sichard26@gmail.com>
  • Loading branch information
JustinVanHeek and ichard26 authored Dec 9, 2024
1 parent 2324303 commit 667acf4
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 0 deletions.
3 changes: 3 additions & 0 deletions news/11012.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Files in the network cache will inherit the read/write permissions of pip's cache
directory (in addition to the current user retaining read/write access). This
enables a single cache to be shared among multiple users.
12 changes: 12 additions & 0 deletions src/pip/_internal/network/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ def _write(self, path: str, data: bytes) -> None:

with adjacent_tmp_file(path) as f:
f.write(data)
# Inherit the read/write permissions of the cache directory
# to enable multi-user cache use-cases.
mode = (
os.stat(self.directory).st_mode
& 0o666 # select read/write permissions of cache directory
| 0o600 # set owner read/write permissions
)
# Change permissions only if there is no risk of following a symlink.
if os.chmod in os.supports_fd:
os.chmod(f.fileno(), mode)
elif os.chmod in os.supports_follow_symlinks:
os.chmod(f.name, mode, follow_symlinks=False)

replace(f.name, path)

Expand Down
31 changes: 31 additions & 0 deletions tests/unit/test_network_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,34 @@ def test_cache_hashes_are_same(self, cache_tmpdir: Path) -> None:
key = "test key"
fake_cache = Mock(FileCache, directory=cache.directory, encode=FileCache.encode)
assert cache._get_cache_path(key) == FileCache._fn(fake_cache, key)

@pytest.mark.skipif("sys.platform == 'win32'")
@pytest.mark.skipif(
os.chmod not in os.supports_fd and os.chmod not in os.supports_follow_symlinks,
reason="requires os.chmod to support file descriptors or not follow symlinks",
)
@pytest.mark.parametrize(
"perms, expected_perms", [(0o300, 0o600), (0o700, 0o600), (0o777, 0o666)]
)
def test_cache_inherit_perms(
self, cache_tmpdir: Path, perms: int, expected_perms: int
) -> None:
key = "foo"
with chmod(cache_tmpdir, perms):
cache = SafeFileCache(os.fspath(cache_tmpdir))
cache.set(key, b"bar")
assert (os.stat(cache._get_cache_path(key)).st_mode & 0o777) == expected_perms

@pytest.mark.skipif("sys.platform == 'win32'")
def test_cache_not_inherit_perms(
self, cache_tmpdir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(os, "supports_fd", os.supports_fd - {os.chmod})
monkeypatch.setattr(
os, "supports_follow_symlinks", os.supports_follow_symlinks - {os.chmod}
)
key = "foo"
with chmod(cache_tmpdir, 0o777):
cache = SafeFileCache(os.fspath(cache_tmpdir))
cache.set(key, b"bar")
assert (os.stat(cache._get_cache_path(key)).st_mode & 0o777) == 0o600

0 comments on commit 667acf4

Please sign in to comment.