Repository: ipython/ipython

Issue:
Do not accept an ipython_dir that's not writeable
As reported by Eric Jones, a colleague found himself with this problem:

Pylab failed to start after an EPD install, and he was dead in the water. 

After poking around, he has a HOMESHARE variable that is set to some strange directory he doesn't know about and also that he does not have write access to.  As a result, ipython will not start up.  This is a show stopper.

We should add a patch that checks if the get_ipython_dir() is writable.  If it isn't, we should use the temp dir or something, or we should not try to write the ipythonrc file, or something.  We want to ensure that file permissions don't result in the inabliity to start using the app.

Jason reported progress on the issue:

Ilan and I just looked at it and it appears to be a simple fix.  path.py get_home_dir() checks a bunch of environment variables on different platforms looking for a home directory.  It checks that the directory exists, but not that it's writable.  We can just add check to issue a warning if a given directory isn't writable and move on to the next option.



Discussion:

Resolving patch:
diff --git a/IPython/utils/path.py b/IPython/utils/path.py
--- a/IPython/utils/path.py
+++ b/IPython/utils/path.py
@@ -16,6 +16,7 @@
 
 import os
 import sys
+import tempfile
 from hashlib import md5
 
 import IPython
@@ -41,6 +42,10 @@ def _get_long_path_name(path):
     """Dummy no-op."""
     return path
 
+def _writable_dir(path):
+    """Whether `path` is a directory, to which the user has write access."""
+    return os.path.isdir(path) and os.access(path, os.W_OK)
+
 if sys.platform == 'win32':
     def _get_long_path_name(path):
         """Get a long path name (expand ~) on Windows using ctypes.
@@ -166,7 +171,6 @@ def get_home_dir():
     raised for all other OSes.
     """
 
-    isdir = os.path.isdir
     env = os.environ
 
     # first, check py2exe distribution root directory for _ipython.
@@ -178,7 +182,7 @@ def get_home_dir():
         else: 
             root=os.path.join(os.path.split(IPython.__file__)[0],"../../")
         root=os.path.abspath(root).rstrip('\\')
-        if isdir(os.path.join(root, '_ipython')):
+        if _writable_dir(os.path.join(root, '_ipython')):
             os.environ["IPYKITROOT"] = root
         return _cast_unicode(root, fs_encoding)
 
@@ -212,7 +216,7 @@ def get_home_dir():
         except KeyError:
             pass
         else:
-            if isdir(homedir):
+            if _writable_dir(homedir):
                 return _cast_unicode(homedir, fs_encoding)
 
         # Now look for a local home directory
@@ -221,7 +225,7 @@ def get_home_dir():
         except KeyError:
             pass
         else:
-            if isdir(homedir):
+            if _writable_dir(homedir):
                 return _cast_unicode(homedir, fs_encoding)
 
         # Now the users profile directory
@@ -230,7 +234,7 @@ def get_home_dir():
         except KeyError:
             pass
         else:
-            if isdir(homedir):
+            if _writable_dir(homedir):
                 return _cast_unicode(homedir, fs_encoding)
 
         # Use the registry to get the 'My Documents' folder.
@@ -245,7 +249,7 @@ def get_home_dir():
         except:
             pass
         else:
-            if isdir(homedir):
+            if _writable_dir(homedir):
                 return _cast_unicode(homedir, fs_encoding)
 
         # A user with a lot of unix tools in win32 may have defined $HOME.
@@ -255,7 +259,7 @@ def get_home_dir():
         except KeyError:
             pass
         else:
-            if isdir(homedir):
+            if _writable_dir(homedir):
                 return _cast_unicode(homedir, fs_encoding)
 
         # If all else fails, raise HomeDirError
@@ -272,14 +276,13 @@ def get_xdg_dir():
     This is only for posix (Linux,Unix,OS X, etc) systems.
     """
 
-    isdir = os.path.isdir
     env = os.environ
     
     if os.name == 'posix':
         # Linux, Unix, AIX, OS X
         # use ~/.config if not set OR empty
         xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
-        if xdg and isdir(xdg):
+        if xdg and _writable_dir(xdg):
             return _cast_unicode(xdg, fs_encoding)
     
     return None
@@ -294,7 +297,7 @@ def get_ipython_dir():
     
     env = os.environ
     pjoin = os.path.join
-    exists = os.path.exists
+    
     
     ipdir_def = '.ipython'
     xdg_def = 'ipython'
@@ -312,7 +315,7 @@ def get_ipython_dir():
             
             xdg_ipdir = pjoin(xdg_dir, xdg_def)
         
-            if exists(xdg_ipdir) or not exists(home_ipdir):
+            if _writable_dir(xdg_ipdir) or not _writable_dir(home_ipdir):
                 ipdir = xdg_ipdir
         
         if ipdir is None:
@@ -320,6 +323,19 @@ def get_ipython_dir():
             ipdir = home_ipdir
 
     ipdir = os.path.normpath(os.path.expanduser(ipdir))
+    
+    if os.path.exists(ipdir) and not _writable_dir(ipdir):
+        # ipdir exists, but is not writable
+        warn.warn("IPython dir '%s' is not a writable location,"
+                        " using a temp directory."%ipdir)
+        ipdir = tempfile.mkdtemp()
+    elif not os.path.exists(ipdir):
+        parent = ipdir.rsplit(os.path.sep, 1)[0]
+        if not _writable_dir(parent):
+            # ipdir does not exist and parent isn't writable
+            warn.warn("IPython parent '%s' is not a writable location,"
+                        " using a temp directory."%parent)
+            ipdir = tempfile.mkdtemp()
 
     return _cast_unicode(ipdir, fs_encoding)
 


Test patch:
diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py
--- a/IPython/utils/tests/test_path.py
+++ b/IPython/utils/tests/test_path.py
@@ -16,6 +16,7 @@
 import shutil
 import sys
 import tempfile
+import StringIO
 
 from os.path import join, abspath, split
 
@@ -26,7 +27,7 @@
 import IPython
 from IPython.testing import decorators as dec
 from IPython.testing.decorators import skip_if_not_win32, skip_win32
-from IPython.utils import path
+from IPython.utils import path, io
 
 # Platform-dependent imports
 try:
@@ -92,7 +93,8 @@ def teardown_environment():
     """Restore things that were remebered by the setup_environment function
     """
     (oldenv, os.name, path.get_home_dir, IPython.__file__,) = oldstuff
-        
+    reload(path)
+    
     for key in env.keys():
         if key not in oldenv:
             del env[key]
@@ -229,6 +231,7 @@ def QueryValueEx(x, y):
 def test_get_ipython_dir_1():
     """test_get_ipython_dir_1, Testcase to see if we can call get_ipython_dir without Exceptions."""
     env_ipdir = os.path.join("someplace", ".ipython")
+    path._writable_dir = lambda path: True
     env['IPYTHON_DIR'] = env_ipdir
     ipdir = path.get_ipython_dir()
     nt.assert_equal(ipdir, env_ipdir)
@@ -238,6 +241,8 @@ def test_get_ipython_dir_1():
 def test_get_ipython_dir_2():
     """test_get_ipython_dir_2, Testcase to see if we can call get_ipython_dir without Exceptions."""
     path.get_home_dir = lambda : "someplace"
+    path.get_xdg_dir = lambda : None
+    path._writable_dir = lambda path: True
     os.name = "posix"
     env.pop('IPYTHON_DIR', None)
     env.pop('IPYTHONDIR', None)
@@ -249,6 +254,7 @@ def test_get_ipython_dir_2():
 def test_get_ipython_dir_3():
     """test_get_ipython_dir_3, use XDG if defined, and .ipython doesn't exist."""
     path.get_home_dir = lambda : "someplace"
+    path._writable_dir = lambda path: True
     os.name = "posix"
     env.pop('IPYTHON_DIR', None)
     env.pop('IPYTHONDIR', None)
@@ -283,18 +289,23 @@ def test_get_ipython_dir_5():
 @with_environment
 def test_get_ipython_dir_6():
     """test_get_ipython_dir_6, use XDG if defined and neither exist."""
-    path.get_home_dir = lambda : 'somehome'
-    path.get_xdg_dir = lambda : 'somexdg'
+    xdg = os.path.join(HOME_TEST_DIR, 'somexdg')
+    os.mkdir(xdg)
+    shutil.rmtree(os.path.join(HOME_TEST_DIR, '.ipython'))
+    path.get_home_dir = lambda : HOME_TEST_DIR
+    path.get_xdg_dir = lambda : xdg
     os.name = "posix"
     env.pop('IPYTHON_DIR', None)
     env.pop('IPYTHONDIR', None)
-    xdg_ipdir = os.path.join("somexdg", "ipython")
+    env.pop('XDG_CONFIG_HOME', None)
+    xdg_ipdir = os.path.join(xdg, "ipython")
     ipdir = path.get_ipython_dir()
     nt.assert_equal(ipdir, xdg_ipdir)
 
 @with_environment
 def test_get_ipython_dir_7():
     """test_get_ipython_dir_7, test home directory expansion on IPYTHON_DIR"""
+    path._writable_dir = lambda path: True
     home_dir = os.path.expanduser('~')
     env['IPYTHON_DIR'] = os.path.join('~', 'somewhere')
     ipdir = path.get_ipython_dir()
@@ -305,6 +316,7 @@ def test_get_ipython_dir_7():
 def test_get_xdg_dir_1():
     """test_get_xdg_dir_1, check xdg_dir"""
     reload(path)
+    path._writable_dir = lambda path: True
     path.get_home_dir = lambda : 'somewhere'
     os.name = "posix"
     env.pop('IPYTHON_DIR', None)
@@ -369,3 +381,24 @@ def test_get_long_path_name():
     p = path.get_long_path_name('/usr/local')
     nt.assert_equals(p,'/usr/local')
 
+@dec.skip_win32 # can't create not-user-writable dir on win
+@with_environment
+def test_not_writable_ipdir():
+    tmpdir = tempfile.mkdtemp()
+    os.name = "posix"
+    env.pop('IPYTHON_DIR', None)
+    env.pop('IPYTHONDIR', None)
+    env.pop('XDG_CONFIG_HOME', None)
+    env['HOME'] = tmpdir
+    ipdir = os.path.join(tmpdir, '.ipython')
+    os.mkdir(ipdir)
+    os.chmod(ipdir, 600)
+    stderr = io.stderr
+    pipe = StringIO.StringIO()
+    io.stderr = pipe
+    ipdir = path.get_ipython_dir()
+    io.stderr.flush()
+    io.stderr = stderr
+    nt.assert_true('WARNING' in pipe.getvalue())
+    env.pop('IPYTHON_DIR', None)
+    
\ No newline at end of file