97 lines
3 KiB
Python
97 lines
3 KiB
Python
"""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
|
|
from secrets_engine.safe_paths import containing_git_worktree
|
|
|
|
|
|
@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()
|
|
worktree = containing_git_worktree(resolved)
|
|
if worktree is not None:
|
|
raise ProvisioningError(
|
|
f"handoff file {resolved} is inside a Git worktree ({worktree}); "
|
|
"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,
|
|
)
|