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

@ -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: