|
"""Path utility functions.""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations |
|
|
|
import errno |
|
import os |
|
import site |
|
import stat |
|
import sys |
|
import tempfile |
|
import warnings |
|
from contextlib import contextmanager |
|
from pathlib import Path |
|
from typing import Any, Iterator, Optional |
|
|
|
import platformdirs |
|
|
|
from .utils import deprecation |
|
|
|
pjoin = os.path.join |
|
|
|
|
|
if sys.platform == "win32" or ( |
|
sys.platform == "darwin" and not sys.prefix.startswith("/opt/homebrew") |
|
): |
|
APPNAME = "Jupyter" |
|
else: |
|
APPNAME = "jupyter" |
|
|
|
|
|
|
|
UF_HIDDEN = getattr(stat, "UF_HIDDEN", 32768) |
|
|
|
|
|
def envset(name: str, default: Optional[bool] = False) -> Optional[bool]: |
|
"""Return the boolean value of a given environment variable. |
|
|
|
An environment variable is considered set if it is assigned to a value |
|
other than 'no', 'n', 'false', 'off', '0', or '0.0' (case insensitive) |
|
|
|
If the environment variable is not defined, the default value is returned. |
|
""" |
|
if name not in os.environ: |
|
return default |
|
|
|
return os.environ[name].lower() not in ["no", "n", "false", "off", "0", "0.0"] |
|
|
|
|
|
def use_platform_dirs() -> bool: |
|
"""Determine if platformdirs should be used for system-specific paths. |
|
|
|
We plan for this to default to False in jupyter_core version 5 and to True |
|
in jupyter_core version 6. |
|
""" |
|
return envset("JUPYTER_PLATFORM_DIRS", False) |
|
|
|
|
|
def get_home_dir() -> str: |
|
"""Get the real path of the home directory""" |
|
homedir = Path("~").expanduser() |
|
|
|
|
|
return str(Path(homedir).resolve()) |
|
|
|
|
|
_dtemps: dict[str, str] = {} |
|
|
|
|
|
def _do_i_own(path: str) -> bool: |
|
"""Return whether the current user owns the given path""" |
|
p = Path(path).resolve() |
|
|
|
|
|
while not p.exists() and p != p.parent: |
|
p = p.parent |
|
|
|
|
|
|
|
try: |
|
return p.owner() == os.getlogin() |
|
except Exception: |
|
pass |
|
|
|
if hasattr(os, "geteuid"): |
|
try: |
|
st = p.stat() |
|
return st.st_uid == os.geteuid() |
|
except (NotImplementedError, OSError): |
|
|
|
pass |
|
|
|
|
|
return os.access(p, os.W_OK) |
|
|
|
|
|
def prefer_environment_over_user() -> bool: |
|
"""Determine if environment-level paths should take precedence over user-level paths.""" |
|
|
|
if "JUPYTER_PREFER_ENV_PATH" in os.environ: |
|
return envset("JUPYTER_PREFER_ENV_PATH") |
|
|
|
|
|
if sys.prefix != sys.base_prefix and _do_i_own(sys.prefix): |
|
return True |
|
|
|
|
|
if ( |
|
"CONDA_PREFIX" in os.environ |
|
and sys.prefix.startswith(os.environ["CONDA_PREFIX"]) |
|
and os.environ.get("CONDA_DEFAULT_ENV", "base") != "base" |
|
and _do_i_own(sys.prefix) |
|
): |
|
return True |
|
|
|
return False |
|
|
|
|
|
def _mkdtemp_once(name: str) -> str: |
|
"""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() -> str: |
|
"""Get the Jupyter config directory for this platform and user. |
|
|
|
Returns JUPYTER_CONFIG_DIR if defined, otherwise the appropriate |
|
directory for the platform. |
|
""" |
|
|
|
env = os.environ |
|
if env.get("JUPYTER_NO_CONFIG"): |
|
return _mkdtemp_once("jupyter-clean-cfg") |
|
|
|
if env.get("JUPYTER_CONFIG_DIR"): |
|
return env["JUPYTER_CONFIG_DIR"] |
|
|
|
if use_platform_dirs(): |
|
return platformdirs.user_config_dir(APPNAME, appauthor=False) |
|
|
|
home_dir = get_home_dir() |
|
return pjoin(home_dir, ".jupyter") |
|
|
|
|
|
def jupyter_data_dir() -> str: |
|
"""Get the config directory for Jupyter data files for this platform and user. |
|
|
|
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"] |
|
|
|
if use_platform_dirs(): |
|
return platformdirs.user_data_dir(APPNAME, appauthor=False) |
|
|
|
home = get_home_dir() |
|
|
|
if sys.platform == "darwin": |
|
return str(Path(home, "Library", "Jupyter")) |
|
if sys.platform == "win32": |
|
appdata = os.environ.get("APPDATA", None) |
|
if appdata: |
|
return str(Path(appdata, "jupyter").resolve()) |
|
return pjoin(jupyter_config_dir(), "data") |
|
|
|
xdg = env.get("XDG_DATA_HOME", None) |
|
if not xdg: |
|
xdg = pjoin(home, ".local", "share") |
|
return pjoin(xdg, "jupyter") |
|
|
|
|
|
def jupyter_runtime_dir() -> str: |
|
"""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 use_platform_dirs(): |
|
SYSTEM_JUPYTER_PATH = platformdirs.site_data_dir( |
|
APPNAME, appauthor=False, multipath=True |
|
).split(os.pathsep) |
|
else: |
|
deprecation( |
|
"Jupyter is migrating its paths to use standard platformdirs\n" |
|
"given by the platformdirs library. To remove this warning and\n" |
|
"see the appropriate new directories, set the environment variable\n" |
|
"`JUPYTER_PLATFORM_DIRS=1` and then run `jupyter --paths`.\n" |
|
"The use of platformdirs will be the default in `jupyter_core` v6" |
|
) |
|
if os.name == "nt": |
|
programdata = os.environ.get("PROGRAMDATA", None) |
|
if programdata: |
|
SYSTEM_JUPYTER_PATH = [pjoin(programdata, "jupyter")] |
|
else: |
|
SYSTEM_JUPYTER_PATH = [str(Path(sys.prefix, "share", "jupyter"))] |
|
else: |
|
SYSTEM_JUPYTER_PATH = [ |
|
"/usr/local/share/jupyter", |
|
"/usr/share/jupyter", |
|
] |
|
|
|
ENV_JUPYTER_PATH: list[str] = [str(Path(sys.prefix, "share", "jupyter"))] |
|
|
|
|
|
def jupyter_path(*subdirs: str) -> list[str]: |
|
"""Return a list of directories to search for data files |
|
|
|
JUPYTER_PATH environment variable has highest priority. |
|
|
|
If the JUPYTER_PREFER_ENV_PATH environment variable is set, the environment-level |
|
directories will have priority over user-level directories. |
|
|
|
If the Python site.ENABLE_USER_SITE variable is True, we also add the |
|
appropriate Python user site subdirectory to the user-level directories. |
|
|
|
|
|
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: list[str] = [] |
|
|
|
|
|
if os.environ.get("JUPYTER_PATH"): |
|
paths.extend(p.rstrip(os.sep) for p in os.environ["JUPYTER_PATH"].split(os.pathsep)) |
|
|
|
|
|
user = [jupyter_data_dir()] |
|
if site.ENABLE_USER_SITE: |
|
|
|
|
|
userbase: Optional[str] |
|
userbase = site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE |
|
|
|
if userbase: |
|
userdir = str(Path(userbase, "share", "jupyter")) |
|
if userdir not in user: |
|
user.append(userdir) |
|
|
|
env = [p for p in ENV_JUPYTER_PATH if p not in SYSTEM_JUPYTER_PATH] |
|
|
|
if prefer_environment_over_user(): |
|
paths.extend(env) |
|
paths.extend(user) |
|
else: |
|
paths.extend(user) |
|
paths.extend(env) |
|
|
|
|
|
paths.extend(SYSTEM_JUPYTER_PATH) |
|
|
|
|
|
if subdirs: |
|
paths = [pjoin(p, *subdirs) for p in paths] |
|
return paths |
|
|
|
|
|
if use_platform_dirs(): |
|
SYSTEM_CONFIG_PATH = platformdirs.site_config_dir( |
|
APPNAME, appauthor=False, multipath=True |
|
).split(os.pathsep) |
|
else: |
|
if os.name == "nt": |
|
programdata = os.environ.get("PROGRAMDATA", None) |
|
if programdata: |
|
SYSTEM_CONFIG_PATH = [str(Path(programdata, "jupyter"))] |
|
else: |
|
SYSTEM_CONFIG_PATH = [] |
|
else: |
|
SYSTEM_CONFIG_PATH = [ |
|
"/usr/local/etc/jupyter", |
|
"/etc/jupyter", |
|
] |
|
ENV_CONFIG_PATH: list[str] = [str(Path(sys.prefix, "etc", "jupyter"))] |
|
|
|
|
|
def jupyter_config_path() -> list[str]: |
|
"""Return the search path for Jupyter config files as a list. |
|
|
|
If the JUPYTER_PREFER_ENV_PATH environment variable is set, the |
|
environment-level directories will have priority over user-level |
|
directories. |
|
|
|
If the Python site.ENABLE_USER_SITE variable is True, we also add the |
|
appropriate Python user site subdirectory to the user-level directories. |
|
""" |
|
if os.environ.get("JUPYTER_NO_CONFIG"): |
|
|
|
return [jupyter_config_dir()] |
|
|
|
paths: list[str] = [] |
|
|
|
|
|
if os.environ.get("JUPYTER_CONFIG_PATH"): |
|
paths.extend(p.rstrip(os.sep) for p in os.environ["JUPYTER_CONFIG_PATH"].split(os.pathsep)) |
|
|
|
|
|
user = [jupyter_config_dir()] |
|
if site.ENABLE_USER_SITE: |
|
userbase: Optional[str] |
|
|
|
|
|
userbase = site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE |
|
|
|
if userbase: |
|
userdir = str(Path(userbase, "etc", "jupyter")) |
|
if userdir not in user: |
|
user.append(userdir) |
|
|
|
env = [p for p in ENV_CONFIG_PATH if p not in SYSTEM_CONFIG_PATH] |
|
|
|
if prefer_environment_over_user(): |
|
paths.extend(env) |
|
paths.extend(user) |
|
else: |
|
paths.extend(user) |
|
paths.extend(env) |
|
|
|
|
|
paths.extend(SYSTEM_CONFIG_PATH) |
|
return paths |
|
|
|
|
|
def exists(path: str) -> bool: |
|
"""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: str, stat_res: Optional[Any] = None) -> bool: |
|
"""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 Path(abs_path).name.startswith("."): |
|
return True |
|
|
|
if stat_res is None: |
|
try: |
|
stat_res = Path(abs_path).stat() |
|
except OSError as e: |
|
if e.errno == errno.ENOENT: |
|
return False |
|
raise |
|
|
|
try: |
|
if ( |
|
stat_res.st_file_attributes |
|
& stat.FILE_ATTRIBUTE_HIDDEN |
|
): |
|
return True |
|
except AttributeError: |
|
|
|
|
|
|
|
warnings.warn( |
|
"hidden files are not detectable on this system, so no file will be marked as hidden.", |
|
stacklevel=2, |
|
) |
|
|
|
return False |
|
|
|
|
|
def is_file_hidden_posix(abs_path: str, stat_res: Optional[Any] = None) -> bool: |
|
"""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 Path(abs_path).name.startswith("."): |
|
return True |
|
|
|
if stat_res is None or stat.S_ISLNK(stat_res.st_mode): |
|
try: |
|
stat_res = Path(abs_path).stat() |
|
except OSError as e: |
|
if e.errno == errno.ENOENT: |
|
return False |
|
raise |
|
|
|
|
|
if stat.S_ISDIR(stat_res.st_mode): |
|
|
|
if not os.access(abs_path, os.X_OK | os.R_OK): |
|
return True |
|
|
|
|
|
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: str, abs_root: str = "") -> bool: |
|
"""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. |
|
""" |
|
abs_path = os.path.normpath(abs_path) |
|
abs_root = os.path.normpath(abs_root) |
|
|
|
if abs_path == 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 Path(inside_root).parts): |
|
return True |
|
|
|
|
|
|
|
path = str(Path(abs_path).parent) |
|
while path and path.startswith(abs_root) and path != abs_root: |
|
if not Path(path).exists(): |
|
path = str(Path(path).parent) |
|
continue |
|
try: |
|
|
|
st = os.lstat(path) |
|
except OSError: |
|
return True |
|
if getattr(st, "st_flags", 0) & UF_HIDDEN: |
|
return True |
|
path = str(Path(path).parent) |
|
|
|
return False |
|
|
|
|
|
def win32_restrict_file_to_user(fname: str) -> None: |
|
"""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 |
|
""" |
|
try: |
|
import win32api |
|
except ImportError: |
|
return _win32_restrict_file_to_user_ctypes(fname) |
|
|
|
import ntsecuritycon as con |
|
import win32security |
|
|
|
|
|
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_GENERIC_READ | con.FILE_GENERIC_WRITE | con.DELETE, |
|
user, |
|
) |
|
dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, admins) |
|
|
|
sd.SetSecurityDescriptorDacl(1, dacl, 0) |
|
win32security.SetFileSecurity(fname, win32security.DACL_SECURITY_INFORMATION, sd) |
|
return None |
|
|
|
|
|
def _win32_restrict_file_to_user_ctypes(fname: str) -> None: |
|
"""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 ctypes |
|
from ctypes import wintypes |
|
|
|
advapi32 = ctypes.WinDLL("advapi32", use_last_error=True) |
|
secur32 = ctypes.WinDLL("secur32", use_last_error=True) |
|
|
|
NameSamCompatible = 2 |
|
WinBuiltinAdministratorsSid = 26 |
|
DACL_SECURITY_INFORMATION = 4 |
|
ACL_REVISION = 2 |
|
ERROR_INSUFFICIENT_BUFFER = 122 |
|
ERROR_MORE_DATA = 234 |
|
|
|
SYNCHRONIZE = 0x100000 |
|
DELETE = 0x00010000 |
|
STANDARD_RIGHTS_REQUIRED = 0xF0000 |
|
STANDARD_RIGHTS_READ = 0x20000 |
|
STANDARD_RIGHTS_WRITE = 0x20000 |
|
FILE_READ_DATA = 1 |
|
FILE_READ_EA = 8 |
|
FILE_READ_ATTRIBUTES = 128 |
|
FILE_WRITE_DATA = 2 |
|
FILE_APPEND_DATA = 4 |
|
FILE_WRITE_EA = 16 |
|
FILE_WRITE_ATTRIBUTES = 256 |
|
FILE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF |
|
FILE_GENERIC_READ = ( |
|
STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE |
|
) |
|
FILE_GENERIC_WRITE = ( |
|
STANDARD_RIGHTS_WRITE |
|
| FILE_WRITE_DATA |
|
| FILE_WRITE_ATTRIBUTES |
|
| FILE_WRITE_EA |
|
| FILE_APPEND_DATA |
|
| SYNCHRONIZE |
|
) |
|
|
|
class ACL(ctypes.Structure): |
|
_fields_ = [ |
|
("AclRevision", wintypes.BYTE), |
|
("Sbz1", wintypes.BYTE), |
|
("AclSize", wintypes.WORD), |
|
("AceCount", wintypes.WORD), |
|
("Sbz2", wintypes.WORD), |
|
] |
|
|
|
PSID = ctypes.c_void_p |
|
PACL = ctypes.POINTER(ACL) |
|
PSECURITY_DESCRIPTOR = ctypes.POINTER(wintypes.BYTE) |
|
|
|
def _nonzero_success(result: int, func: Any, args: Any) -> Any: |
|
if not result: |
|
raise ctypes.WinError(ctypes.get_last_error()) |
|
return args |
|
|
|
secur32.GetUserNameExW.errcheck = _nonzero_success |
|
secur32.GetUserNameExW.restype = wintypes.BOOL |
|
secur32.GetUserNameExW.argtypes = ( |
|
ctypes.c_int, |
|
wintypes.LPWSTR, |
|
wintypes.PULONG, |
|
) |
|
|
|
advapi32.CreateWellKnownSid.errcheck = _nonzero_success |
|
advapi32.CreateWellKnownSid.restype = wintypes.BOOL |
|
advapi32.CreateWellKnownSid.argtypes = ( |
|
wintypes.DWORD, |
|
PSID, |
|
PSID, |
|
wintypes.PDWORD, |
|
) |
|
|
|
advapi32.LookupAccountNameW.errcheck = _nonzero_success |
|
advapi32.LookupAccountNameW.restype = wintypes.BOOL |
|
advapi32.LookupAccountNameW.argtypes = ( |
|
wintypes.LPWSTR, |
|
wintypes.LPWSTR, |
|
PSID, |
|
wintypes.LPDWORD, |
|
wintypes.LPWSTR, |
|
wintypes.LPDWORD, |
|
wintypes.LPDWORD, |
|
) |
|
|
|
advapi32.AddAccessAllowedAce.errcheck = _nonzero_success |
|
advapi32.AddAccessAllowedAce.restype = wintypes.BOOL |
|
advapi32.AddAccessAllowedAce.argtypes = ( |
|
PACL, |
|
wintypes.DWORD, |
|
wintypes.DWORD, |
|
PSID, |
|
) |
|
|
|
advapi32.SetSecurityDescriptorDacl.errcheck = _nonzero_success |
|
advapi32.SetSecurityDescriptorDacl.restype = wintypes.BOOL |
|
advapi32.SetSecurityDescriptorDacl.argtypes = ( |
|
PSECURITY_DESCRIPTOR, |
|
wintypes.BOOL, |
|
PACL, |
|
wintypes.BOOL, |
|
) |
|
|
|
advapi32.GetFileSecurityW.errcheck = _nonzero_success |
|
advapi32.GetFileSecurityW.restype = wintypes.BOOL |
|
advapi32.GetFileSecurityW.argtypes = ( |
|
wintypes.LPCWSTR, |
|
wintypes.DWORD, |
|
PSECURITY_DESCRIPTOR, |
|
wintypes.DWORD, |
|
wintypes.LPDWORD, |
|
) |
|
|
|
advapi32.SetFileSecurityW.errcheck = _nonzero_success |
|
advapi32.SetFileSecurityW.restype = wintypes.BOOL |
|
advapi32.SetFileSecurityW.argtypes = ( |
|
wintypes.LPCWSTR, |
|
wintypes.DWORD, |
|
PSECURITY_DESCRIPTOR, |
|
) |
|
|
|
advapi32.MakeAbsoluteSD.errcheck = _nonzero_success |
|
advapi32.MakeAbsoluteSD.restype = wintypes.BOOL |
|
advapi32.MakeAbsoluteSD.argtypes = ( |
|
PSECURITY_DESCRIPTOR, |
|
PSECURITY_DESCRIPTOR, |
|
wintypes.LPDWORD, |
|
PACL, |
|
wintypes.LPDWORD, |
|
PACL, |
|
wintypes.LPDWORD, |
|
PSID, |
|
wintypes.LPDWORD, |
|
PSID, |
|
wintypes.LPDWORD, |
|
) |
|
|
|
advapi32.MakeSelfRelativeSD.errcheck = _nonzero_success |
|
advapi32.MakeSelfRelativeSD.restype = wintypes.BOOL |
|
advapi32.MakeSelfRelativeSD.argtypes = ( |
|
PSECURITY_DESCRIPTOR, |
|
PSECURITY_DESCRIPTOR, |
|
wintypes.LPDWORD, |
|
) |
|
|
|
advapi32.InitializeAcl.errcheck = _nonzero_success |
|
advapi32.InitializeAcl.restype = wintypes.BOOL |
|
advapi32.InitializeAcl.argtypes = ( |
|
PACL, |
|
wintypes.DWORD, |
|
wintypes.DWORD, |
|
) |
|
|
|
def CreateWellKnownSid(WellKnownSidType: Any) -> Any: |
|
|
|
pSid = (ctypes.c_char * 1)() |
|
cbSid = wintypes.DWORD() |
|
try: |
|
advapi32.CreateWellKnownSid(WellKnownSidType, None, pSid, ctypes.byref(cbSid)) |
|
except OSError as e: |
|
if e.winerror != ERROR_INSUFFICIENT_BUFFER: |
|
raise |
|
pSid = (ctypes.c_char * cbSid.value)() |
|
advapi32.CreateWellKnownSid(WellKnownSidType, None, pSid, ctypes.byref(cbSid)) |
|
return pSid[:] |
|
|
|
def GetUserNameEx(NameFormat: Any) -> Any: |
|
|
|
|
|
nSize = ctypes.pointer(ctypes.c_ulong(0)) |
|
try: |
|
secur32.GetUserNameExW(NameFormat, None, nSize) |
|
except OSError as e: |
|
if e.winerror != ERROR_MORE_DATA: |
|
raise |
|
if not nSize.contents.value: |
|
return None |
|
lpNameBuffer = ctypes.create_unicode_buffer(nSize.contents.value) |
|
secur32.GetUserNameExW(NameFormat, lpNameBuffer, nSize) |
|
return lpNameBuffer.value |
|
|
|
def LookupAccountName(lpSystemName: Any, lpAccountName: Any) -> Any: |
|
|
|
|
|
cbSid = wintypes.DWORD(0) |
|
cchReferencedDomainName = wintypes.DWORD(0) |
|
peUse = wintypes.DWORD(0) |
|
try: |
|
advapi32.LookupAccountNameW( |
|
lpSystemName, |
|
lpAccountName, |
|
None, |
|
ctypes.byref(cbSid), |
|
None, |
|
ctypes.byref(cchReferencedDomainName), |
|
ctypes.byref(peUse), |
|
) |
|
except OSError as e: |
|
if e.winerror != ERROR_INSUFFICIENT_BUFFER: |
|
raise |
|
Sid = ctypes.create_unicode_buffer("", cbSid.value) |
|
pSid = ctypes.cast(ctypes.pointer(Sid), wintypes.LPVOID) |
|
lpReferencedDomainName = ctypes.create_unicode_buffer("", cchReferencedDomainName.value + 1) |
|
success = advapi32.LookupAccountNameW( |
|
lpSystemName, |
|
lpAccountName, |
|
pSid, |
|
ctypes.byref(cbSid), |
|
lpReferencedDomainName, |
|
ctypes.byref(cchReferencedDomainName), |
|
ctypes.byref(peUse), |
|
) |
|
if not success: |
|
raise ctypes.WinError() |
|
return pSid, lpReferencedDomainName.value, peUse.value |
|
|
|
def AddAccessAllowedAce(pAcl: Any, dwAceRevision: Any, AccessMask: Any, pSid: Any) -> Any: |
|
|
|
|
|
advapi32.AddAccessAllowedAce(pAcl, dwAceRevision, AccessMask, pSid) |
|
|
|
def GetFileSecurity(lpFileName: Any, RequestedInformation: Any) -> Any: |
|
|
|
nLength = wintypes.DWORD(0) |
|
try: |
|
advapi32.GetFileSecurityW( |
|
lpFileName, |
|
RequestedInformation, |
|
None, |
|
0, |
|
ctypes.byref(nLength), |
|
) |
|
except OSError as e: |
|
if e.winerror != ERROR_INSUFFICIENT_BUFFER: |
|
raise |
|
if not nLength.value: |
|
return None |
|
pSecurityDescriptor = (wintypes.BYTE * nLength.value)() |
|
advapi32.GetFileSecurityW( |
|
lpFileName, |
|
RequestedInformation, |
|
pSecurityDescriptor, |
|
nLength, |
|
ctypes.byref(nLength), |
|
) |
|
return pSecurityDescriptor |
|
|
|
def SetFileSecurity( |
|
lpFileName: Any, RequestedInformation: Any, pSecurityDescriptor: Any |
|
) -> Any: |
|
|
|
advapi32.SetFileSecurityW(lpFileName, RequestedInformation, pSecurityDescriptor) |
|
|
|
def SetSecurityDescriptorDacl( |
|
pSecurityDescriptor: Any, bDaclPresent: Any, pDacl: Any, bDaclDefaulted: Any |
|
) -> Any: |
|
|
|
advapi32.SetSecurityDescriptorDacl(pSecurityDescriptor, bDaclPresent, pDacl, bDaclDefaulted) |
|
|
|
def MakeAbsoluteSD(pSelfRelativeSecurityDescriptor: Any) -> Any: |
|
|
|
|
|
pAbsoluteSecurityDescriptor = None |
|
lpdwAbsoluteSecurityDescriptorSize = wintypes.DWORD(0) |
|
pDacl = None |
|
lpdwDaclSize = wintypes.DWORD(0) |
|
pSacl = None |
|
lpdwSaclSize = wintypes.DWORD(0) |
|
pOwner = None |
|
lpdwOwnerSize = wintypes.DWORD(0) |
|
pPrimaryGroup = None |
|
lpdwPrimaryGroupSize = wintypes.DWORD(0) |
|
try: |
|
advapi32.MakeAbsoluteSD( |
|
pSelfRelativeSecurityDescriptor, |
|
pAbsoluteSecurityDescriptor, |
|
ctypes.byref(lpdwAbsoluteSecurityDescriptorSize), |
|
pDacl, |
|
ctypes.byref(lpdwDaclSize), |
|
pSacl, |
|
ctypes.byref(lpdwSaclSize), |
|
pOwner, |
|
ctypes.byref(lpdwOwnerSize), |
|
pPrimaryGroup, |
|
ctypes.byref(lpdwPrimaryGroupSize), |
|
) |
|
except OSError as e: |
|
if e.winerror != ERROR_INSUFFICIENT_BUFFER: |
|
raise |
|
pAbsoluteSecurityDescriptor = (wintypes.BYTE * lpdwAbsoluteSecurityDescriptorSize.value)() |
|
pDaclData = (wintypes.BYTE * lpdwDaclSize.value)() |
|
pDacl = ctypes.cast(pDaclData, PACL).contents |
|
pSaclData = (wintypes.BYTE * lpdwSaclSize.value)() |
|
pSacl = ctypes.cast(pSaclData, PACL).contents |
|
pOwnerData = (wintypes.BYTE * lpdwOwnerSize.value)() |
|
pOwner = ctypes.cast(pOwnerData, PSID) |
|
pPrimaryGroupData = (wintypes.BYTE * lpdwPrimaryGroupSize.value)() |
|
pPrimaryGroup = ctypes.cast(pPrimaryGroupData, PSID) |
|
advapi32.MakeAbsoluteSD( |
|
pSelfRelativeSecurityDescriptor, |
|
pAbsoluteSecurityDescriptor, |
|
ctypes.byref(lpdwAbsoluteSecurityDescriptorSize), |
|
pDacl, |
|
ctypes.byref(lpdwDaclSize), |
|
pSacl, |
|
ctypes.byref(lpdwSaclSize), |
|
pOwner, |
|
lpdwOwnerSize, |
|
pPrimaryGroup, |
|
ctypes.byref(lpdwPrimaryGroupSize), |
|
) |
|
return pAbsoluteSecurityDescriptor |
|
|
|
def MakeSelfRelativeSD(pAbsoluteSecurityDescriptor: Any) -> Any: |
|
|
|
|
|
pSelfRelativeSecurityDescriptor = None |
|
lpdwBufferLength = wintypes.DWORD(0) |
|
try: |
|
advapi32.MakeSelfRelativeSD( |
|
pAbsoluteSecurityDescriptor, |
|
pSelfRelativeSecurityDescriptor, |
|
ctypes.byref(lpdwBufferLength), |
|
) |
|
except OSError as e: |
|
if e.winerror != ERROR_INSUFFICIENT_BUFFER: |
|
raise |
|
pSelfRelativeSecurityDescriptor = (wintypes.BYTE * lpdwBufferLength.value)() |
|
advapi32.MakeSelfRelativeSD( |
|
pAbsoluteSecurityDescriptor, |
|
pSelfRelativeSecurityDescriptor, |
|
ctypes.byref(lpdwBufferLength), |
|
) |
|
return pSelfRelativeSecurityDescriptor |
|
|
|
def NewAcl() -> Any: |
|
|
|
nAclLength = 32767 |
|
acl_data = ctypes.create_string_buffer(nAclLength) |
|
pAcl = ctypes.cast(acl_data, PACL).contents |
|
advapi32.InitializeAcl(pAcl, nAclLength, ACL_REVISION) |
|
return pAcl |
|
|
|
SidAdmins = CreateWellKnownSid(WinBuiltinAdministratorsSid) |
|
SidUser = LookupAccountName("", GetUserNameEx(NameSamCompatible))[0] |
|
|
|
Acl = NewAcl() |
|
AddAccessAllowedAce(Acl, ACL_REVISION, FILE_ALL_ACCESS, SidAdmins) |
|
AddAccessAllowedAce( |
|
Acl, |
|
ACL_REVISION, |
|
FILE_GENERIC_READ | FILE_GENERIC_WRITE | DELETE, |
|
SidUser, |
|
) |
|
|
|
SelfRelativeSD = GetFileSecurity(fname, DACL_SECURITY_INFORMATION) |
|
AbsoluteSD = MakeAbsoluteSD(SelfRelativeSD) |
|
SetSecurityDescriptorDacl(AbsoluteSD, 1, Acl, 0) |
|
SelfRelativeSD = MakeSelfRelativeSD(AbsoluteSD) |
|
|
|
SetFileSecurity(fname, DACL_SECURITY_INFORMATION, SelfRelativeSD) |
|
|
|
|
|
def get_file_mode(fname: str) -> int: |
|
"""Retrieves the file mode corresponding to fname in a filesystem-tolerant manner. |
|
|
|
Parameters |
|
---------- |
|
|
|
fname : unicode |
|
The path to the file to get mode from |
|
|
|
""" |
|
|
|
|
|
|
|
|
|
return ( |
|
stat.S_IMODE(Path(fname).stat().st_mode) & 0o6677 |
|
) |
|
|
|
|
|
allow_insecure_writes = os.getenv("JUPYTER_ALLOW_INSECURE_WRITES", "false").lower() in ("true", "1") |
|
|
|
|
|
@contextmanager |
|
def secure_write(fname: str, binary: bool = False) -> Iterator[Any]: |
|
"""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" |
|
encoding = None if binary else "utf-8" |
|
open_flag = os.O_CREAT | os.O_WRONLY | os.O_TRUNC |
|
try: |
|
Path(fname).unlink() |
|
except OSError: |
|
|
|
pass |
|
|
|
if os.name == "nt": |
|
if allow_insecure_writes: |
|
|
|
|
|
issue_insecure_write_warning() |
|
else: |
|
|
|
|
|
|
|
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, encoding=encoding) as f: |
|
if os.name != "nt": |
|
|
|
file_mode = get_file_mode(fname) |
|
if file_mode != 0o0600: |
|
if allow_insecure_writes: |
|
issue_insecure_write_warning() |
|
else: |
|
msg = ( |
|
f"Permissions assignment failed for secure file: '{fname}'." |
|
f" Got '{oct(file_mode)}' instead of '0o0600'." |
|
) |
|
raise RuntimeError(msg) |
|
yield f |
|
|
|
|
|
def issue_insecure_write_warning() -> None: |
|
"""Issue an insecure write warning.""" |
|
|
|
def format_warning(msg: str, *args: Any, **kwargs: Any) -> str: |
|
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'.", |
|
stacklevel=2, |
|
) |
|
|