feat: admit existing OpenBao catalog lanes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

This commit is contained in:
tegwick 2026-08-21 08:20:33 +02:00
parent 9d383442c8
commit 784be978bf
29 changed files with 1490 additions and 79 deletions

View file

@ -37,38 +37,56 @@ def apply_plan(client: OpenBaoClient, entry: CatalogEntry, plan: Plan, ttl: str
# 1. KV mount. Auth-capability lanes grant operational access on an existing
# mount (for example ssh/sign/<role>) and never create or store KV values.
if entry.stores_kv_value():
if entry.manages_mount:
if client.kv_mount_exists(entry.mount):
skipped.append(f"kv-mount {entry.mount} (already present)")
else:
client.ensure_kv_mount(entry.mount)
applied.append(f"kv-mount {entry.mount}")
elif entry.stores_kv_value():
skipped.append(f"kv-mount {entry.mount} (externally managed; no mutation)")
else:
skipped.append(f"kv-mount {entry.mount} (not applicable for {entry.kind})")
# 2. Consumer ACL policy (write only if changed).
current = client.read_policy(plan.policy_name)
if current and _normalize(current) == _normalize(plan.policy_hcl):
skipped.append(f"policy {plan.policy_name} (unchanged)")
if entry.manages_delivery_auth:
current = client.read_policy(plan.policy_name)
if current and _normalize(current) == _normalize(plan.policy_hcl):
skipped.append(f"policy {plan.policy_name} (unchanged)")
else:
client.write_policy(plan.policy_name, plan.policy_hcl)
applied.append(f"policy {plan.policy_name}")
else:
client.write_policy(plan.policy_name, plan.policy_hcl)
applied.append(f"policy {plan.policy_name}")
skipped.append(f"policy {plan.policy_name} (externally managed; no mutation)")
# 3. Consumer approle bound to that policy.
client.ensure_approle_enabled()
if entry.kind == "auth-capability":
client.write_approle(
plan.role_name,
[plan.policy_name],
ttl=entry.token_ttl,
max_ttl=entry.token_max_ttl,
secret_id_ttl=entry.secret_id_ttl,
secret_id_num_uses=entry.secret_id_num_uses,
token_num_uses=entry.token_num_uses,
)
if entry.manages_delivery_auth:
client.ensure_approle_enabled()
if entry.kind == "auth-capability":
client.write_approle(
plan.role_name,
[plan.policy_name],
ttl=entry.token_ttl,
max_ttl=entry.token_max_ttl,
secret_id_ttl=entry.secret_id_ttl,
secret_id_num_uses=entry.secret_id_num_uses,
token_num_uses=entry.token_num_uses,
)
else:
client.write_approle(
plan.role_name,
[plan.policy_name],
ttl=entry.delivery_token_ttl or ttl,
max_ttl=entry.delivery_token_max_ttl,
secret_id_ttl=entry.delivery_secret_id_ttl,
secret_id_num_uses=entry.delivery_secret_id_num_uses,
token_num_uses=entry.delivery_token_num_uses,
)
applied.append(f"approle {plan.role_name} -> [{plan.policy_name}]")
elif entry.has_delivery_auth:
skipped.append(f"approle {plan.role_name} (externally managed; no mutation)")
else:
client.write_approle(plan.role_name, [plan.policy_name], ttl=ttl)
applied.append(f"approle {plan.role_name} -> [{plan.policy_name}]")
skipped.append("approle (no native delivery auth declared)")
return ApplyResult(applied=applied, skipped=skipped)

View file

@ -27,6 +27,10 @@ VALID_DELIVERY_MODES = (
"approle-login",
)
VALID_APPROVAL_MODELS = ("decision", "ccr", "dual-control", "bootstrap-only")
VALID_MOUNT_MANAGEMENT = ("engine", "existing")
VALID_DELIVERY_AUTH_MANAGEMENT = ("engine", "existing", "none")
VALID_DELIVERY_AUTH_METHODS = ("approle", "none")
VALID_RISK_CLASSIFICATIONS = ("standard", "high")
REQUIRED_FIELDS = (
"id",
@ -66,6 +70,10 @@ class CatalogEntry:
rotation: dict[str, Any]
deactivation: dict[str, Any]
audit: dict[str, Any]
mount_management: str = "engine"
delivery_auth: dict[str, Any] = field(default_factory=dict)
workload_delivery: list[dict[str, Any]] = field(default_factory=list)
risk: dict[str, Any] = field(default_factory=dict)
delivery_config: dict[str, Any] = field(default_factory=dict)
auth_capability: dict[str, Any] = field(default_factory=dict)
description: str = ""
@ -95,14 +103,62 @@ class CatalogEntry:
def policy_name(self) -> str:
if self.kind == "auth-capability":
return self.auth_capability.get("policy_name") or self.id
if self.delivery_auth.get("policy_name"):
return str(self.delivery_auth["policy_name"])
return f"se-{self.stage}-{self.id}"
@property
def role_name(self) -> str:
if self.kind == "auth-capability":
return self.auth_capability.get("role_name") or self.id
if self.delivery_auth.get("role_name"):
return str(self.delivery_auth["role_name"])
return f"se-{self.stage}-{self.id}"
@property
def manages_mount(self) -> bool:
return self.kind == "kv" and self.mount_management == "engine"
@property
def delivery_auth_method(self) -> str:
if self.kind == "auth-capability":
return "approle"
return str(self.delivery_auth.get("method", "approle"))
@property
def delivery_auth_management(self) -> str:
if self.kind == "auth-capability":
return "engine"
return str(self.delivery_auth.get("management", "engine"))
@property
def manages_delivery_auth(self) -> bool:
return self.delivery_auth_management == "engine"
@property
def has_delivery_auth(self) -> bool:
return self.delivery_auth_management != "none"
@property
def delivery_token_ttl(self) -> str:
return str(self.delivery_auth.get("token_ttl", "30m"))
@property
def delivery_token_max_ttl(self) -> str:
return str(self.delivery_auth.get("token_max_ttl", self.delivery_token_ttl))
@property
def delivery_secret_id_ttl(self) -> str:
return str(self.delivery_auth.get("secret_id_ttl", self.delivery_token_ttl))
@property
def delivery_secret_id_num_uses(self) -> int:
return int(self.delivery_auth.get("secret_id_num_uses", 0))
@property
def delivery_token_num_uses(self) -> int:
return int(self.delivery_auth.get("token_num_uses", 0))
@property
def auth_allowed_paths(self) -> dict[str, list[str]]:
"""Allowed OpenBao paths/capabilities for an auth-capability lane."""
@ -156,8 +212,16 @@ def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> Catalog
)
if data["kind"] == "auth-capability":
data.setdefault("fields", [])
data.setdefault("mount_management", "existing")
data.setdefault("delivery_auth", {})
data.setdefault("workload_delivery", [])
else:
data.setdefault("auth_capability", {})
data.setdefault("mount_management", "engine")
data.setdefault(
"delivery_auth", {"method": "approle", "management": "engine"}
)
data.setdefault("workload_delivery", [])
missing = [k for k in REQUIRED_FIELDS if k not in data or data[k] in (None, "", [], {})]
if data["kind"] == "kv" and not data.get("fields"):
@ -195,6 +259,75 @@ def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> Catalog
f"{source}: each consumer needs at least 'name' and 'auth'"
)
if data["mount_management"] not in VALID_MOUNT_MANAGEMENT:
raise CatalogError(
f"{source}: mount_management '{data['mount_management']}' invalid; "
f"must be one of {VALID_MOUNT_MANAGEMENT}"
)
workload_delivery = data["workload_delivery"]
if not isinstance(workload_delivery, list):
raise CatalogError(f"{source}: workload_delivery must be a list")
for item in workload_delivery:
if (
not isinstance(item, dict)
or not isinstance(item.get("mode"), str)
or not item["mode"].strip()
or not isinstance(item.get("owner"), str)
or not item["owner"].strip()
):
raise CatalogError(
f"{source}: each workload_delivery item needs non-empty mode and owner"
)
if data["kind"] == "kv":
delivery_auth = data["delivery_auth"]
if not isinstance(delivery_auth, dict):
raise CatalogError(f"{source}: delivery_auth must be a mapping")
auth_method = delivery_auth.get("method", "approle")
auth_management = delivery_auth.get("management", "engine")
if auth_method not in VALID_DELIVERY_AUTH_METHODS:
raise CatalogError(
f"{source}: delivery_auth.method '{auth_method}' invalid; "
f"must be one of {VALID_DELIVERY_AUTH_METHODS}"
)
if auth_management not in VALID_DELIVERY_AUTH_MANAGEMENT:
raise CatalogError(
f"{source}: delivery_auth.management '{auth_management}' invalid; "
f"must be one of {VALID_DELIVERY_AUTH_MANAGEMENT}"
)
if auth_management == "none" and auth_method != "none":
raise CatalogError(
f"{source}: delivery_auth.management none requires method none"
)
if auth_management != "none" and auth_method != "approle":
raise CatalogError(
f"{source}: current native delivery auth must use approle"
)
if auth_management == "existing" and not delivery_auth.get("role_name"):
raise CatalogError(
f"{source}: existing delivery auth requires delivery_auth.role_name"
)
for ttl_field in ("token_ttl", "token_max_ttl", "secret_id_ttl"):
if ttl_field in delivery_auth and (
not isinstance(delivery_auth[ttl_field], str)
or not delivery_auth[ttl_field].strip()
):
raise CatalogError(
f"{source}: delivery_auth.{ttl_field} must be a non-empty string"
)
for uses_field in ("secret_id_num_uses", "token_num_uses"):
uses = delivery_auth.get(uses_field, 0)
if isinstance(uses, bool) or not isinstance(uses, int) or uses < 0:
raise CatalogError(
f"{source}: delivery_auth.{uses_field} must be a non-negative integer"
)
native_modes = {"exec-env", "exec-file", "npm-config", "read-check", "wrapped"}
if native_modes.intersection(modes) and auth_management == "none":
raise CatalogError(
f"{source}: native delivery/verification modes require delivery_auth"
)
approval = data["approval"]
if not isinstance(approval, dict) or "model" not in approval:
raise CatalogError(f"{source}: approval must include a 'model'")
@ -204,6 +337,27 @@ def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> Catalog
f"allowed {VALID_APPROVAL_MODELS}"
)
risk = data.get("risk", {})
if not isinstance(risk, dict):
raise CatalogError(f"{source}: risk must be a mapping")
classification = risk.get("classification", "standard")
if classification not in VALID_RISK_CLASSIFICATIONS:
raise CatalogError(
f"{source}: risk.classification '{classification}' invalid; "
f"must be one of {VALID_RISK_CLASSIFICATIONS}"
)
if classification == "high":
if approval["model"] == "bootstrap-only":
raise CatalogError(
f"{source}: high-risk lanes cannot use bootstrap-only approval"
)
for lifecycle_name in ("rotation", "deactivation"):
lifecycle = data[lifecycle_name]
if not isinstance(lifecycle, dict) or not lifecycle.get("owner"):
raise CatalogError(
f"{source}: high-risk lanes require {lifecycle_name}.owner"
)
# npm-config delivery must declare WHERE it publishes (registry + scope), so
# the registry is catalog data, never hardcoded in the engine.
if "npm-config" in modes:

View file

@ -63,6 +63,19 @@ def _writer(cfg: Config) -> EvidenceWriter:
return EvidenceWriter(evidence_dir=cfg.evidence_dir, hub_url=cfg.hub_url, topic_id=cfg.topic_id)
def _require_lane_approval(cfg: Config, entry):
"""Resolve and enforce the lane approval for a privileged live action."""
if not entry.approval_required():
return None
decision = resolve_decision(
hub_url=cfg.hub_url,
repo_root=repo_root(),
decision_ref=entry.approval.get("decision_ref", entry.id),
)
require_approved(entry, decision)
return decision
# -- command handlers ------------------------------------------------------
@ -89,13 +102,23 @@ def cmd_catalog_show(cfg: Config, args) -> int:
print(f"openbao: mount={e.mount} allowed={sorted(e.auth_allowed_paths)}")
print(f"approle: {e.role_name} policy={e.policy_name}")
else:
print(f"openbao: {e.mount}/{e.path} fields={e.fields}")
print(
f"openbao: {e.mount}/{e.path} fields={e.fields} "
f"mount_management={e.mount_management}"
)
print(
f"delivery auth: {e.delivery_auth_method}/"
f"{e.delivery_auth_management} role={e.role_name if e.has_delivery_auth else '-'}"
)
print(f"workload: {e.workload_delivery}")
print(f"consumers: {[c['name'] for c in e.consumers]}")
print(f"delivery: {e.delivery_modes}")
print(f"approval: {e.approval.get('model')} ref={e.approval.get('decision_ref','')}")
print(f"verification: {e.verification}")
print(f"rotation: {e.rotation}")
print(f"deactivation: {e.deactivation}")
if e.risk:
print(f"risk: {e.risk}")
print(f"description: {e.description.strip()}")
return 0
@ -172,6 +195,7 @@ def cmd_provision(cfg: Config, args) -> int:
if args.stage != entry.stage:
from secrets_engine.errors import ProvisioningError
raise ProvisioningError(f"lane '{entry.id}' is stage '{entry.stage}', not '{args.stage}'")
decision = _require_lane_approval(cfg, entry)
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
field = args.field or (entry.fields[0] if entry.fields else "")
if args.generate:
@ -182,12 +206,13 @@ def cmd_provision(cfg: Config, args) -> int:
mode = "from-file"
print(f"provisioned lane '{entry.id}' field '{f}' ({mode}) — value not displayed")
_writer(cfg).record("provision", result=mode, catalog_id=entry.id, stage=entry.stage,
detail={"field": f})
decision_id=decision.id if decision else "", detail={"field": f})
return 0
def cmd_verify(cfg: Config, args) -> int:
entry = get_entry(cfg.catalog_dir, args.catalog_id)
decision = _require_lane_approval(cfg, entry)
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
field = args.field or (entry.fields[0] if entry.fields else "")
if entry.stores_kv_value() and not field:
@ -202,7 +227,8 @@ def cmd_verify(cfg: Config, args) -> int:
if not r.passed:
rc = 7
_writer(cfg).record("verify", result=f"{r.check}:{'pass' if r.passed else 'fail'}",
catalog_id=entry.id, stage=entry.stage, detail=r.detail)
catalog_id=entry.id, stage=entry.stage,
decision_id=decision.id if decision else "", detail=r.detail)
return rc
@ -215,13 +241,7 @@ def cmd_handoff(cfg: Config, args) -> int:
raise ProvisioningError(f"lane '{entry.id}' is stage '{entry.stage}', not '{args.stage}'")
if entry.kind != "auth-capability":
raise ProvisioningError(f"lane '{entry.id}' is {entry.kind}; handoff needs auth-capability")
decision = None
if entry.approval_required():
decision = resolve_decision(
hub_url=cfg.hub_url, repo_root=repo_root(),
decision_ref=entry.approval.get("decision_ref", entry.id),
)
require_approved(entry, decision)
decision = _require_lane_approval(cfg, entry)
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
result = write_approle_handoff(
client,
@ -257,12 +277,7 @@ def cmd_exec(cfg: Config, args) -> int:
from secrets_engine.exec_delivery import exec_with_secret
entry = get_entry(cfg.catalog_dir, args.catalog)
# require approval + readiness before running.
if entry.approval_required():
decision = resolve_decision(
hub_url=cfg.hub_url, repo_root=repo_root(),
decision_ref=entry.approval.get("decision_ref", entry.id),
)
require_approved(entry, decision)
decision = _require_lane_approval(cfg, entry)
if not args.command:
from secrets_engine.errors import DeliveryError
raise DeliveryError("no command after '--'")
@ -270,9 +285,11 @@ def cmd_exec(cfg: Config, args) -> int:
field = args.field or (entry.fields[0] if entry.fields else "")
w = _writer(cfg)
w.record("exec", result="attempt", catalog_id=entry.id, stage=entry.stage,
decision_id=decision.id if decision else "",
detail={"command": args.command[0], "mode": args.mode})
rc = exec_with_secret(client, entry, field, args.command, mode=args.mode)
w.record("exec", result=f"exit-{rc}", catalog_id=entry.id, stage=entry.stage,
decision_id=decision.id if decision else "",
detail={"command": args.command[0]})
return rc
@ -322,6 +339,7 @@ def cmd_route(cfg: Config, args) -> int:
def cmd_revoke(cfg: Config, args) -> int:
entry = get_entry(cfg.catalog_dir, args.catalog_id)
decision = None if args.dry_run else _require_lane_approval(cfg, entry)
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
if entry.kind == "auth-capability":
if args.dry_run:
@ -336,7 +354,10 @@ def cmd_revoke(cfg: Config, args) -> int:
f"revoked lane '{entry.id}': deleted approle {entry.role_name} "
f"and policy {entry.policy_name}"
)
_writer(cfg).record("revoke", result="auth-capability-deactivated", catalog_id=entry.id, stage=entry.stage)
_writer(cfg).record(
"revoke", result="auth-capability-deactivated", catalog_id=entry.id,
stage=entry.stage, decision_id=decision.id if decision else ""
)
return 0
if args.dry_run:
print(f"(dry-run) would delete KV metadata {entry.mount}/{entry.path} "
@ -344,7 +365,10 @@ def cmd_revoke(cfg: Config, args) -> int:
return 0
client.kv_delete_metadata(entry.mount, entry.path)
print(f"revoked lane '{entry.id}': KV metadata deleted at {entry.mount}/{entry.path}")
_writer(cfg).record("revoke", result="deactivated", catalog_id=entry.id, stage=entry.stage)
_writer(cfg).record(
"revoke", result="deactivated", catalog_id=entry.id, stage=entry.stage,
decision_id=decision.id if decision else ""
)
return 0

View file

@ -52,6 +52,10 @@ def resolve_npm_token_env(entry: CatalogEntry, *, policy_dir=None) -> str:
def _fetch_value(client: OpenBaoClient, entry: CatalogEntry, field: str) -> str:
"""Read the field value via an approle-scoped token. Held in memory only."""
if entry.delivery_auth_method != "approle" or not entry.has_delivery_auth:
raise DeliveryError(
f"lane '{entry.id}' has no AppRole delivery auth for native exec"
)
try:
token = client.approle_login_token(entry.role_name)
except Exception as e:
@ -126,6 +130,10 @@ def exec_with_secret(
"""
if not command:
raise DeliveryError("no command given to exec")
if field not in entry.fields:
raise DeliveryError(
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
)
declared = set(entry.delivery_modes)
if mode == "auto":

View file

@ -13,6 +13,7 @@ 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)
@ -27,12 +28,12 @@ class HandoffResult:
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"
)
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

View file

@ -23,6 +23,7 @@ from dataclasses import dataclass
from pathlib import Path
from secrets_engine.errors import BackendError, ProvisioningError
from secrets_engine.safe_paths import containing_git_worktree
def _check_token_file(path: Path) -> str:
@ -36,12 +37,12 @@ def _check_token_file(path: Path) -> str:
f"(mode {oct(st.st_mode & 0o777)}); must be 0600"
)
# Refuse a token file living inside a Git worktree.
for parent in path.resolve().parents:
if (parent / ".git").exists():
raise ProvisioningError(
f"bootstrap token file {path} is inside a Git worktree ({parent}); "
"store it outside any repo"
)
worktree = containing_git_worktree(path)
if worktree is not None:
raise ProvisioningError(
f"bootstrap token file {path} is inside a Git worktree ({worktree}); "
"store it outside any repo"
)
token = path.read_text(encoding="utf-8").strip()
if not token:
raise ProvisioningError(f"bootstrap token file {path} is empty")

View file

@ -21,7 +21,7 @@ from secrets_engine.roles import (
@dataclass
class PlanAction:
kind: str # "kv-mount" | "policy" | "approle"
kind: str # mutation or non-mutating check/preview action
target: str # human-readable target
detail: dict[str, Any] = field(default_factory=dict)
@ -87,19 +87,60 @@ def build_plan(entry: CatalogEntry, stage: str, *, decision_id: str = "") -> Pla
assert_path_in_stage(entry) # path must be in-stage, no wildcards
policy_name, policy_hcl = consumer_policy_for(entry) # runs assert_policy_safe
actions = [
PlanAction("kv-mount", entry.mount, {"type": "kv-v2"}),
PlanAction(
"policy",
policy_name,
{"paths": f"{entry.mount}/data/{entry.path}"},
),
PlanAction(
"approle",
entry.role_name,
{"token_policies": policy_name, "auth": "approle"},
),
]
mount_action = (
PlanAction("kv-mount", entry.mount, {"type": "kv-v2", "management": "engine"})
if entry.manages_mount
else PlanAction(
"kv-mount-check",
entry.mount,
{"type": "kv-v2", "management": "existing", "mutation": "none"},
)
)
actions = [mount_action]
if entry.manages_delivery_auth:
actions.extend(
[
PlanAction(
"policy",
policy_name,
{"paths": f"{entry.mount}/data/{entry.path}"},
),
PlanAction(
"approle",
entry.role_name,
{
"token_policies": policy_name,
"auth": "approle",
"token_ttl": entry.delivery_token_ttl,
"token_max_ttl": entry.delivery_token_max_ttl,
"token_num_uses": entry.delivery_token_num_uses,
},
),
]
)
elif entry.has_delivery_auth:
actions.extend(
[
PlanAction(
"policy-check",
policy_name,
{"paths": f"{entry.mount}/data/{entry.path}", "mutation": "none"},
),
PlanAction(
"approle-check",
entry.role_name,
{"auth": "approle", "management": "existing", "mutation": "none"},
),
]
)
else:
actions.append(
PlanAction(
"policy-preview",
policy_name,
{"paths": f"{entry.mount}/data/{entry.path}", "mutation": "none"},
)
)
return Plan(
catalog_id=entry.id,
stage=stage,

View file

@ -18,6 +18,7 @@ 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
def _read_value_file(path: Path) -> str:
@ -29,12 +30,12 @@ def _read_value_file(path: Path) -> str:
f"value file {path} is group/other-accessible "
f"(mode {oct(st.st_mode & 0o777)}); must be 0600"
)
for parent in path.resolve().parents:
if (parent / ".git").exists():
raise ProvisioningError(
f"value file {path} is inside a Git worktree ({parent}); "
"keep secret material outside repos"
)
worktree = containing_git_worktree(path)
if worktree is not None:
raise ProvisioningError(
f"value file {path} is inside a Git worktree ({worktree}); "
"keep secret material outside repos"
)
value = path.read_text(encoding="utf-8").strip()
if not value:
raise ProvisioningError(f"value file {path} is empty")
@ -54,7 +55,8 @@ def provision_from_file(
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
)
value = _read_value_file(Path(file_path))
client.ensure_kv_mount(entry.mount)
if entry.manages_mount:
client.ensure_kv_mount(entry.mount)
client.kv_put(entry.mount, entry.path, field, value)
del value
return field
@ -77,7 +79,8 @@ def provision_generated(client: OpenBaoClient, entry: CatalogEntry, field: str)
)
alphabet = string.ascii_letters + string.digits
value = "test-" + "".join(_secrets.choice(alphabet) for _ in range(32))
client.ensure_kv_mount(entry.mount)
if entry.manages_mount:
client.ensure_kv_mount(entry.mount)
client.kv_put(entry.mount, entry.path, field, value)
del value
return field

View file

@ -60,9 +60,10 @@ def route_lane(
metadata_applied = False
value_present = False
if client is not None and client.is_reachable():
policy_applied = client.read_policy(entry.policy_name) is not None
role_applied = client.approle_exists(entry.role_name)
metadata_applied = policy_applied and role_applied
if entry.has_delivery_auth:
policy_applied = client.read_policy(entry.policy_name) is not None
role_applied = client.approle_exists(entry.role_name)
metadata_applied = policy_applied and role_applied
if entry.stores_kv_value():
# Presence check uses the engine's own token; reports boolean only.
field = entry.fields[0] if entry.fields else ""
@ -80,8 +81,17 @@ def route_lane(
missing = f"approved decision for '{decision_ref}'"
next_command = f"secrets-engine decision inspect {decision_ref or entry.id}"
elif not metadata_applied:
missing = "OpenBao policy/role apply"
next_command = f"secrets-engine apply {decision_ref or entry.id} --stage {entry.stage}"
if entry.kind == "kv" and entry.delivery_auth_management == "existing":
missing = "externally managed OpenBao policy/AppRole readiness"
next_command = (
f"secrets-engine verify {entry.id} --positive --negative"
)
elif entry.kind == "kv" and not entry.has_delivery_auth:
missing = "native delivery auth declaration"
next_command = f"secrets-engine plan {decision_ref or entry.id} --stage {entry.stage}"
else:
missing = "OpenBao policy/role apply"
next_command = f"secrets-engine apply {decision_ref or entry.id} --stage {entry.stage}"
elif entry.kind == "auth-capability":
missing = ""
next_command = (
@ -96,7 +106,12 @@ def route_lane(
)
else:
missing = ""
next_command = f"secrets-engine exec --catalog {entry.id} -- <command...>"
if {"exec-env", "npm-config"}.intersection(entry.delivery_modes):
next_command = f"secrets-engine exec --catalog {entry.id} -- <command...>"
else:
next_command = (
f"secrets-engine verify {entry.id} --positive --negative"
)
return RouteResult(
catalog_id=entry.id,

View file

@ -0,0 +1,20 @@
"""Filesystem checks shared by secret provisioning and handoff paths."""
from __future__ import annotations
from pathlib import Path
def containing_git_worktree(path: Path) -> Path | None:
"""Return the nearest enclosing Git worktree, if one is identifiable.
A real worktree has either a ``.git`` file (linked worktrees/submodules) or
a ``.git`` directory containing ``HEAD``. Merely finding an empty directory
named ``.git`` is not enough; sandbox and test environments may use such a
marker outside any repository.
"""
resolved = path.expanduser().resolve()
for parent in (resolved.parent, *resolved.parent.parents):
marker = parent / ".git"
if marker.is_file() or (marker.is_dir() and (marker / "HEAD").is_file()):
return parent
return None

View file

@ -29,6 +29,12 @@ class VerifyResult:
def verify_positive(client: OpenBaoClient, entry: CatalogEntry, field: str) -> VerifyResult:
"""Approved consumer token must be able to read the field."""
if entry.delivery_auth_method != "approle" or not entry.has_delivery_auth:
return VerifyResult(
"positive",
False,
{"reason": "lane has no AppRole delivery auth", "path": entry.path},
)
try:
token = client.approle_login_token(entry.role_name)
except Exception as e: # backend errors -> failed verification, not a value leak
@ -145,6 +151,10 @@ def verify_auth_capability_negative(client: OpenBaoClient, entry: CatalogEntry)
def run_verification(
client: OpenBaoClient, entry: CatalogEntry, field: str, *, positive: bool, negative: bool
) -> list[VerifyResult]:
if entry.kind == "kv" and field not in entry.fields:
raise VerificationError(
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
)
results: list[VerifyResult] = []
if entry.kind == "auth-capability":
if positive: