diff --git a/news/8139.bugfix b/news/8139.bugfix new file mode 100644 index 00000000000..5cb857c71ee --- /dev/null +++ b/news/8139.bugfix @@ -0,0 +1,2 @@ +Correctly set permissions on metadata files during wheel installation, +to permit non-privileged users to read from system site-packages. diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 0b3fbe2ffc2..e7315ee4b52 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -8,6 +8,7 @@ import collections import compileall +import contextlib import csv import logging import os.path @@ -32,17 +33,18 @@ from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.unpacking import unpack_file +from pip._internal.utils.unpacking import current_umask, unpack_file from pip._internal.utils.wheel import parse_wheel if MYPY_CHECK_RUNNING: from email.message import Message from typing import ( Dict, List, Optional, Sequence, Tuple, Any, - Iterable, Callable, Set, + Iterable, Iterator, Callable, Set, ) from pip._internal.models.scheme import Scheme + from pip._internal.utils.filesystem import NamedTemporaryFileResult InstalledCSVRow = Tuple[str, ...] @@ -565,19 +567,27 @@ def is_entrypoint_wrapper(name): if msg is not None: logger.warning(msg) + generated_file_mode = 0o666 - current_umask() + + @contextlib.contextmanager + def _generate_file(path, **kwargs): + # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] + with adjacent_tmp_file(path, **kwargs) as f: + yield f + os.chmod(f.name, generated_file_mode) + replace(f.name, path) + # Record pip as the installer installer_path = os.path.join(dest_info_dir, 'INSTALLER') - with adjacent_tmp_file(installer_path) as installer_file: + with _generate_file(installer_path) as installer_file: installer_file.write(b'pip\n') - replace(installer_file.name, installer_path) generated.append(installer_path) # Record the PEP 610 direct URL reference if direct_url is not None: direct_url_path = os.path.join(dest_info_dir, DIRECT_URL_METADATA_NAME) - with adjacent_tmp_file(direct_url_path) as direct_url_file: + with _generate_file(direct_url_path) as direct_url_file: direct_url_file.write(direct_url.to_json().encode("utf-8")) - replace(direct_url_file.name, direct_url_path) generated.append(direct_url_path) # Record details of all files installed @@ -589,10 +599,9 @@ def is_entrypoint_wrapper(name): changed=changed, generated=generated, lib_dir=lib_dir) - with adjacent_tmp_file(record_path, **csv_io_kwargs('w')) as record_file: + with _generate_file(record_path, **csv_io_kwargs('w')) as record_file: writer = csv.writer(record_file) writer.writerows(sorted_outrows(rows)) # sort to simplify testing - replace(record_file.name, record_path) def install_wheel( diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index fc719e8905e..2350927541d 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -239,13 +239,19 @@ def prep(self, data, tmpdir): self.dest_dist_info = os.path.join( self.scheme.purelib, 'sample-1.2.0.dist-info') + def assert_permission(self, path, mode): + target_mode = os.stat(path).st_mode & 0o777 + assert (target_mode & mode) == mode, oct(target_mode) + def assert_installed(self): # lib assert os.path.isdir( os.path.join(self.scheme.purelib, 'sample')) # dist-info metadata = os.path.join(self.dest_dist_info, 'METADATA') - assert os.path.isfile(metadata) + self.assert_permission(metadata, 0o644) + record = os.path.join(self.dest_dist_info, 'RECORD') + self.assert_permission(record, 0o644) # data files data_file = os.path.join(self.scheme.data, 'my_data', 'data_file') assert os.path.isfile(data_file) @@ -286,7 +292,7 @@ def test_std_install_with_direct_url(self, data, tmpdir): direct_url_path = os.path.join( self.dest_dist_info, DIRECT_URL_METADATA_NAME ) - assert os.path.isfile(direct_url_path) + self.assert_permission(direct_url_path, 0o644) with open(direct_url_path, 'rb') as f: expected_direct_url_json = direct_url.to_json() direct_url_json = f.read().decode("utf-8")