Source code for scitex_ssh._allowlist

"""Per-host policy for gating sensitive features (currently: tunnels).

Config file: ~/.scitex/ssh/config.yaml

Schema:
  default:
    tunnels: deny           # default policy for unlisted hosts
  hosts:
    mba:    {tunnels: allow}
    nas:    {tunnels: allow}
    spartan: {tunnels: deny}

If the file does not exist: default policy is `deny` (fail-closed).
"""

from __future__ import annotations

from pathlib import Path
from typing import Literal


[docs] class PolicyError(RuntimeError): """Raised when an action is denied by the allowlist."""
CONFIG_PATH = Path.home() / ".scitex" / "ssh" / "config.yaml" def is_allowed( host: str, feature: Literal["tunnels"], *, config_path: Path | None = None, ) -> bool: """Return True iff `host` is allowed to use `feature`. Parameters ---------- config_path : Path, optional Override the config file location. Defaults to ``CONFIG_PATH`` (``~/.scitex/ssh/config.yaml``). Used by tests to point at a real config in ``tmp_path`` without patching module globals. """ cfg = _load(config_path if config_path is not None else CONFIG_PATH) if cfg is None: return False # fail-closed when no config hosts = cfg.get("hosts") or {} host_cfg = hosts.get(host) or {} if feature in host_cfg: return host_cfg[feature] == "allow" default_cfg = cfg.get("default") or {} return default_cfg.get(feature) == "allow" def require( host: str, feature: Literal["tunnels"], *, config_path: Path | None = None, ) -> None: if not is_allowed(host, feature, config_path=config_path): path_for_msg = config_path if config_path is not None else CONFIG_PATH raise PolicyError( f"feature {feature!r} is not allowed for host {host!r}. " f"Edit {path_for_msg} to add 'hosts.{host}.{feature}: allow' " f"if your environment permits it." ) def _load(config_path: Path) -> dict | None: if not config_path.exists(): return None try: import yaml except ImportError: # yaml not installed — treat as no config return None with open(config_path) as f: return yaml.safe_load(f) or {} # EOF