feat: admit existing OpenBao catalog lanes
This commit is contained in:
parent
9d383442c8
commit
784be978bf
29 changed files with 1490 additions and 79 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue