feat: adopt security zones and explicit workload refs
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
This commit is contained in:
parent
12c637cbf2
commit
7ce58ae638
52 changed files with 1547 additions and 658 deletions
|
|
@ -71,6 +71,6 @@ def policy_gate_status() -> str:
|
|||
cfg = load_config()
|
||||
except ConfigError:
|
||||
return "advisory — no warden.yaml (caller identity; gate not enforced)"
|
||||
if cfg.policy.enabled:
|
||||
return f"enforced — flex-auth at {cfg.policy.flex_auth_url}"
|
||||
return "advisory — policy.enabled=false (gate ships with flex-auth deploy)"
|
||||
if cfg.policy.flex_auth_url:
|
||||
return f"zone-aware — flex-auth at {cfg.policy.flex_auth_url}"
|
||||
return "zone-aware — evaluator unconfigured; unknown-zone fail_open applies"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import os
|
|||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
_AUDIT_FILENAME = "audit.jsonl"
|
||||
_MAX_BYTES = 5 * 1024 * 1024
|
||||
|
|
@ -215,7 +215,6 @@ def collect_activity(
|
|||
since = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
events = read_events(state_dir, since=since, kinds=kinds)
|
||||
if include_legacy:
|
||||
legacy_kinds = kinds or {"sign", "access", "worker"}
|
||||
if not kinds or "sign" in kinds:
|
||||
events.extend(_legacy_sign_events(state_dir, since))
|
||||
if not kinds or "access" in kinds:
|
||||
|
|
@ -278,4 +277,4 @@ def fetch_hub_notes(*, days: int = 7, hub_url: Optional[str] = None) -> list[dic
|
|||
"summary": summary,
|
||||
}
|
||||
)
|
||||
return notes
|
||||
return notes
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ def _append_signature_log(
|
|||
}
|
||||
if spec.policy_decision_id:
|
||||
entry["policy_decision_id"] = spec.policy_decision_id
|
||||
if spec.policy_zone:
|
||||
entry["policy_zone"] = spec.policy_zone
|
||||
if spec.policy_failure_mode:
|
||||
entry["policy_failure_mode"] = spec.policy_failure_mode
|
||||
if spec.policy_outcome:
|
||||
entry["policy_outcome"] = spec.policy_outcome
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
with (state_dir / "signatures.log").open("a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
|
@ -76,6 +82,9 @@ def _append_signature_log(
|
|||
actor_type=spec.actor_type.value,
|
||||
backend=backend,
|
||||
ttl_hours=spec.ttl_hours,
|
||||
policy_zone=spec.policy_zone,
|
||||
policy_failure_mode=spec.policy_failure_mode,
|
||||
policy_outcome=spec.policy_outcome,
|
||||
)
|
||||
except Exception:
|
||||
pass # audit must not block signing
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ before it evaluates the request: `Authorization: Bearer <token>` is passed to a
|
|||
Kubernetes TokenReview, and `resource.system: ops-warden` is bound to the
|
||||
principal `system:serviceaccount:ops-warden:ops-warden`. Until ops-warden sends
|
||||
that header, the pin logs `caller authentication warning` and can only run in
|
||||
`warn` mode — which is why `policy.enabled` cannot flip.
|
||||
`warn` mode — which is why enforcing caller authentication is a separate gate.
|
||||
|
||||
This module resolves the token at call time and hands it straight to the request.
|
||||
Nothing is cached to disk, logged, or echoed: ops-warden carries the value, it
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from warden.config import ConfigError, WardenConfig, load_config
|
|||
from warden.policy import check_sign_policy
|
||||
from warden.inventory import ActorEntry, InventoryError, PrincipalsInventory, load_inventory, save_inventory
|
||||
from warden.models import ActorType, CertSpec, DEFAULT_TTL_HOURS, validate_actor_name
|
||||
from warden.routing.catalog import blocker_stale_days
|
||||
from warden.scorecard import run_scorecard
|
||||
|
||||
app = typer.Typer(
|
||||
|
|
@ -120,7 +121,7 @@ def _get_ca(cfg: WardenConfig):
|
|||
|
||||
|
||||
def _apply_policy_gate(cfg: WardenConfig, spec: CertSpec) -> None:
|
||||
"""Run flex-auth check when policy.enabled; sets spec.policy_decision_id."""
|
||||
"""Run the zone-aware flex-auth check; record any returned decision id."""
|
||||
decision_id = check_sign_policy(cfg.policy, spec)
|
||||
if decision_id:
|
||||
spec.policy_decision_id = decision_id
|
||||
|
|
@ -644,6 +645,7 @@ def _entry_summary(entry) -> dict:
|
|||
# Agent read-boundary (WP-0026 T04) — high-risk lanes deny raw agent data reads.
|
||||
"risk": entry.risk,
|
||||
"high_risk": entry.is_high_risk,
|
||||
"workload_ref": entry.workload_ref.to_dict(),
|
||||
# Renewal guidance (WP-0026 T06) — advisory, no secret values. `has_rotation`
|
||||
# lets a caller gate before asking for the full block via `warden rotate-guide`.
|
||||
"has_rotation": entry.has_rotation,
|
||||
|
|
@ -771,9 +773,6 @@ def route_list(
|
|||
)
|
||||
|
||||
|
||||
from warden.routing.catalog import blocker_stale_days
|
||||
|
||||
|
||||
def _gap_is_stale(entry, delegation, reviewed: str, stale_days) -> bool:
|
||||
"""An interim lane needs attention on either of two independent grounds.
|
||||
|
||||
|
|
@ -1255,6 +1254,14 @@ def _access_proxy(
|
|||
"token stays in the caller's own store.[/dim]"
|
||||
)
|
||||
else:
|
||||
if no_policy:
|
||||
err.print(
|
||||
"[red]--no-policy is retired[/red]: security-zones_v0.1 selects "
|
||||
"the policy stance and failure mode. Remove the flag; an unresolved "
|
||||
"workload uses the explicit unknown-zone profile."
|
||||
)
|
||||
raise typer.Exit(2)
|
||||
|
||||
# G1 — caller identity. ops-warden adds no token of its own.
|
||||
if not caller_auth_present():
|
||||
err.print(
|
||||
|
|
@ -1263,24 +1270,20 @@ def _access_proxy(
|
|||
)
|
||||
raise typer.Exit(3)
|
||||
|
||||
# G3 — policy gate before fetch.
|
||||
if cfg.policy.enabled:
|
||||
try:
|
||||
decision_id = check_fetch_policy(
|
||||
cfg.policy, need_id=entry.id, owner_repo=entry.owner_repo, domain=domain
|
||||
)
|
||||
except CAError as e:
|
||||
err.print(f"[red]Policy gate denied the fetch:[/red] {e}")
|
||||
raise typer.Exit(4)
|
||||
err.print(f"[green]flex-auth allow[/green] (decision {decision_id}).")
|
||||
elif not no_policy:
|
||||
err.print(
|
||||
"[yellow]flex-auth gate is not enforced[/yellow] (policy.enabled=false). "
|
||||
"Re-run with [bold]--no-policy[/bold] to proxy ungated, or enable the gate."
|
||||
# G3 — the zone-aware policy gate always runs before fetch.
|
||||
try:
|
||||
decision_id = check_fetch_policy(
|
||||
cfg.policy, need_id=entry.id, owner_repo=entry.owner_repo, domain=domain
|
||||
)
|
||||
except CAError as e:
|
||||
err.print(f"[red]Policy gate denied the fetch:[/red] {e}")
|
||||
raise typer.Exit(4)
|
||||
if decision_id:
|
||||
err.print(f"[green]flex-auth decision[/green] ({decision_id}).")
|
||||
else:
|
||||
err.print("[yellow]Proxying ungated[/yellow] (--no-policy; gate not enforced).")
|
||||
err.print(
|
||||
"[yellow]flex-auth unavailable; unknown-zone fail_open applied[/yellow]."
|
||||
)
|
||||
|
||||
# Wrapping (WP-0026 T02) uses its own command shape; the value-bearing transports
|
||||
# share the resolved fetch command.
|
||||
|
|
@ -1434,7 +1437,10 @@ def access(
|
|||
] = False,
|
||||
no_policy: Annotated[
|
||||
bool,
|
||||
typer.Option("--no-policy", help="Acknowledge proxying when the flex-auth gate is not enforced"),
|
||||
typer.Option(
|
||||
"--no-policy",
|
||||
help="Retired compatibility flag; zone-aware policy evaluation cannot be bypassed",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Operator front door: how to obtain any credential, gated and audited.
|
||||
|
|
|
|||
|
|
@ -45,9 +45,19 @@ class CallerAuthConfig:
|
|||
|
||||
@dataclass
|
||||
class PolicyConfig:
|
||||
enabled: bool = False
|
||||
flex_auth_url: str = "http://127.0.0.1:8080"
|
||||
fail_closed: bool = True
|
||||
flex_auth_url: Optional[str] = None
|
||||
zone_registry_path: Optional[Path] = None
|
||||
failure_modes: Dict[str, str] = field(
|
||||
default_factory=lambda: {
|
||||
"z0-experimental": "fail_open",
|
||||
"z1-operational": "fail_open",
|
||||
"z2-protected": "fail_open",
|
||||
"z2-continuity": "fail_open",
|
||||
"z3-critical": "fail_closed",
|
||||
"unknown": "fail_open",
|
||||
"not-applicable": "fail_closed",
|
||||
}
|
||||
)
|
||||
tenant: str = "tenant:platform"
|
||||
subject_env: str = "WARDEN_POLICY_SUBJECT"
|
||||
system: str = "ops-warden"
|
||||
|
|
@ -148,6 +158,13 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
|
|||
)
|
||||
|
||||
policy_raw = raw.get("policy") or {}
|
||||
retired = sorted({"enabled", "fail_closed"}.intersection(policy_raw))
|
||||
if retired:
|
||||
raise ConfigError(
|
||||
"retired policy setting(s) "
|
||||
+ ", ".join(f"policy.{key}" for key in retired)
|
||||
+ "; security-zones_v0.1 now selects stance and failure mode"
|
||||
)
|
||||
caller_raw = policy_raw.get("caller_auth") or {}
|
||||
caller_command = caller_raw.get("command")
|
||||
if isinstance(caller_command, str):
|
||||
|
|
@ -175,10 +192,33 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
|
|||
raise ConfigError("policy.caller_auth.token_path is required for mode: file")
|
||||
if caller_cfg.mode == "command" and not caller_cfg.command:
|
||||
raise ConfigError("policy.caller_auth.command is required for mode: command")
|
||||
failure_modes = PolicyConfig().failure_modes
|
||||
configured_failure_modes = policy_raw.get("failure_modes") or {}
|
||||
if not isinstance(configured_failure_modes, dict):
|
||||
raise ConfigError("policy.failure_modes must be a mapping")
|
||||
failure_modes.update(
|
||||
{str(zone): str(mode) for zone, mode in configured_failure_modes.items()}
|
||||
)
|
||||
invalid_modes = {
|
||||
zone: mode
|
||||
for zone, mode in failure_modes.items()
|
||||
if mode not in {"fail_open", "fail_closed"}
|
||||
}
|
||||
if invalid_modes:
|
||||
raise ConfigError(
|
||||
"policy.failure_modes values must be fail_open or fail_closed: "
|
||||
f"{invalid_modes}"
|
||||
)
|
||||
zone_registry_path = policy_raw.get("zone_registry_path")
|
||||
flex_auth_url = str(policy_raw.get("flex_auth_url", "")).strip() or None
|
||||
policy_cfg = PolicyConfig(
|
||||
enabled=bool(policy_raw.get("enabled", False)),
|
||||
flex_auth_url=str(policy_raw.get("flex_auth_url", "http://127.0.0.1:8080")),
|
||||
fail_closed=bool(policy_raw.get("fail_closed", True)),
|
||||
flex_auth_url=flex_auth_url,
|
||||
zone_registry_path=(
|
||||
Path(os.path.expanduser(str(zone_registry_path)))
|
||||
if zone_registry_path
|
||||
else None
|
||||
),
|
||||
failure_modes=failure_modes,
|
||||
tenant=str(policy_raw.get("tenant", "tenant:platform")),
|
||||
subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")),
|
||||
system=str(policy_raw.get("system", "ops-warden")),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ class CertSpec:
|
|||
principals: List[str]
|
||||
identity: str = "" # defaults to actor_name if empty
|
||||
policy_decision_id: Optional[str] = None
|
||||
policy_zone: Optional[str] = None
|
||||
policy_failure_mode: Optional[str] = None
|
||||
policy_outcome: Optional[str] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.identity:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"""flex-auth policy gate for SSH signing (opt-in via warden.yaml)."""
|
||||
"""Zone-aware flex-auth policy gates for OpsWarden."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -20,21 +21,60 @@ def pubkey_fingerprint(pubkey_path: Path) -> str:
|
|||
return f"sha256:{digest}"
|
||||
|
||||
|
||||
def _caller_headers(cfg: PolicyConfig) -> dict[str, str]:
|
||||
def _caller_headers(cfg: PolicyConfig, *, fail_closed: bool) -> dict[str, str]:
|
||||
"""Bearer header identifying ops-warden itself to flex-auth (FLEX-WP-0016).
|
||||
|
||||
When the token cannot be obtained we refuse the call under ``fail_closed``
|
||||
When the token cannot be obtained we refuse the call under the selected
|
||||
zone's ``fail_closed`` behavior
|
||||
rather than silently falling back to an unauthenticated request — an
|
||||
unauthenticated call is exactly what keeps the flex-auth pin in ``warn``.
|
||||
"""
|
||||
try:
|
||||
return caller_auth_headers(cfg.caller_auth)
|
||||
except CallerIdentityError as e:
|
||||
if cfg.fail_closed:
|
||||
if fail_closed:
|
||||
raise CAError(f"flex-auth caller identity unavailable: {e}") from e
|
||||
return {}
|
||||
|
||||
|
||||
def _resource_zone(cfg: PolicyConfig, resource_id: str) -> str:
|
||||
"""Read a compiled resource zone; absence or ambiguity is always unknown."""
|
||||
if cfg.zone_registry_path is None:
|
||||
return "unknown"
|
||||
try:
|
||||
registry = json.loads(cfg.zone_registry_path.read_text())
|
||||
resources = registry["resource_manifests"][0]["resources"]
|
||||
resource = next(item for item in resources if item.get("id") == resource_id)
|
||||
attributes = resource.get("attributes") or {}
|
||||
if attributes.get("security_zone_admission") == "not-applicable":
|
||||
return "not-applicable"
|
||||
zone = str(attributes.get("security_zone") or "unknown")
|
||||
return zone if zone in cfg.failure_modes else "unknown"
|
||||
except (OSError, ValueError, KeyError, StopIteration, TypeError):
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _is_fail_closed(cfg: PolicyConfig, zone: str) -> bool:
|
||||
return cfg.failure_modes.get(zone, cfg.failure_modes["unknown"]) == "fail_closed"
|
||||
|
||||
|
||||
def _evaluator_failure(
|
||||
message: str,
|
||||
*,
|
||||
fail_closed: bool,
|
||||
cause: Exception | None = None,
|
||||
spec: CertSpec | None = None,
|
||||
) -> None:
|
||||
if fail_closed:
|
||||
if spec is not None:
|
||||
spec.policy_outcome = "fail_closed"
|
||||
if cause is None:
|
||||
raise CAError(message)
|
||||
raise CAError(message) from cause
|
||||
if spec is not None:
|
||||
spec.policy_outcome = "fail_open"
|
||||
|
||||
|
||||
def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
|
||||
return os.environ.get(cfg.subject_env, "").strip() or spec.actor_name
|
||||
|
||||
|
|
@ -42,11 +82,21 @@ def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
|
|||
def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
||||
"""Call flex-auth /v1/check before signing.
|
||||
|
||||
Returns decision id when policy is enabled and effect is allow.
|
||||
Returns None when policy is disabled.
|
||||
Raises CAError on deny or when fail_closed and flex-auth is unreachable.
|
||||
Returns a decision id on ``allow`` or ``audit_only``. A deny always blocks.
|
||||
Evaluator failures use the PEP-owned failure mode for the target workload's
|
||||
compiled zone; absent resolution is the explicit ``unknown`` profile.
|
||||
"""
|
||||
if not cfg.enabled:
|
||||
resource_id = f"ssh-cert:actor/{spec.actor_name}"
|
||||
zone = _resource_zone(cfg, resource_id)
|
||||
fail_closed = _is_fail_closed(cfg, zone)
|
||||
spec.policy_zone = zone
|
||||
spec.policy_failure_mode = "fail_closed" if fail_closed else "fail_open"
|
||||
if cfg.flex_auth_url is None:
|
||||
_evaluator_failure(
|
||||
f"flex-auth URL is not configured for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
pubkey_path = Path(os.path.expanduser(str(spec.pubkey_path)))
|
||||
|
|
@ -76,37 +126,54 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
|||
}
|
||||
|
||||
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg)
|
||||
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||
try:
|
||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth denied or rejected sign policy check (HTTP {e.response.status_code})"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) "
|
||||
f"for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} "
|
||||
f"(fail_closed=true): {e}"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
decision = response.json()
|
||||
except ValueError as e:
|
||||
raise CAError("flex-auth returned non-JSON decision") from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth returned a non-JSON decision for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
effect = str(decision.get("effect", "")).lower()
|
||||
decision_id = decision.get("id") or decision.get("request_id")
|
||||
if effect != "allow":
|
||||
if effect not in {"allow", "audit_only"}:
|
||||
spec.policy_outcome = "deny"
|
||||
reason = decision.get("reason") or "no reason provided"
|
||||
raise CAError(f"flex-auth denied SSH sign for {spec.actor_name!r}: {reason}")
|
||||
|
||||
if not decision_id:
|
||||
raise CAError("flex-auth allow decision missing id")
|
||||
_evaluator_failure(
|
||||
f"flex-auth {effect} decision missing id for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
spec.policy_outcome = effect
|
||||
return str(decision_id)
|
||||
|
||||
|
||||
|
|
@ -116,13 +183,17 @@ def check_fetch_policy(
|
|||
"""Call flex-auth /v1/check before proxying a non-SSH credential fetch (WP-0014).
|
||||
|
||||
The action is ``read`` on a ``secret`` resource owned by another subsystem —
|
||||
ops-warden is the conduit, not the owner. Returns the decision id on allow,
|
||||
None when policy is disabled, and raises CAError on deny (or on an unreachable
|
||||
flex-auth when fail_closed). No secret value is ever part of this request.
|
||||
ops-warden is the conduit, not the owner. Unresolved target workload identity
|
||||
selects the explicit ``unknown`` profile; no secret value enters the request.
|
||||
"""
|
||||
if not cfg.enabled:
|
||||
zone = "unknown"
|
||||
fail_closed = _is_fail_closed(cfg, zone)
|
||||
if cfg.flex_auth_url is None:
|
||||
_evaluator_failure(
|
||||
"flex-auth URL is not configured for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
)
|
||||
return None
|
||||
|
||||
subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator"
|
||||
request = {
|
||||
"subject": {"id": subject_id, "type": "operator", "tenant": cfg.tenant},
|
||||
|
|
@ -137,33 +208,44 @@ def check_fetch_policy(
|
|||
}
|
||||
|
||||
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg)
|
||||
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||
try:
|
||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth denied or rejected fetch policy check (HTTP {e.response.status_code})"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} (fail_closed=true): {e}"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
decision = response.json()
|
||||
except ValueError as e:
|
||||
raise CAError("flex-auth returned non-JSON decision") from e
|
||||
_evaluator_failure(
|
||||
"flex-auth returned a non-JSON decision for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
|
||||
effect = str(decision.get("effect", "")).lower()
|
||||
decision_id = decision.get("id") or decision.get("request_id")
|
||||
if effect != "allow":
|
||||
if effect not in {"allow", "audit_only"}:
|
||||
reason = decision.get("reason") or "no reason provided"
|
||||
raise CAError(f"flex-auth denied secret read for {need_id!r}: {reason}")
|
||||
if not decision_id:
|
||||
raise CAError("flex-auth allow decision missing id")
|
||||
return str(decision_id)
|
||||
_evaluator_failure(
|
||||
f"flex-auth {effect} decision missing id for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
)
|
||||
return None
|
||||
return str(decision_id)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from warden.routing.catalog import (
|
|||
find_catalog_path,
|
||||
load_catalog,
|
||||
)
|
||||
from warden.routing.models import Delegation, RouteEntry
|
||||
from warden.routing.models import Delegation, RouteEntry, WorkloadReference
|
||||
|
||||
__all__ = [
|
||||
"Catalog",
|
||||
|
|
@ -20,6 +20,7 @@ __all__ = [
|
|||
"CatalogFreshness",
|
||||
"Delegation",
|
||||
"RouteEntry",
|
||||
"WorkloadReference",
|
||||
"find_catalog_path",
|
||||
"load_catalog",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -26,9 +26,11 @@ import yaml
|
|||
from warden.routing.models import (
|
||||
VALID_DELEGATION_MODES,
|
||||
VALID_RISK,
|
||||
VALID_WORKLOAD_APPLICABILITY,
|
||||
Delegation,
|
||||
RotationGuide,
|
||||
RouteEntry,
|
||||
WorkloadReference,
|
||||
)
|
||||
|
||||
# Structured handoff string fields (WP-0014) — templates and pointers only.
|
||||
|
|
@ -64,6 +66,7 @@ _REQUIRED_FIELDS = (
|
|||
"canon_ref",
|
||||
"reviewed",
|
||||
"status",
|
||||
"workload_ref",
|
||||
)
|
||||
_VALID_STATUS = ("active", "draft")
|
||||
_VALID_LANES = ("secret", "login")
|
||||
|
|
@ -516,6 +519,82 @@ def _parse_delegation(entry_id: str, raw: Optional[dict]) -> Optional[Delegation
|
|||
)
|
||||
|
||||
|
||||
def _parse_workload_ref(entry_id: str, raw: object) -> WorkloadReference:
|
||||
"""Parse an explicit workload join without attempting identity inference."""
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref must be a mapping; every lane must "
|
||||
"declare applicable or not-applicable"
|
||||
)
|
||||
|
||||
applicability = str(raw.get("applicability", "")).strip()
|
||||
if applicability not in VALID_WORKLOAD_APPLICABILITY:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref.applicability {applicability!r} invalid "
|
||||
f"(expected one of {VALID_WORKLOAD_APPLICABILITY})"
|
||||
)
|
||||
|
||||
def optional(name: str) -> Optional[str]:
|
||||
value = raw.get(name)
|
||||
return str(value).strip() if value is not None and str(value).strip() else None
|
||||
|
||||
ref = WorkloadReference(
|
||||
applicability=applicability,
|
||||
rapp_id=optional("rapp_id"),
|
||||
name=optional("name"),
|
||||
deployable=optional("deployable"),
|
||||
declaration_ref=optional("declaration_ref"),
|
||||
reason=optional("reason"),
|
||||
unknown_reason=optional("unknown_reason"),
|
||||
)
|
||||
|
||||
target_fields = (ref.rapp_id, ref.name, ref.deployable, ref.declaration_ref)
|
||||
if applicability == "not-applicable":
|
||||
if not ref.reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref.reason is required for not-applicable"
|
||||
)
|
||||
if any(target_fields) or ref.unknown_reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} not-applicable workload_ref must not carry a "
|
||||
"workload target or unknown_reason"
|
||||
)
|
||||
return ref
|
||||
|
||||
if ref.unknown_reason:
|
||||
if any(target_fields) or ref.reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} unknown workload_ref must carry only "
|
||||
"applicability and unknown_reason"
|
||||
)
|
||||
return ref
|
||||
|
||||
if not ref.name:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} applicable workload_ref requires name or "
|
||||
"unknown_reason"
|
||||
)
|
||||
if ref.rapp_id:
|
||||
if ref.declaration_ref:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} managed workload_ref must not also carry "
|
||||
"declaration_ref"
|
||||
)
|
||||
elif not ref.declaration_ref:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} operational workload_ref requires declaration_ref"
|
||||
)
|
||||
if ref.deployable and not ref.rapp_id:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref.deployable requires rapp_id"
|
||||
)
|
||||
if ref.reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} applicable workload_ref must not carry reason"
|
||||
)
|
||||
return ref
|
||||
|
||||
|
||||
def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError(f"entry #{index} is not a mapping")
|
||||
|
|
@ -576,12 +655,16 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
|||
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
|
||||
)
|
||||
|
||||
risk = str(raw.get("risk", "standard")).strip() or "standard"
|
||||
if risk not in VALID_RISK:
|
||||
risk_value = raw.get("risk")
|
||||
risk = str(risk_value).strip() if risk_value is not None else "ungraded"
|
||||
risk = risk or "ungraded"
|
||||
if risk != "ungraded" and risk not in VALID_RISK:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} has invalid risk {risk!r} (expected one of {VALID_RISK})"
|
||||
)
|
||||
|
||||
workload_ref = _parse_workload_ref(entry_id, raw.get("workload_ref"))
|
||||
|
||||
return RouteEntry(
|
||||
id=entry_id,
|
||||
title=str(raw["title"]),
|
||||
|
|
@ -593,6 +676,7 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
|||
canon_ref=str(raw["canon_ref"]),
|
||||
reviewed=str(raw["reviewed"]),
|
||||
status=status,
|
||||
workload_ref=workload_ref,
|
||||
steps=[str(s) for s in steps],
|
||||
cert_command=str(cert_command) if cert_command else None,
|
||||
auth_method=handoff["auth_method"],
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ VALID_RISK = ("standard", "high")
|
|||
# interim — ops-warden covers a gap; intended_owner + blocked_on required
|
||||
# permanent — ops-warden is the designed owner of this front door (SSH only today)
|
||||
VALID_DELEGATION_MODES = ("native", "interim", "permanent")
|
||||
VALID_WORKLOAD_APPLICABILITY = ("applicable", "not-applicable")
|
||||
|
||||
IMPLICIT_DELEGATION_BLOCKED_ON = (
|
||||
"unclassified — no delegation block; treat as a question, not a settlement"
|
||||
|
|
@ -100,6 +101,46 @@ class Delegation:
|
|||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkloadReference:
|
||||
"""Authoritative workload join for a catalog lane (WARDEN-WP-0032).
|
||||
|
||||
Managed deployables use the Repo Manager v1 ``rapp_id``/``name`` tuple.
|
||||
Independently governed operational workloads use ``name`` plus an owner
|
||||
declaration reference. An applicable lane whose owner has not published an
|
||||
identity remains explicitly ``unknown``; it is never inferred from the
|
||||
credential path or repository name.
|
||||
"""
|
||||
|
||||
applicability: str # applicable | not-applicable
|
||||
rapp_id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
deployable: Optional[str] = None
|
||||
declaration_ref: Optional[str] = None
|
||||
reason: Optional[str] = None
|
||||
unknown_reason: Optional[str] = None
|
||||
|
||||
@property
|
||||
def resolution(self) -> str:
|
||||
if self.applicability == "not-applicable":
|
||||
return "not-applicable"
|
||||
if self.unknown_reason:
|
||||
return "unknown"
|
||||
return "resolved"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"applicability": self.applicability,
|
||||
"rapp_id": self.rapp_id,
|
||||
"name": self.name,
|
||||
"deployable": self.deployable,
|
||||
"declaration_ref": self.declaration_ref,
|
||||
"reason": self.reason,
|
||||
"unknown_reason": self.unknown_reason,
|
||||
"resolution": self.resolution,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RouteEntry:
|
||||
id: str
|
||||
|
|
@ -112,6 +153,8 @@ class RouteEntry:
|
|||
canon_ref: str
|
||||
reviewed: str
|
||||
status: str # "active" | "draft"
|
||||
# Explicit workload applicability and authoritative join. Never inferred.
|
||||
workload_ref: Optional[WorkloadReference] = None
|
||||
# SSH lane only — None/empty for routed (non-executed) needs.
|
||||
steps: List[str] = field(default_factory=list)
|
||||
cert_command: Optional[str] = None
|
||||
|
|
@ -169,6 +212,34 @@ class RouteEntry:
|
|||
"""
|
||||
return self.risk not in LOW_RISK_GRADES
|
||||
|
||||
def risk_for_zone(
|
||||
self,
|
||||
*,
|
||||
effective_zone: str = "unknown",
|
||||
admission: str = "unknown",
|
||||
synthetic_only: bool = False,
|
||||
) -> str:
|
||||
"""Resolve an absent grade using security-zones_v0.1 section 5.1.
|
||||
|
||||
Explicit grades always win. The sole lower default is a satisfied
|
||||
``z0-experimental`` workload proven synthetic-only. Every other zone,
|
||||
failed/unknown admission, and missing context fails safe to at least
|
||||
``high``; z3 reports ``critical`` (which the read boundary treats as
|
||||
high). Catalog CI still requires explicit grades, so this is the safe
|
||||
runtime behavior for malformed or newer external catalogs.
|
||||
"""
|
||||
if self.is_graded:
|
||||
return self.risk
|
||||
if (
|
||||
effective_zone == "z0-experimental"
|
||||
and admission == "satisfied"
|
||||
and synthetic_only
|
||||
):
|
||||
return "standard"
|
||||
if effective_zone == "z3-critical" and admission == "satisfied":
|
||||
return "critical"
|
||||
return "high"
|
||||
|
||||
@property
|
||||
def has_rotation(self) -> bool:
|
||||
"""True when this lane carries renewal guidance (WP-0026 T06)."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue