"""Out-of-band AppRole handoff helpers for auth-capability lanes. The secret_id is secret material. It is minted only after output paths have been validated, written to a mode-0600 file outside any Git worktree, and never printed or recorded in evidence. """ from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from secrets_engine.catalog import CatalogEntry from secrets_engine.errors import ProvisioningError from secrets_engine.openbao import OpenBaoClient @dataclass(frozen=True) class HandoffResult: role_name: str role_id_file: str secret_id_file: str token_ttl: str secret_id_ttl: str secret_id_num_uses: int def _assert_outside_git_worktree(path: Path) -> Path: resolved = path.expanduser().resolve() for parent in (resolved.parent, *resolved.parent.parents): if (parent / ".git").exists(): raise ProvisioningError( f"handoff file {resolved} is inside a Git worktree ({parent}); " "keep AppRole material outside repos" ) return resolved def _validate_output_path(path: Path) -> Path: resolved = _assert_outside_git_worktree(path) if resolved.exists() and resolved.stat().st_mode & 0o077: raise ProvisioningError( f"handoff file {resolved} is group/other-accessible " f"(mode {oct(resolved.stat().st_mode & 0o777)}); must be 0600" ) return resolved def _write_mode_0600(path: Path, value: str) -> None: path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) fd: int | None = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as fh: fd = None fh.write(value) fh.write("\n") finally: if fd is not None: os.close(fd) def write_approle_handoff( client: OpenBaoClient, entry: CatalogEntry, *, role_id_file: Path, secret_id_file: Path, ) -> HandoffResult: """Mint and write AppRole handoff material without printing the secret_id.""" if entry.kind != "auth-capability": raise ProvisioningError( f"lane '{entry.id}' is {entry.kind}; handoff is only for auth-capability lanes" ) role_path = _validate_output_path(role_id_file) secret_path = _validate_output_path(secret_id_file) if role_path == secret_path: raise ProvisioningError("role_id_file and secret_id_file must be different") role_id = client.read_approle_role_id(entry.role_name) secret_id = client.create_approle_secret_id(entry.role_name) try: _write_mode_0600(role_path, role_id) _write_mode_0600(secret_path, secret_id) finally: del secret_id return HandoffResult( role_name=entry.role_name, role_id_file=str(role_path), secret_id_file=str(secret_path), token_ttl=entry.token_ttl, secret_id_ttl=entry.secret_id_ttl, secret_id_num_uses=entry.secret_id_num_uses, )