123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- # encoding: utf-8
- """Path utility functions."""
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- # Derived from IPython.utils.path, which is
- # Copyright (c) IPython Development Team.
- # Distributed under the terms of the Modified BSD License.
- import os
- import sys
- import stat
- import errno
- import tempfile
- import warnings
- from ipython_genutils import py3compat
- from contextlib import contextmanager
- from distutils.util import strtobool
- from ipython_genutils import py3compat
- pjoin = os.path.join
- # UF_HIDDEN is a stat flag not defined in the stat module.
- # It is used by BSD to indicate hidden files.
- UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
- def get_home_dir():
- """Get the real path of the home directory"""
- homedir = os.path.expanduser('~')
- # Next line will make things work even when /home/ is a symlink to
- # /usr/home as it is on FreeBSD, for example
- homedir = os.path.realpath(homedir)
- homedir = py3compat.str_to_unicode(homedir, encoding=sys.getfilesystemencoding())
- return homedir
- _dtemps = {}
- def _mkdtemp_once(name):
- """Make or reuse a temporary directory.
- If this is called with the same name in the same process, it will return
- the same directory.
- """
- try:
- return _dtemps[name]
- except KeyError:
- d = _dtemps[name] = tempfile.mkdtemp(prefix=name + '-')
- return d
- def jupyter_config_dir():
- """Get the Jupyter config directory for this platform and user.
- Returns JUPYTER_CONFIG_DIR if defined, else ~/.jupyter
- """
- env = os.environ
- home_dir = get_home_dir()
- if env.get('JUPYTER_NO_CONFIG'):
- return _mkdtemp_once('jupyter-clean-cfg')
- if env.get('JUPYTER_CONFIG_DIR'):
- return env['JUPYTER_CONFIG_DIR']
- return pjoin(home_dir, '.jupyter')
- def jupyter_data_dir():
- """Get the config directory for Jupyter data files.
- These are non-transient, non-configuration files.
- Returns JUPYTER_DATA_DIR if defined, else a platform-appropriate path.
- """
- env = os.environ
- if env.get('JUPYTER_DATA_DIR'):
- return env['JUPYTER_DATA_DIR']
- home = get_home_dir()
- if sys.platform == 'darwin':
- return os.path.join(home, 'Library', 'Jupyter')
- elif os.name == 'nt':
- appdata = os.environ.get('APPDATA', None)
- if appdata:
- return pjoin(appdata, 'jupyter')
- else:
- return pjoin(jupyter_config_dir(), 'data')
- else:
- # Linux, non-OS X Unix, AIX, etc.
- xdg = env.get("XDG_DATA_HOME", None)
- if not xdg:
- xdg = pjoin(home, '.local', 'share')
- return pjoin(xdg, 'jupyter')
- def jupyter_runtime_dir():
- """Return the runtime dir for transient jupyter files.
- Returns JUPYTER_RUNTIME_DIR if defined.
- The default is now (data_dir)/runtime on all platforms;
- we no longer use XDG_RUNTIME_DIR after various problems.
- """
- env = os.environ
- if env.get('JUPYTER_RUNTIME_DIR'):
- return env['JUPYTER_RUNTIME_DIR']
- return pjoin(jupyter_data_dir(), 'runtime')
- if os.name == 'nt':
- programdata = os.environ.get('PROGRAMDATA', None)
- if programdata:
- SYSTEM_JUPYTER_PATH = [pjoin(programdata, 'jupyter')]
- else: # PROGRAMDATA is not defined by default on XP.
- SYSTEM_JUPYTER_PATH = [os.path.join(sys.prefix, 'share', 'jupyter')]
- else:
- SYSTEM_JUPYTER_PATH = [
- "/usr/local/share/jupyter",
- "/usr/share/jupyter",
- ]
- ENV_JUPYTER_PATH = [os.path.join(sys.prefix, 'share', 'jupyter')]
- def jupyter_path(*subdirs):
- """Return a list of directories to search for data files
- JUPYTER_PATH environment variable has highest priority.
- If ``*subdirs`` are given, that subdirectory will be added to each element.
- Examples:
- >>> jupyter_path()
- ['~/.local/jupyter', '/usr/local/share/jupyter']
- >>> jupyter_path('kernels')
- ['~/.local/jupyter/kernels', '/usr/local/share/jupyter/kernels']
- """
- paths = []
- # highest priority is env
- if os.environ.get('JUPYTER_PATH'):
- paths.extend(
- p.rstrip(os.sep)
- for p in os.environ['JUPYTER_PATH'].split(os.pathsep)
- )
- # then user dir
- paths.append(jupyter_data_dir())
- # then sys.prefix
- for p in ENV_JUPYTER_PATH:
- if p not in SYSTEM_JUPYTER_PATH:
- paths.append(p)
- # finally, system
- paths.extend(SYSTEM_JUPYTER_PATH)
- # add subdir, if requested
- if subdirs:
- paths = [ pjoin(p, *subdirs) for p in paths ]
- return paths
- if os.name == 'nt':
- programdata = os.environ.get('PROGRAMDATA', None)
- if programdata:
- SYSTEM_CONFIG_PATH = [os.path.join(programdata, 'jupyter')]
- else: # PROGRAMDATA is not defined by default on XP.
- SYSTEM_CONFIG_PATH = []
- else:
- SYSTEM_CONFIG_PATH = [
- "/usr/local/etc/jupyter",
- "/etc/jupyter",
- ]
- ENV_CONFIG_PATH = [os.path.join(sys.prefix, 'etc', 'jupyter')]
- def jupyter_config_path():
- """Return the search path for Jupyter config files as a list."""
- paths = [jupyter_config_dir()]
- if os.environ.get('JUPYTER_NO_CONFIG'):
- return paths
- # highest priority is env
- if os.environ.get('JUPYTER_CONFIG_PATH'):
- paths.extend(
- p.rstrip(os.sep)
- for p in os.environ['JUPYTER_CONFIG_PATH'].split(os.pathsep)
- )
- # then sys.prefix
- for p in ENV_CONFIG_PATH:
- if p not in SYSTEM_CONFIG_PATH:
- paths.append(p)
- paths.extend(SYSTEM_CONFIG_PATH)
- return paths
- def exists(path):
- """Replacement for `os.path.exists` which works for host mapped volumes
- on Windows containers
- """
- try:
- os.lstat(path)
- except OSError:
- return False
- return True
- def is_file_hidden_win(abs_path, stat_res=None):
- """Is a file hidden?
- This only checks the file itself; it should be called in combination with
- checking the directory containing the file.
- Use is_hidden() instead to check the file and its parent directories.
- Parameters
- ----------
- abs_path : unicode
- The absolute path to check.
- stat_res : os.stat_result, optional
- Ignored on Windows, exists for compatibility with POSIX version of the
- function.
- """
- if os.path.basename(abs_path).startswith('.'):
- return True
- win32_FILE_ATTRIBUTE_HIDDEN = 0x02
- import ctypes
- try:
- attrs = ctypes.windll.kernel32.GetFileAttributesW(
- py3compat.cast_unicode(abs_path)
- )
- except AttributeError:
- pass
- else:
- if attrs > 0 and attrs & win32_FILE_ATTRIBUTE_HIDDEN:
- return True
- return False
- def is_file_hidden_posix(abs_path, stat_res=None):
- """Is a file hidden?
- This only checks the file itself; it should be called in combination with
- checking the directory containing the file.
- Use is_hidden() instead to check the file and its parent directories.
- Parameters
- ----------
- abs_path : unicode
- The absolute path to check.
- stat_res : os.stat_result, optional
- The result of calling stat() on abs_path. If not passed, this function
- will call stat() internally.
- """
- if os.path.basename(abs_path).startswith('.'):
- return True
- if stat_res is None or stat.S_ISLNK(stat_res.st_mode):
- try:
- stat_res = os.stat(abs_path)
- except OSError as e:
- if e.errno == errno.ENOENT:
- return False
- raise
- # check that dirs can be listed
- if stat.S_ISDIR(stat_res.st_mode):
- # use x-access, not actual listing, in case of slow/large listings
- if not os.access(abs_path, os.X_OK | os.R_OK):
- return True
- # check UF_HIDDEN
- if getattr(stat_res, 'st_flags', 0) & UF_HIDDEN:
- return True
- return False
- if sys.platform == 'win32':
- is_file_hidden = is_file_hidden_win
- else:
- is_file_hidden = is_file_hidden_posix
- def is_hidden(abs_path, abs_root=''):
- """Is a file hidden or contained in a hidden directory?
- This will start with the rightmost path element and work backwards to the
- given root to see if a path is hidden or in a hidden directory. Hidden is
- determined by either name starting with '.' or the UF_HIDDEN flag as
- reported by stat.
- If abs_path is the same directory as abs_root, it will be visible even if
- that is a hidden folder. This only checks the visibility of files
- and directories *within* abs_root.
- Parameters
- ----------
- abs_path : unicode
- The absolute path to check for hidden directories.
- abs_root : unicode
- The absolute path of the root directory in which hidden directories
- should be checked for.
- """
- if os.path.normpath(abs_path) == os.path.normpath(abs_root):
- return False
- if is_file_hidden(abs_path):
- return True
- if not abs_root:
- abs_root = abs_path.split(os.sep, 1)[0] + os.sep
- inside_root = abs_path[len(abs_root):]
- if any(part.startswith('.') for part in inside_root.split(os.sep)):
- return True
- # check UF_HIDDEN on any location up to root.
- # is_file_hidden() already checked the file, so start from its parent dir
- path = os.path.dirname(abs_path)
- while path and path.startswith(abs_root) and path != abs_root:
- if not exists(path):
- path = os.path.dirname(path)
- continue
- try:
- # may fail on Windows junctions
- st = os.lstat(path)
- except OSError:
- return True
- if getattr(st, 'st_flags', 0) & UF_HIDDEN:
- return True
- path = os.path.dirname(path)
- return False
- def win32_restrict_file_to_user(fname):
- """Secure a windows file to read-only access for the user.
- Follows guidance from win32 library creator:
- http://timgolden.me.uk/python/win32_how_do_i/add-security-to-a-file.html
- This method should be executed against an already generated file which
- has no secrets written to it yet.
- Parameters
- ----------
- fname : unicode
- The path to the file to secure
- """
- import win32api
- import win32security
- import ntsecuritycon as con
- # everyone, _domain, _type = win32security.LookupAccountName("", "Everyone")
- admins = win32security.CreateWellKnownSid(win32security.WinBuiltinAdministratorsSid)
- user, _domain, _type = win32security.LookupAccountName("", win32api.GetUserNameEx(win32api.NameSamCompatible))
- sd = win32security.GetFileSecurity(fname, win32security.DACL_SECURITY_INFORMATION)
- dacl = win32security.ACL()
- # dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, everyone)
- dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_GENERIC_READ | con.FILE_GENERIC_WRITE, user)
- dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, admins)
- sd.SetSecurityDescriptorDacl(1, dacl, 0)
- win32security.SetFileSecurity(fname, win32security.DACL_SECURITY_INFORMATION, sd)
- def get_file_mode(fname):
- """Retrieves the file mode corresponding to fname in a filesystem-tolerant manner.
- Parameters
- ----------
- fname : unicode
- The path to the file to get mode from
- """
- # Some filesystems (e.g., CIFS) auto-enable the execute bit on files. As a result, we
- # should tolerate the execute bit on the file's owner when validating permissions - thus
- # the missing least significant bit on the third octal digit. In addition, we also tolerate
- # the sticky bit being set, so the lsb from the fourth octal digit is also removed.
- return stat.S_IMODE(os.stat(fname).st_mode) & 0o6677 # Use 4 octal digits since S_IMODE does the same
- allow_insecure_writes = strtobool(os.getenv('JUPYTER_ALLOW_INSECURE_WRITES', 'false'))
- @contextmanager
- def secure_write(fname, binary=False):
- """Opens a file in the most restricted pattern available for
- writing content. This limits the file mode to `0o0600` and yields
- the resulting opened filed handle.
- Parameters
- ----------
- fname : unicode
- The path to the file to write
- binary: boolean
- Indicates that the file is binary
- """
- mode = 'wb' if binary else 'w'
- open_flag = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
- try:
- os.remove(fname)
- except (IOError, OSError):
- # Skip any issues with the file not existing
- pass
- if os.name == 'nt':
- if allow_insecure_writes:
- # Mounted file systems can have a number of failure modes inside this block.
- # For windows machines in insecure mode we simply skip this to avoid failures :/
- issue_insecure_write_warning()
- else:
- # Python on windows does not respect the group and public bits for chmod, so we need
- # to take additional steps to secure the contents.
- # Touch file pre-emptively to avoid editing permissions in open files in Windows
- fd = os.open(fname, open_flag, 0o0600)
- os.close(fd)
- open_flag = os.O_WRONLY | os.O_TRUNC
- win32_restrict_file_to_user(fname)
- with os.fdopen(os.open(fname, open_flag, 0o0600), mode) as f:
- if os.name != 'nt':
- # Enforce that the file got the requested permissions before writing
- file_mode = get_file_mode(fname)
- if 0o0600 != file_mode:
- if allow_insecure_writes:
- issue_insecure_write_warning()
- else:
- raise RuntimeError("Permissions assignment failed for secure file: '{file}'."
- " Got '{permissions}' instead of '0o0600'."
- .format(file=fname, permissions=oct(file_mode)))
- yield f
- def issue_insecure_write_warning():
- def format_warning(msg, *args, **kwargs):
- return str(msg) + '\n'
- warnings.formatwarning = format_warning
- warnings.warn("WARNING: Insecure writes have been enabled via environment variable "
- "'JUPYTER_ALLOW_INSECURE_WRITES'! If this is not intended, remove the "
- "variable or set its value to 'False'.")
|