Repository: pypa/pip

Issue:
Incorrect file permissions in dist-info directory after installing a package
**Environment**

* pip version: 20.1
* Python version: 3.8.1
* OS: ubuntu 18.04 (xenial)


**Description**
After a pip install with umask set to 027, files in the environment have incorrect permissions.

I expected the files to have permissions 640, but they ended up as 637. This is both too restrictive at the group level and too permissive at the world level.

**Expected behavior**
Correct file permissions.

I think the cause is this fix:

https://github.com/pypa/pip/pull/8144/files#diff-81eaeaa2196a8c5382958f2d9f22b593R570
```
    generated_file_mode = 0o666 - current_umask()
```

```
>>> oct(0o666 - 0o027)
'0637'
```

I'd have expected a bitwise AND so the result would be 0640.


**How to Reproduce**
virtualenv ./env
source env/bin/activate
umask 027
pip install six
ls -lR env/lib/python3.8/site-packages/six-1.14.0.dist-info/


**Output**

```
root@674aabd90334:~# virtualenv ./env
Using base prefix '/opt/Python/3.8.1'
New python executable in /root/env/bin/python3.8
copying /opt/Python/3.8.1/bin/python3.8 => /root/env/bin/python3.8
Also creating executable in /root/env/bin/python
Installing setuptools, pip, wheel...
done.
root@674aabd90334:~# source env/bin/activate
(env) root@674aabd90334:~# umask 027
(env) root@674aabd90334:~# pip install six
Collecting six
  Using cached six-1.14.0-py2.py3-none-any.whl (10 kB)
Installing collected packages: six
Successfully installed six-1.14.0
(env) root@674aabd90334:~# ls -lR env/lib/python3.8/site-packages/six-1.14.0.dist-info/
env/lib/python3.8/site-packages/six-1.14.0.dist-info/:
total 24
-rw--wxrwx 1 root root    4 Apr 28 21:13 INSTALLER
-rw-r----- 1 root root 1066 Apr 28 21:13 LICENSE
-rw-r----- 1 root root 1795 Apr 28 21:13 METADATA
-rw--wxrwx 1 root root  560 Apr 28 21:13 RECORD
-rw-r----- 1 root root  110 Apr 28 21:13 WHEEL
-rw-r----- 1 root root    4 Apr 28 21:13 top_level.txt
```



Discussion:
Indeed `0o666 & ~current_umask()` should be better.

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
@@ -567,7 +567,7 @@ def is_entrypoint_wrapper(name):
         if msg is not None:
             logger.warning(msg)
 
-    generated_file_mode = 0o666 - current_umask()
+    generated_file_mode = 0o666 & ~current_umask()
 
     @contextlib.contextmanager
     def _generate_file(path, **kwargs):


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
@@ -243,15 +243,15 @@ 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):
+    def assert_installed(self, expected_permission):
         # lib
         assert os.path.isdir(
             os.path.join(self.scheme.purelib, 'sample'))
         # dist-info
         metadata = os.path.join(self.dest_dist_info, 'METADATA')
-        self.assert_permission(metadata, 0o644)
+        self.assert_permission(metadata, expected_permission)
         record = os.path.join(self.dest_dist_info, 'RECORD')
-        self.assert_permission(record, 0o644)
+        self.assert_permission(record, expected_permission)
         # data files
         data_file = os.path.join(self.scheme.data, 'my_data', 'data_file')
         assert os.path.isfile(data_file)
@@ -268,7 +268,28 @@ def test_std_install(self, data, tmpdir):
             scheme=self.scheme,
             req_description=str(self.req),
         )
-        self.assert_installed()
+        self.assert_installed(0o644)
+
+    @pytest.mark.parametrize("user_mask, expected_permission", [
+        (0o27, 0o640)
+    ])
+    def test_std_install_with_custom_umask(self, data, tmpdir,
+                                           user_mask, expected_permission):
+        """Test that the files created after install honor the permissions
+        set when the user sets a custom umask"""
+
+        prev_umask = os.umask(user_mask)
+        try:
+            self.prep(data, tmpdir)
+            wheel.install_wheel(
+                self.name,
+                self.wheelpath,
+                scheme=self.scheme,
+                req_description=str(self.req),
+            )
+            self.assert_installed(expected_permission)
+        finally:
+            os.umask(prev_umask)
 
     def test_std_install_with_direct_url(self, data, tmpdir):
         """Test that install_wheel creates direct_url.json metadata when
@@ -340,7 +361,7 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir):
             req_description=str(self.req),
             _temp_dir_for_testing=self.src,
         )
-        self.assert_installed()
+        self.assert_installed(0o644)
         assert not os.path.isdir(
             os.path.join(self.dest_dist_info, 'empty_dir'))