Repository: pypa/pip

Issue:
PEP 610 support (git packages) can break pip freeze
First of all, great job guys on adding PEP 610 support, this is really awesome !

But, if you install a *git package* with `sudo`, it will cause any non root `pip freeze` to crash 

**Environment**
* pip version: 20.1b1
* Python version: 3.6
* OS: Ubuntu

If you do
```console 
sudo pip install git+https://github.com/...
```
Then when you run `pip freeze` (notice without `sudo`)

You get the following exception:
```
ERROR: Exception:
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/pip/_internal/cli/base_command.py", line 188, in _main
    status = self.run(options, args)
  File "/usr/local/lib/python3.6/site-packages/pip/_internal/commands/freeze.py", line 98, in run
    for line in freeze(**freeze_kwargs):
  File "/usr/local/lib/python3.6/site-packages/pip/_internal/operations/freeze.py", line 68, in freeze
    req = FrozenRequirement.from_dist(dist)
  File "/usr/local/lib/python3.6/site-packages/pip/_internal/operations/freeze.py", line 256, in from_dist
    direct_url = dist_get_direct_url(dist)
  File "/usr/local/lib/python3.6/site-packages/pip/_internal/utils/direct_url_helpers.py", line 118, in dist_get_direct_url
    return DirectUrl.from_json(dist.get_metadata(DIRECT_URL_METADATA_NAME))
  File "/usr/local/lib/python3.6/site-packages/pip/_vendor/pkg_resources/__init__.py", line 1420, in get_metadata
    value = self._get(path)
  File "/usr/local/lib/python3.6/site-packages/pip/_vendor/pkg_resources/__init__.py", line 1616, in _get
    with open(path, 'rb') as stream:
PermissionError: [Errno 13] Permission denied: '/usr/local/lib/python3.6/site-packages/trains-0.14.1.dist-info/direct_url.json'
```

As you can see the `direct_url.json` file  is missing the r flag for group / others , which raises the exception.
```
-rw------- 1 root root 135 Apr 25 23:22 /usr/local/lib/python3.6/site-packages/trains-0.14.1.dist-info/direct_url.json
```

I tested running `chmod 644` on this file, and after that `pip freeze` works like a charm.

For reference, the `top_level.txt` file that is next to the `direct_url.json` has the correct permission




Discussion:
For completeness, does the `RECORD` file has the same permission? Most files in`.dist-info` are extracted from the wheel, and get their permission fixed during the process. But these two are written from scratch, and should be in a similar situation.

(Note for implementer: the permission fix for files extracted from wheel is in `pip._internal.utils.unpacking`)
@uranusjr see below full listing of the directory

```bash
ll /usr/local/lib/python3.6/site-packages/trains-0.14.1.dist-info/                                                                                  total 72K
-rw------- 1 root root 135 Apr 26 02:05 direct_url.json
-rw-r--r-- 1 root root  69 Apr 26 02:05 entry_points.txt
-rw------- 1 root root   4 Apr 26 02:05 INSTALLER
-rw-r--r-- 1 root root 12K Apr 26 02:05 LICENSE
-rw-r--r-- 1 root root 11K Apr 26 02:05 METADATA
-rw------- 1 root root 26K Apr 26 02:05 RECORD
-rw-r--r-- 1 root root   7 Apr 26 02:05 top_level.txt
-rw-r--r-- 1 root root 110 Apr 26 02:05 WHEEL
```

The permissions shown as per above comment also hold true when performing a pip install without sudo using a git VCS URL, but the pip freeze doesn't crash in this case

```
$ pip --version
pip 20.1.dev1 from /Users/devesh/pip/src/pip (python 3.8)
$ python --version
Python 3.8.2

$ pip install git+https://github.com/pypa/twine#egg=master
Collecting master
  Cloning https://github.com/pypa/twine to /private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pip-install-d7wn0lz6/master
  Running command git clone -q https://github.com/pypa/twine /private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pip-install-d7wn0lz6/master
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
  WARNING: Generating metadata for package master produced metadata for project name twine. Fix your #egg=master fragments.
Requirement already satisfied: requests>=2.20 in ./.env/lib/python3.8/site-packages (from twine) (2.23.0)
Requirement already satisfied: requests-toolbelt!=0.9.0,>=0.8.0 in ./.env/lib/python3.8/site-packages (from twine) (0.9.1)
Requirement already satisfied: pkginfo>=1.4.2 in ./.env/lib/python3.8/site-packages (from twine) (1.5.0.1)
Requirement already satisfied: keyring>=15.1 in ./.env/lib/python3.8/site-packages (from twine) (21.2.0)
Requirement already satisfied: setuptools>=0.7.0 in ./.env/lib/python3.8/site-packages (from twine) (41.2.0)
Requirement already satisfied: readme-renderer>=21.0 in ./.env/lib/python3.8/site-packages (from twine) (26.0)
Requirement already satisfied: tqdm>=4.14 in ./.env/lib/python3.8/site-packages (from twine) (4.45.0)
Requirement already satisfied: idna<3,>=2.5 in ./.env/lib/python3.8/site-packages (from requests>=2.20->twine) (2.9)
Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in ./.env/lib/python3.8/site-packages (from requests>=2.20->twine) (1.25.9)
Requirement already satisfied: chardet<4,>=3.0.2 in ./.env/lib/python3.8/site-packages (from requests>=2.20->twine) (3.0.4)
Requirement already satisfied: certifi>=2017.4.17 in ./.env/lib/python3.8/site-packages (from requests>=2.20->twine) (2020.4.5.1)
Requirement already satisfied: docutils>=0.13.1 in ./.env/lib/python3.8/site-packages (from readme-renderer>=21.0->twine) (0.16)
Requirement already satisfied: Pygments>=2.5.1 in ./.env/lib/python3.8/site-packages (from readme-renderer>=21.0->twine) (2.6.1)
Requirement already satisfied: six in ./.env/lib/python3.8/site-packages (from readme-renderer>=21.0->twine) (1.14.0)
Requirement already satisfied: bleach>=2.1.0 in ./.env/lib/python3.8/site-packages (from readme-renderer>=21.0->twine) (3.1.4)
Requirement already satisfied: webencodings in ./.env/lib/python3.8/site-packages (from bleach>=2.1.0->readme-renderer>=21.0->twine) (0.5.1)
Building wheels for collected packages: twine, twine
  Building wheel for twine (PEP 517) ... done
  Created wheel for twine: filename=twine-3.1.2.dev52+g2cec216-py3-none-any.whl size=36679 sha256=72ec0b185e4b51d5e5e95860a68b81daf25d3cbe83667a79a8649a0eec04622a
  Stored in directory: /private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pip-ephem-wheel-cache-t4yq_ai4/wheels/73/dd/fb/3039711df8645ab0ccc3e7d5f834289bf9ee7569c222c1fbc4
  Building wheel for twine (PEP 517) ... done
  Created wheel for twine: filename=twine-3.1.2.dev52+g2cec216-py3-none-any.whl size=36679 sha256=90d3e0d5596caa33d2cc171f611f1c1430450a66085ec72d5fcda503a09c9806
  Stored in directory: /private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pip-ephem-wheel-cache-t4yq_ai4/wheels/d4/ae/3f/06dd543cc8064e720fa3f9bcc69f3c644ea7c931864ee2c7b3
Successfully built twine twine
Installing collected packages: twine
Successfully installed twine-3.1.2.dev52+g2cec216

$ ls -l .env/lib/python3.8/site-packages/twine-3.1.2.dev52+g2cec216.dist-info/
total 104
-rw-------  1 devesh  staff      4 Apr 26 10:51 INSTALLER
-rw-r--r--  1 devesh  staff   9695 Apr 26 10:51 LICENSE
-rw-r--r--  1 devesh  staff  16240 Apr 26 10:51 METADATA
-rw-------  1 devesh  staff   2811 Apr 26 10:51 RECORD
-rw-r--r--  1 devesh  staff     92 Apr 26 10:51 WHEEL
-rw-------  1 devesh  staff    125 Apr 26 10:51 direct_url.json
-rw-r--r--  1 devesh  staff    186 Apr 26 10:51 entry_points.txt
-rw-r--r--  1 devesh  staff      6 Apr 26 10:51 top_level.txt


$ pip freeze
...
twine @ git+https://github.com/pypa/twine@2cec2169faeb9a2906b2288776be91d660561a06
...
```
The problem has been introduced in #7929 when we started using `adjacent_tmp_file` to create `RECORD`, `direct_url.json` and `INSTALLER`. Files created with `tempfile` functions have restricted permissions to avoid security issues when such files are created in world-readable directories such as `/tmp`.

That is something that needs fixing, possibly in 20.1.



Resolving patch:
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
--- 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(


Test patch:
diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py
--- 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")