Repository: pypa/pip

Issue:
Sudo usage breaks pip.
Any usage of sudo + pip (not in a venv) seems to break pip due to the new temporary directory check & mkdir creation. A simple easily reproducible example that includes one command and a second command that will break due to the new directory uid check (which doesn't play nice with sudo).

It would seem that --help (and likely other commands) should work for all users, and not require write access to some temporary folder for building (when --help is not a build command). 

```
$ pip-python --version
pip 1.3.1 from /usr/lib/python2.6/site-packages (python 2.6)
$ sudo pip-python --help
The temporary folder for building (/tmp/pip-build-harlowja) is not owned by your user!
pip will not work until the temporary folder is either deleted or owned by your user account.
Traceback (most recent call last):
  File "/usr/bin/pip-python", line 9, in <module>
    load_entry_point('pip==1.3.1', 'console_scripts', 'pip')()
  File "/usr/lib/python2.6/site-packages/pkg_resources.py", line 343, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "/usr/lib/python2.6/site-packages/pkg_resources.py", line 2354, in load_entry_point
    return ep.load()
  File "/usr/lib/python2.6/site-packages/pkg_resources.py", line 2060, in load
    entry = __import__(self.module_name, globals(),globals(), ['__name__'])
  File "/usr/lib/python2.6/site-packages/pip/__init__.py", line 9, in <module>
    from pip.util import get_installed_distributions, get_prog
  File "/usr/lib/python2.6/site-packages/pip/util.py", line 15, in <module>
    from pip.locations import site_packages, running_under_virtualenv, virtualenv_no_global
  File "/usr/lib/python2.6/site-packages/pip/locations.py", line 64, in <module>
    build_prefix = _get_build_prefix()
  File "/usr/lib/python2.6/site-packages/pip/locations.py", line 54, in _get_build_prefix
    raise pip.exceptions.InstallationError(msg)
pip.exceptions.InstallationError: The temporary folder for building (/tmp/pip-build-harlowja) is not owned by your user!
```

It also works the other way around (which is likely even worse since now any pip command the user will try to do will not work from here on out due to the user not being able to write a directory that is root owned).

```
$ sudo pip-python --version
pip 1.3.1 from /usr/lib/python2.6/site-packages (python 2.6)
$ pip-python --help
The temporary folder for building (/tmp/pip-build-harlowja) is not owned by your user!
pip will not work until the temporary folder is either deleted or owned by your user account.
Traceback (most recent call last):
  File "/usr/bin/pip-python", line 9, in <module>
    load_entry_point('pip==1.3.1', 'console_scripts', 'pip')()
  File "/usr/lib/python2.6/site-packages/pkg_resources.py", line 343, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "/usr/lib/python2.6/site-packages/pkg_resources.py", line 2354, in load_entry_point
    return ep.load()
  File "/usr/lib/python2.6/site-packages/pkg_resources.py", line 2060, in load
    entry = __import__(self.module_name, globals(),globals(), ['__name__'])
  File "/usr/lib/python2.6/site-packages/pip/__init__.py", line 9, in <module>
    from pip.util import get_installed_distributions, get_prog
  File "/usr/lib/python2.6/site-packages/pip/util.py", line 15, in <module>
    from pip.locations import site_packages, running_under_virtualenv, virtualenv_no_global
  File "/usr/lib/python2.6/site-packages/pip/locations.py", line 64, in <module>
    build_prefix = _get_build_prefix()
  File "/usr/lib/python2.6/site-packages/pip/locations.py", line 54, in _get_build_prefix
    raise pip.exceptions.InstallationError(msg)
pip.exceptions.InstallationError: The temporary folder for building (/tmp/pip-build-harlowja) is not owned by your user!
```



Discussion:
related comment: https://github.com/pypa/pip/pull/780/files#r4330003

2 side points:
1. the fact that the build dir is created/confirmed on import (vs when it's actually needed) was brought up during the initial review, but it ended up being merged this way.  
2. these fixed build dirs are likely to go away with #906 anyway.

Thanks, very good to know its going to be fixed. 

cc @d1b

this problem is not universal.   I don't have this issue on ubuntu or centos.
what distro are you on?

can you give me the results from these:

`sudo python -c "import getpass; print(getpass.getuser())"`
`sudo python -c "import os; print(os.environ.get('LOGNAME'))"`
`sudo python -c "import pwd, os; print(pwd.getpwuid(os.geteuid()).pw_name)"`

This is RHEL6.2

-bash-4.1$ sudo python -c "import getpass; print(getpass.getuser())"
harlowja
-bash-4.1$ sudo python -c "import os; print(os.environ.get('LOGNAME'))"
harlowja
-bash-4.1$ sudo python -c "import pwd, os; print(pwd.getpwuid(os.geteuid()).pw_name)"
root

thanks @harlowja .  all my answers were 'root'.
I guess for unix, we can use `pwd.getpwuid(os.geteuid()).pw_name`.
for windows, it has to be `getpass.getuser()`, since  geteuid only works on unix.
I'll post a pull later and have you confirm it works.

Ya, thats pretty weird. I wonder if since RHEL6.2 uses 2.6 that 2.6 has this behavior? But then centos should also. Pretty odd...

@qwcode this is very interesting indeed. On ubuntu 13.04 with python 2.7.4, 
sudo python -c "import getpass;print(getpass.getuser())"

prints out 'root'. It would seem that 'sudo' on some systems end up with environmental variables that differ from the norm. So we should change it to be`pwd.getpwuid(os.geteuid()).pw_name` on unix. In addition, we will need to  change the `if file_uid != os.getuid():` check.

Same issue for me on OSX. Build dirs are created as root if running via sudo. I was going to write an issue but this seems to cover it. In this case I was using virtualenv and virtualenvwrapper, I accidently used sudo pip install markdown, Ctrl+c'ed quickly, tried the command without sudo and sure enough I couldn't write to the build dir.

@russkel what's the output you see (from pip) ? 

@d1b this is what it chucked out. I rm'ed the dir as sudo and I am on my merry way.

```
(project)Samorost:project russ$ pip install markdown
Downloading/unpacking markdown
  Downloading Markdown-2.3.1.tar.gz (267kB): 267kB downloaded
Exception:
Traceback (most recent call last):
  File "/Users/russ/.virtualenvs/project/lib/python2.7/site-packages/pip-1.3.1-py2.7.egg/pip/basecommand.py", line 139, in main
    status = self.run(options, args)
  File "/Users/russ/.virtualenvs/project/lib/python2.7/site-packages/pip-1.3.1-py2.7.egg/pip/commands/install.py", line 266, in run
    requirement_set.prepare_files(finder, force_root_egg_info=self.bundle, bundle=self.bundle)
  File "/Users/russ/.virtualenvs/project/lib/python2.7/site-packages/pip-1.3.1-py2.7.egg/pip/req.py", line 1033, in prepare_files
    self.unpack_url(url, location, self.is_download)
  File "/Users/russ/.virtualenvs/project/lib/python2.7/site-packages/pip-1.3.1-py2.7.egg/pip/req.py", line 1161, in unpack_url
    retval = unpack_http_url(link, location, self.download_cache, self.download_dir)
  File "/Users/russ/.virtualenvs/project/lib/python2.7/site-packages/pip-1.3.1-py2.7.egg/pip/download.py", line 559, in unpack_http_url
    unpack_file(temp_location, location, content_type, link)
  File "/Users/russ/.virtualenvs/project/lib/python2.7/site-packages/pip-1.3.1-py2.7.egg/pip/util.py", line 590, in unpack_file
    untar_file(filename, location)
  File "/Users/russ/.virtualenvs/project/lib/python2.7/site-packages/pip-1.3.1-py2.7.egg/pip/util.py", line 504, in untar_file
    os.makedirs(location)
  File "/Users/russ/.virtualenvs/project/bin/../lib/python2.7/os.py", line 157, in makedirs
    mkdir(name, mode)
OSError: [Errno 13] Permission denied: '/Users/russ/.virtualenvs/project/build/markdown'
```

From my shell backlog:
`drwxr-xr-x   3 root  staff  102 11 Jun 18:57 build`

@russkel the issue you hit is not related to this issue (afaik).

@d1b  I started to make the change to use `pwd.getpwuid(os.geteuid()).pw_name` on unix, but ran into needing to fix the mocking in your tests, and didn't have the time.  feel free to submit a pull, with the test fixes, and it will be a quick merge.

@qwcode sure thing.


Resolving patch:
diff --git a/pip/locations.py b/pip/locations.py
--- a/pip/locations.py
+++ b/pip/locations.py
@@ -48,10 +48,17 @@ def virtualenv_no_global():
     if running_under_virtualenv() and os.path.isfile(no_global_file):
         return True
 
+def __get_username():
+    """ Returns the effective username of the current process. """
+    if sys.platform == 'win32':
+        return getpass.getuser()
+    import pwd
+    return pwd.getpwuid(os.geteuid()).pw_name
+
 def _get_build_prefix():
     """ Returns a safe build_prefix """
-    path = os.path.join(tempfile.gettempdir(), 'pip-build-%s' % \
-        getpass.getuser())
+    path = os.path.join(tempfile.gettempdir(), 'pip-build-%s' %
+        __get_username())
     if sys.platform == 'win32':
         """ on windows(tested on 7) temp dirs are isolated """
         return path
@@ -66,7 +73,7 @@ def _get_build_prefix():
             os.close(fd)
         except OSError:
             file_uid = None
-        if file_uid != os.getuid():
+        if file_uid != os.geteuid():
             msg = "The temporary folder for building (%s) is not owned by your user!" \
                 % path
             print (msg)


Test patch:
diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py
--- a/tests/unit/test_locations.py
+++ b/tests/unit/test_locations.py
@@ -9,9 +9,15 @@
 import getpass
 from mock import Mock
 from nose import SkipTest
-from nose.tools import assert_raises
+from nose.tools import assert_raises, assert_equal
 import pip
 
+if sys.platform == 'win32':
+   pwd = Mock()
+else:
+    import pwd
+
+
 class TestLocations:
     def setup(self):
         self.tempdir = tempfile.mkdtemp()
@@ -28,23 +34,28 @@ def patch(self):
         self.tempfile_gettempdir = tempfile.gettempdir
         self.old_os_fstat = os.fstat
         if sys.platform != 'win32':
-            # os.getuid not implemented on windows
-            self.old_os_getuid = os.getuid
+            # os.geteuid and pwd.getpwuid are not implemented on windows
+            self.old_os_geteuid = os.geteuid
+            self.old_pwd_getpwuid = pwd.getpwuid
         self.old_getpass_getuser = getpass.getuser
 
         # now patch
         tempfile.gettempdir = lambda : self.tempdir
         getpass.getuser = lambda : self.username
-        os.getuid = lambda : self.st_uid
+        os.geteuid = lambda : self.st_uid
         os.fstat = lambda fd : self.get_mock_fstat(fd)
 
+        if sys.platform != 'win32':
+            pwd.getpwuid = lambda uid: self.get_mock_getpwuid(uid)
+
     def revert_patch(self):
         """ revert the patches to python methods """
         tempfile.gettempdir = self.tempfile_gettempdir
         getpass.getuser = self.old_getpass_getuser
         if sys.platform != 'win32':
-            # os.getuid not implemented on windows
-            os.getuid = self.old_os_getuid
+            # os.geteuid and pwd.getpwuid are not implemented on windows
+            os.geteuid = self.old_os_geteuid
+            pwd.getpwuid = self.old_pwd_getpwuid
         os.fstat = self.old_os_fstat
 
     def get_mock_fstat(self, fd):
@@ -55,6 +66,14 @@ def get_mock_fstat(self, fd):
         result.st_uid = self.st_uid
         return result
 
+    def get_mock_getpwuid(self, uid):
+        """ returns a basic mock pwd.getpwuid call result.
+            Currently only the pw_name attribute has been set.
+        """
+        result = Mock()
+        result.pw_name = self.username
+        return result
+
     def get_build_dir_location(self):
         """ returns a string pointing to the
             current build_prefix.
@@ -65,7 +84,8 @@ def test_dir_path(self):
         """ test the path name for the build_prefix
         """
         from pip import locations
-        assert locations._get_build_prefix() == self.get_build_dir_location()
+        assert_equal(locations._get_build_prefix(),
+            self.get_build_dir_location())
 
     def test_dir_created(self):
         """ test that the build_prefix directory is generated when
@@ -89,7 +109,7 @@ def test_error_raised_when_owned_by_another(self):
         if sys.platform == 'win32':
             raise SkipTest()
         from pip import locations
-        os.getuid = lambda : 1111
+        os.geteuid = lambda : 1111
         os.mkdir(self.get_build_dir_location() )
         assert_raises(pip.exceptions.InstallationError, locations._get_build_prefix)