NK-WP-0056-T11 done: bounded executor core runner
Consumes one admission once through the approval-engine carrier after an effect-free preflight (validity, source pins, trust, window vs budget, binding digest, actor, human aal2 approvers), emits run.started, runs steps with settle-timed checks, reverts on failure with per-revert checks, retries within attempts, stops on unknown outcomes, honours budget and kill switch, and emits one value-free receipt (PDP not-evaluated). 22 tests with fakes. BEA-9 extended to revert steps; examples regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 994874@bnt-lap001 Assistant-Session: 6017c416-d619-47eb-bd1a-f670a02d7d87
This commit is contained in:
parent
3b1abc4054
commit
d1cf754222
14 changed files with 834 additions and 23 deletions
2
Makefile
2
Makefile
|
|
@ -210,7 +210,7 @@ bounded-execution-admission-check: ## Validate the example admissions (read-only
|
|||
python3 tools/bounded-execution-admission/validate.py validate examples/bounded-execution-admission/*.yaml
|
||||
|
||||
bounded-executor-test: ## Bounded executor RBAC and declaration tests; the pilot admission inside its declared grants (NK-WP-0056-T03)
|
||||
python3 -m pytest sso-mfa/k8s/bounded-executor
|
||||
python3 -m pytest sso-mfa/k8s/bounded-executor bounded-executor/tests
|
||||
python3 tools/service-identity/validate.py tools/service-identity/declarations/bounded-executor.yaml \
|
||||
--admissions examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml
|
||||
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@
|
|||
| task | NK-WP-0056-T08 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T09 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T10 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T11 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T11 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T12 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||
| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||
|
|
|
|||
37
bounded-executor/README.md
Normal file
37
bounded-executor/README.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Bounded executor (NK-WP-0056)
|
||||
|
||||
Runs one founder-approved Bounded Execution Admission (`canon/standards/bounded-execution-admission_v0.1.md`)
|
||||
exactly once. Founder decision 6d1adce9: net-kingdom writes the executor; `rapp-bounded-executor` deploys it as one
|
||||
Job per admission in `sso` (T12).
|
||||
|
||||
`executor.py` is the core runner (T11):
|
||||
|
||||
- **preflight, no effect:**
|
||||
- the admission is valid (BEA-1..14);
|
||||
- the executed files match their pins;
|
||||
- the trust states hold;
|
||||
- every operation and check is known;
|
||||
- the window is open and longer than the wall-time budget;
|
||||
- the approval-engine approval binds this admission's digest, names this executor as actor and was approved
|
||||
only by listed HUMAN approvers at aal2. The name-to-subject map is configured; no guessing.
|
||||
- **consume once:** `request_digest` = admission digest, `decision_id` = run id. An unclear outcome is resolved by
|
||||
reading the approval back, never by a second consume.
|
||||
- **custody:** emit `execution.run.started` to audit-core. If the event can't be stored, the run stops.
|
||||
- **steps:** in plan order, one consumer at a time. Checks run after every mutating step, after their settle time.
|
||||
The kill switch and the budget are checked between steps.
|
||||
- **failure:**
|
||||
- A failed check, an exhausted budget or the kill switch reverts in reverse order.
|
||||
- Each revert must pass its own checks.
|
||||
- A clean revert may retry within `attempts`.
|
||||
- An outcome that can't be determined stops without guessing.
|
||||
- A failed revert stops and alerts.
|
||||
- **receipt:** one `execution.attribution.receipt` per run, with PDP `not-evaluated` in the pilot. It never holds a
|
||||
value that an operation handled.
|
||||
|
||||
Operations, checks, the approval carrier, the trust oracle, the audit sink, alerts and clocks are injected. T12 adds
|
||||
the pilot's real operations (OpenBao CAS write, LLDAP self-rotation, ExternalSecret sync, rollout, synthetic
|
||||
journey), the CLI, the Containerfile and the image workflow.
|
||||
|
||||
```bash
|
||||
python3 -m pytest bounded-executor/tests # or: make bounded-executor-test
|
||||
```
|
||||
348
bounded-executor/executor.py
Normal file
348
bounded-executor/executor.py
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Bounded executor: core runner (NK-WP-0056-T11).
|
||||
|
||||
Runs one Bounded Execution Admission (canon/standards/bounded-execution-admission_v0.1.md) exactly once:
|
||||
|
||||
preflight (no effect) the admission is valid (BEA-1..14); the executed source matches its pins; the
|
||||
trust states hold; every operation and check is known; the approval carried by
|
||||
approval-engine binds this admission's digest, names this executor as actor, was
|
||||
approved by listed HUMAN approvers at aal2, is unconsumed, and the window is open
|
||||
consume once, with request_digest = admission digest and decision_id = run id; an
|
||||
unclear outcome is resolved by reading the approval back, never by consuming again
|
||||
started `execution.run.started` to the audit sink; no custody, no run
|
||||
steps in plan order, one consumer at a time; checks after each mutating step, after
|
||||
their settle time; the kill switch and the budget are checked between steps
|
||||
failure a failed check or an exhausted budget reverts the mutated steps in reverse
|
||||
order (or stops, as the admission says) and re-runs their checks; an error of
|
||||
unknown outcome stops without guessing; a failed revert stops and alerts
|
||||
receipt one `execution.attribution.receipt` per run (PDP `not-evaluated` in the pilot)
|
||||
|
||||
Every effect goes through an injected interface: Carrier, operation and check callables, TrustOracle,
|
||||
AuditSink, Alert, clocks. The runner never prints, logs or stores a value an operation handles.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import pathlib
|
||||
import time
|
||||
import uuid
|
||||
from typing import Callable, Protocol
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
REPO = HERE.parent
|
||||
_spec = importlib.util.spec_from_file_location("bea_validate", REPO / "tools/bounded-execution-admission/validate.py")
|
||||
bea = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(bea)
|
||||
|
||||
ACTION = "bounded-execution"
|
||||
STARTED = "execution.run.started"
|
||||
RECEIPT = "execution.attribution.receipt"
|
||||
|
||||
|
||||
class Refused(Exception):
|
||||
"""Refused before any effect."""
|
||||
|
||||
|
||||
class Unknown(Exception):
|
||||
"""An operation's outcome is unknown: stop, do not guess, do not revert blindly."""
|
||||
|
||||
|
||||
class Carrier(Protocol):
|
||||
def get(self, approval_id: str) -> dict: ...
|
||||
def consume(self, approval_id: str, request_digest: str, decision_id: str) -> dict: ...
|
||||
|
||||
|
||||
class AuditSink(Protocol):
|
||||
def emit(self, event: dict) -> None: ...
|
||||
|
||||
|
||||
Operation = Callable[[dict, "Context"], None] # (step, context) -> None, raises Unknown on unclear outcome
|
||||
Check = Callable[[dict, "Context"], bool] # (check, context) -> passed
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Config:
|
||||
executor_identity: str # must equal spec.executor.identity and the approval's actor
|
||||
runtime_principal: str # e.g. system:serviceaccount:sso:nk-bind-executor
|
||||
tenant: str
|
||||
environment: str
|
||||
approver_subjects: dict[str, str] # admission approver name -> carrier subject_id (no guessing)
|
||||
source_root: pathlib.Path = REPO
|
||||
clock_source: str = "system-utc"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Context:
|
||||
"""What operations and checks may see. Values they fetch stay inside them."""
|
||||
run_id: str
|
||||
admission_id: str
|
||||
started_monotonic: float
|
||||
data: dict = dataclasses.field(default_factory=dict) # operation-private state (e.g. versions), never emitted
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Deps:
|
||||
carrier: Carrier
|
||||
operations: dict[str, Operation]
|
||||
checks: dict[str, Check]
|
||||
trust: Callable[[str], bool]
|
||||
audit: AuditSink
|
||||
alert: Callable[[str, dict], None]
|
||||
should_stop: Callable[[], bool] = lambda: False
|
||||
wall: Callable[[], dt.datetime] = lambda: dt.datetime.now(dt.UTC)
|
||||
monotonic: Callable[[], float] = time.monotonic
|
||||
sleep: Callable[[float], None] = time.sleep
|
||||
|
||||
|
||||
def iso(t: dt.datetime) -> str:
|
||||
return t.astimezone(dt.UTC).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
class Run:
|
||||
def __init__(self, admission: dict, approval_id: str, config: Config, deps: Deps, schema: dict | None = None):
|
||||
self.doc = admission
|
||||
self.spec = admission.get("spec", {})
|
||||
self.approval_id = approval_id
|
||||
self.cfg = config
|
||||
self.d = deps
|
||||
self.schema = schema or json.loads(bea.SCHEMA.read_text())
|
||||
self.run_id = str(uuid.uuid4())
|
||||
self.steps: list[dict] = []
|
||||
self.approver_subject: str | None = None
|
||||
self.consumed_at: str | None = None
|
||||
self.start_wall: dt.datetime | None = None
|
||||
self.start_mono: float | None = None
|
||||
|
||||
# ------------------------------------------------------------------ preflight (no effect)
|
||||
def preflight(self) -> None:
|
||||
report = bea.evaluate(self.doc, self.schema)
|
||||
if not report["valid"]:
|
||||
raise Refused("admission_invalid:" + ",".join(sorted({f["rule"] for f in report["findings"]})))
|
||||
spec = self.spec
|
||||
if spec["executor"]["identity"] != self.cfg.executor_identity:
|
||||
raise Refused("not_this_executor")
|
||||
for pinned in spec["plan"]["source"]["files"]:
|
||||
path = self.cfg.source_root / pinned["path"]
|
||||
if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != pinned["sha256"]:
|
||||
raise Refused("source_drift")
|
||||
for state in spec["trust_requires"]:
|
||||
if not self.d.trust(state):
|
||||
raise Refused("trust_not_satisfied:" + state)
|
||||
for step in spec["plan"]["steps"] + spec["plan"].get("revert_steps", []):
|
||||
if step["operation"] not in self.d.operations:
|
||||
raise Refused("unknown_operation")
|
||||
for check in spec["checks"]:
|
||||
if check["id"] not in self.d.checks:
|
||||
raise Refused("unknown_check")
|
||||
self._check_window()
|
||||
self._check_approval(self.d.carrier.get(self.approval_id), consumed_ok=False)
|
||||
|
||||
def _check_window(self) -> None:
|
||||
now = self.d.wall()
|
||||
start, end = (bea.parse_time(self.spec["window"][k]) for k in ("not_before", "not_after"))
|
||||
if not start <= now < end:
|
||||
raise Refused("outside_window")
|
||||
if now + dt.timedelta(seconds=self.spec["budget"]["wall_time_seconds"]) > end:
|
||||
raise Refused("window_shorter_than_budget") # a run must not spill past its admission
|
||||
|
||||
def _check_approval(self, approval: dict, consumed_ok: bool) -> None:
|
||||
spec = self.spec
|
||||
binding = approval.get("binding") or {}
|
||||
target = binding.get("target") or {}
|
||||
if (binding.get("action") != ACTION or binding.get("actor") != self.cfg.executor_identity
|
||||
or target.get("admission_id") != spec["id"]
|
||||
or target.get("admission_digest") != bea.admission_digest(self.doc)
|
||||
or binding.get("human_control") is not True):
|
||||
raise Refused("approval_binding")
|
||||
status = approval.get("status")
|
||||
if status != "approved" and not (consumed_ok and status == "consumed"):
|
||||
raise Refused("approval_status")
|
||||
if not consumed_ok and approval.get("consumption"):
|
||||
raise Refused("approval_already_consumed")
|
||||
allowed = {self.cfg.approver_subjects.get(name) for name in spec["approval"]["approvers"]} - {None}
|
||||
entries = approval.get("entries") or []
|
||||
if not entries:
|
||||
raise Refused("approval_no_entries")
|
||||
for entry in entries: # aal2 is recorded, not enforced, by approval-engine today: check every entry
|
||||
if (entry.get("subject_id") not in allowed or str(entry.get("principal_type", "")).lower() != "human"
|
||||
or entry.get("assurance") != "aal2" or entry.get("subject_id") == self.cfg.executor_identity):
|
||||
raise Refused("approval_entry")
|
||||
self.approver_subject = entries[0]["subject_id"]
|
||||
window_end = bea.parse_time(spec["window"]["not_after"])
|
||||
expires = (approval.get("validity") or {}).get("expires_at")
|
||||
if expires and bea.parse_time(expires) < window_end:
|
||||
raise Refused("approval_expires_before_window_end")
|
||||
|
||||
# ------------------------------------------------------------------ consume (the one irreversible act)
|
||||
def consume(self) -> None:
|
||||
self._check_window() # again, right before the one irreversible act
|
||||
digest = bea.admission_digest(self.doc)
|
||||
try:
|
||||
response = self.d.carrier.consume(self.approval_id, digest, self.run_id)
|
||||
except Exception:
|
||||
response = None # unclear outcome: read back, never consume twice
|
||||
if response is not None:
|
||||
if (response.get("status") != "consumed" or response.get("request_digest") != digest
|
||||
or response.get("decision_id") != self.run_id or response.get("idempotent") is not False):
|
||||
raise Refused("consume_response")
|
||||
self.consumed_at = response.get("consumed_at")
|
||||
approval = self.d.carrier.get(self.approval_id)
|
||||
self._check_approval(approval, consumed_ok=True)
|
||||
consumption = approval.get("consumption") or {}
|
||||
if approval.get("status") != "consumed" or consumption.get("request_digest") != digest \
|
||||
or consumption.get("decision_id") != self.run_id:
|
||||
raise Refused("consume_not_confirmed")
|
||||
self.consumed_at = consumption.get("consumed_at", self.consumed_at)
|
||||
|
||||
# ------------------------------------------------------------------ execution
|
||||
def _budget_left(self) -> float:
|
||||
return self.spec["budget"]["wall_time_seconds"] - (self.d.monotonic() - self.start_mono)
|
||||
|
||||
def _record(self, step: dict, kind: str, outcome: str, checks: list[dict] | None = None) -> None:
|
||||
self.steps.append({"id": step["id"], "kind": kind, "operation": step["operation"], "target": step["target"],
|
||||
"consumer": step["consumer"], "outcome": outcome, "checks": checks or []})
|
||||
|
||||
def _run_checks(self, ids: list[str], ctx: Context) -> tuple[bool, list[dict]]:
|
||||
results = []
|
||||
for cid in ids:
|
||||
check = next(c for c in self.spec["checks"] if c["id"] == cid)
|
||||
settle = check.get("settle_seconds", 0)
|
||||
if settle:
|
||||
if self._budget_left() < settle + check["timeout_seconds"]:
|
||||
results.append({"id": cid, "result": "budget"})
|
||||
return False, results
|
||||
self.d.sleep(settle)
|
||||
began = self.d.monotonic()
|
||||
try:
|
||||
passed = bool(self.d.checks[cid](check, ctx))
|
||||
except Exception:
|
||||
passed = False
|
||||
if self.d.monotonic() - began > check["timeout_seconds"]:
|
||||
passed = False
|
||||
results.append({"id": cid, "result": "pass" if passed else "fail"})
|
||||
if not passed:
|
||||
return False, results
|
||||
return True, results
|
||||
|
||||
def _revert(self, done: list[dict], ctx: Context) -> bool:
|
||||
"""Undo the mutated steps in reverse order; each revert step must pass its own checks."""
|
||||
reverts = {s["id"]: s for s in self.spec["plan"].get("revert_steps", [])}
|
||||
for step in reversed(done):
|
||||
if not step["mutates"]:
|
||||
continue
|
||||
if step.get("irreversible"):
|
||||
return False
|
||||
revert = reverts[step["revert"]]
|
||||
try:
|
||||
self.d.operations[revert["operation"]](revert, ctx)
|
||||
except Exception:
|
||||
self._record(revert, "revert", "error")
|
||||
return False
|
||||
ok, results = self._run_checks(revert.get("checks_after", []), ctx)
|
||||
self._record(revert, "revert", "done" if ok else "check_failed", results)
|
||||
if not ok:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _attempt(self, ctx: Context) -> str:
|
||||
"""One pass over the plan. Returns completed | reverted | revert_failed | stopped | unknown | budget."""
|
||||
policy = self.spec["stop_and_revert"]
|
||||
done: list[dict] = []
|
||||
for step in self.spec["plan"]["steps"]:
|
||||
if self.d.should_stop() or self._budget_left() <= 0:
|
||||
reason = "stopped" if self.d.should_stop() else "budget"
|
||||
action = policy["on_budget_exhausted"] if reason == "budget" else "revert"
|
||||
if action == "revert" and done:
|
||||
return reason if self._revert(done, ctx) else "revert_failed"
|
||||
return reason
|
||||
try:
|
||||
self.d.operations[step["operation"]](step, ctx)
|
||||
except Unknown:
|
||||
self._record(step, "step", "unknown")
|
||||
return "unknown"
|
||||
except Exception:
|
||||
self._record(step, "step", "unknown")
|
||||
return "unknown"
|
||||
done.append(step)
|
||||
ok, results = self._run_checks(step.get("checks_after", []), ctx) if step["mutates"] else (True, [])
|
||||
self._record(step, "step", "done" if ok else "check_failed", results)
|
||||
if not ok:
|
||||
if policy["on_check_failure"] == "revert":
|
||||
return "reverted" if self._revert(done, ctx) else "revert_failed"
|
||||
return "stopped"
|
||||
return "completed"
|
||||
|
||||
# ------------------------------------------------------------------ evidence
|
||||
def _event(self, kind: str, body: dict) -> dict:
|
||||
return {"id": f"run:{self.run_id}:{'started' if kind == STARTED else 'receipt'}", "type": kind,
|
||||
"correlation_id": self.run_id, "subject": f"admission:{self.spec['id']}",
|
||||
"source": "net-kingdom.bounded-executor", "tenant": self.cfg.tenant, "data": body}
|
||||
|
||||
def _receipt(self, outcome: str, attempts: int) -> dict:
|
||||
end = self.d.wall()
|
||||
return self._event(RECEIPT, {
|
||||
"run_id": self.run_id,
|
||||
"initiator": {"sub": self.approver_subject, "role": "approver"},
|
||||
"executing_workload": self.cfg.executor_identity,
|
||||
"runtime_principal": self.cfg.runtime_principal,
|
||||
"tenant": self.cfg.tenant, "environment": self.cfg.environment,
|
||||
"source_commit": self.spec["plan"]["source"]["commit"],
|
||||
"artifact_digest": self.spec["plan_digest"],
|
||||
"target": f"admission:{self.spec['id']}", "action": ACTION,
|
||||
"decision_refs": [
|
||||
{"label": "admission", "ref": self.spec["id"]},
|
||||
{"label": "admission_digest", "ref": bea.admission_digest(self.doc)},
|
||||
{"label": "approval", "ref": self.approval_id},
|
||||
],
|
||||
"pdp": "not-evaluated",
|
||||
"time_interval": {"start": iso(self.start_wall), "end": iso(end), "clock_source": self.cfg.clock_source},
|
||||
"outcome": outcome,
|
||||
"reverted": any(s["kind"] == "revert" for s in self.steps),
|
||||
"attempts": attempts,
|
||||
"budget": {"limit": {"wall_time_seconds": self.spec["budget"]["wall_time_seconds"],
|
||||
"attempts": self.spec["budget"]["attempts"]},
|
||||
"used": {"wall_time_seconds": round(self.d.monotonic() - self.start_mono, 3),
|
||||
"attempts": attempts}},
|
||||
"steps": self.steps,
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------------ the run
|
||||
def execute(self) -> dict:
|
||||
try:
|
||||
self.preflight()
|
||||
except Refused as refusal:
|
||||
return {"outcome": "refused", "reason": str(refusal), "run_id": None, "effects": False}
|
||||
self.start_wall, self.start_mono = self.d.wall(), self.d.monotonic()
|
||||
try:
|
||||
self.consume()
|
||||
except Refused as refusal: # consumed or not, nothing else may happen now
|
||||
self.d.alert("consume_unconfirmed", {"admission": self.spec["id"], "run_id": self.run_id})
|
||||
return {"outcome": "refused", "reason": str(refusal), "run_id": self.run_id, "effects": "consume-only"}
|
||||
try:
|
||||
self.d.audit.emit(self._event(STARTED, {"run_id": self.run_id, "admission": self.spec["id"],
|
||||
"approval": self.approval_id, "consumed_at": self.consumed_at,
|
||||
"executing_workload": self.cfg.executor_identity}))
|
||||
except Exception:
|
||||
self.d.alert("started_not_in_custody", {"admission": self.spec["id"], "run_id": self.run_id})
|
||||
return {"outcome": "stopped", "reason": "audit_unavailable", "run_id": self.run_id, "effects": "consume-only"}
|
||||
ctx = Context(self.run_id, self.spec["id"], self.start_mono)
|
||||
attempts, outcome = 0, "budget"
|
||||
while attempts < self.spec["budget"]["attempts"]:
|
||||
attempts += 1
|
||||
outcome = self._attempt(ctx)
|
||||
if outcome != "reverted" or self._budget_left() <= 0:
|
||||
break # only a clean revert may be retried, and only within budget
|
||||
if outcome in ("unknown", "revert_failed"):
|
||||
self.d.alert(outcome, {"admission": self.spec["id"], "run_id": self.run_id})
|
||||
receipt = self._receipt(outcome, attempts)
|
||||
try:
|
||||
self.d.audit.emit(receipt)
|
||||
except Exception:
|
||||
self.d.alert("receipt_not_in_custody", {"admission": self.spec["id"], "run_id": self.run_id})
|
||||
return {"outcome": outcome, "reason": "receipt_not_in_custody", "run_id": self.run_id, "receipt": receipt}
|
||||
return {"outcome": outcome, "run_id": self.run_id, "receipt": receipt}
|
||||
372
bounded-executor/tests/test_executor.py
Normal file
372
bounded-executor/tests/test_executor.py
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
"""Core runner tests (NK-WP-0056-T11). Fakes only: carrier, operations, checks, trust, audit, clocks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location("executor", HERE.parent / "executor.py")
|
||||
X = importlib.util.module_from_spec(spec)
|
||||
sys.modules["executor"] = X # dataclasses resolve deferred annotations through sys.modules
|
||||
spec.loader.exec_module(X)
|
||||
bea = X.bea
|
||||
|
||||
PILOT = bea.load(X.REPO / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml")
|
||||
ADMIN_SUB = "kc-sub-0f3a" # opaque KeyCape subject of the human approver `admin`
|
||||
NOW = dt.datetime(2026, 11, 3, 9, 30, tzinfo=dt.UTC)
|
||||
SECRET = "synthetic-bind-value-never-emitted"
|
||||
|
||||
|
||||
class Clock:
|
||||
def __init__(self):
|
||||
self.t = 0.0
|
||||
self.sleeps = []
|
||||
|
||||
def monotonic(self):
|
||||
return self.t
|
||||
|
||||
def sleep(self, s):
|
||||
self.sleeps.append(s)
|
||||
self.t += s
|
||||
|
||||
|
||||
class FakeCarrier:
|
||||
"""approval-engine's object and consume semantics (store.Approval.as_dict, Engine.consume)."""
|
||||
|
||||
def __init__(self, admission, **over):
|
||||
self.approval = {
|
||||
"id": "apr-1", "status": "approved",
|
||||
"binding": {"action": "bounded-execution", "actor": admission["spec"]["executor"]["identity"],
|
||||
"principal": "admin", "purpose": admission["spec"]["purpose"],
|
||||
"target": {"admission_id": admission["spec"]["id"],
|
||||
"admission_digest": bea.admission_digest(admission)},
|
||||
"human_control": True, "pdp_path": False},
|
||||
"validity": {"not_before": admission["spec"]["window"]["not_before"],
|
||||
"expires_at": admission["spec"]["window"]["not_after"]},
|
||||
"entries": [{"subject_id": ADMIN_SUB, "assurance": "aal2", "principal_type": "human",
|
||||
"approved_at": "2026-11-03T08:55:00Z"}],
|
||||
}
|
||||
for k, v in over.items():
|
||||
self.approval[k] = v
|
||||
self.consumes = 0
|
||||
self.raise_on_consume = False
|
||||
self.consume_lands = True
|
||||
|
||||
def get(self, approval_id):
|
||||
return copy.deepcopy(self.approval)
|
||||
|
||||
def consume(self, approval_id, request_digest, decision_id):
|
||||
self.consumes += 1
|
||||
if self.consume_lands:
|
||||
if self.approval["status"] == "consumed":
|
||||
same = self.approval["consumption"]["request_digest"] == request_digest
|
||||
if not same:
|
||||
raise RuntimeError("409")
|
||||
return {"approval_id": approval_id, "status": "consumed", "request_digest": request_digest,
|
||||
"decision_id": self.approval["consumption"].get("decision_id"),
|
||||
"consumed_at": "x", "idempotent": True}
|
||||
self.approval["status"] = "consumed"
|
||||
self.approval["consumption"] = {"request_digest": request_digest, "decision_id": decision_id,
|
||||
"consumed_at": "2026-11-03T09:30:00Z"}
|
||||
if self.raise_on_consume:
|
||||
raise TimeoutError("connection reset")
|
||||
return {"approval_id": approval_id, "status": "consumed", "request_digest": request_digest,
|
||||
"decision_id": decision_id, "consumed_at": "2026-11-03T09:30:00Z", "idempotent": False}
|
||||
|
||||
|
||||
class World:
|
||||
def __init__(self, admission, clock):
|
||||
self.calls = []
|
||||
self.fail_check = {} # check id -> number of times to fail
|
||||
self.raise_op = set() # operation names that raise
|
||||
self.unknown_op = set()
|
||||
self.slow_op = 0.0
|
||||
self.events = []
|
||||
self.alerts = []
|
||||
self.audit_down = False
|
||||
self.stop_after = None
|
||||
self.clock = clock
|
||||
self.carrier = FakeCarrier(admission)
|
||||
self.trust_ok = True
|
||||
|
||||
def op(self, step, ctx):
|
||||
if step["operation"] in self.unknown_op:
|
||||
raise X.Unknown()
|
||||
if step["id"] in self.raise_op:
|
||||
raise RuntimeError("boom")
|
||||
ctx.data["value"] = SECRET # operations may hold values; the runner must never emit them
|
||||
self.calls.append(step["id"])
|
||||
self.clock.t += self.slow_op
|
||||
|
||||
def check(self, check, ctx):
|
||||
self.calls.append("check:" + check["id"])
|
||||
left = self.fail_check.get(check["id"], 0)
|
||||
if left:
|
||||
self.fail_check[check["id"]] = left - 1
|
||||
return False
|
||||
return True
|
||||
|
||||
def emit(self, event):
|
||||
if self.audit_down:
|
||||
raise ConnectionError()
|
||||
self.events.append(event)
|
||||
|
||||
def should_stop(self):
|
||||
return self.stop_after is not None and len([c for c in self.calls if not c.startswith("check:")]) >= self.stop_after
|
||||
|
||||
def deps(self, admission):
|
||||
ops = {s["operation"]: self.op for s in admission["spec"]["plan"]["steps"] + admission["spec"]["plan"]["revert_steps"]}
|
||||
checks = {c["id"]: self.check for c in admission["spec"]["checks"]}
|
||||
audit = type("A", (), {"emit": lambda _s, e: self.emit(e)})()
|
||||
return X.Deps(carrier=self.carrier, operations=ops, checks=checks, trust=lambda s: self.trust_ok,
|
||||
audit=audit, alert=lambda kind, info: self.alerts.append(kind),
|
||||
should_stop=self.should_stop, wall=lambda: NOW,
|
||||
monotonic=self.clock.monotonic, sleep=self.clock.sleep)
|
||||
|
||||
|
||||
def synthetic_admission(root: pathlib.Path, mutate=None) -> dict:
|
||||
"""The pilot example, with its pinned files replaced by temporary ones and the digest recomputed."""
|
||||
doc = copy.deepcopy(PILOT)
|
||||
files = []
|
||||
for f in doc["spec"]["plan"]["source"]["files"]:
|
||||
path = root / f["path"]
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("synthetic " + f["path"])
|
||||
files.append({"path": f["path"], "sha256": hashlib.sha256(path.read_bytes()).hexdigest()})
|
||||
doc["spec"]["plan"]["source"]["files"] = files
|
||||
by_path = {f["path"]: f["sha256"] for f in files}
|
||||
for c in doc["spec"]["checks"]:
|
||||
c["sha256"] = by_path[c["ref"]]
|
||||
if mutate:
|
||||
mutate(doc["spec"])
|
||||
doc["spec"]["plan_digest"] = bea.plan_digest(doc)
|
||||
return doc
|
||||
|
||||
|
||||
class Base(unittest.TestCase):
|
||||
mutate = None
|
||||
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
self.root = pathlib.Path(tmp.name)
|
||||
self.doc = synthetic_admission(self.root, self.mutate)
|
||||
self.clock = Clock()
|
||||
self.w = World(self.doc, self.clock)
|
||||
self.cfg = X.Config(executor_identity="service:nk-bind-executor",
|
||||
runtime_principal="system:serviceaccount:sso:nk-bind-executor",
|
||||
tenant="tenant:platform", environment="PRODUCTION",
|
||||
approver_subjects={"admin": ADMIN_SUB}, source_root=self.root)
|
||||
|
||||
def run_it(self, doc=None):
|
||||
doc = doc or self.doc
|
||||
return X.Run(doc, "apr-1", self.cfg, self.w.deps(doc)).execute()
|
||||
|
||||
def ops(self):
|
||||
return [c for c in self.w.calls if not c.startswith("check:")]
|
||||
|
||||
def assert_no_effect(self, result, reason):
|
||||
self.assertEqual((result["outcome"], result["reason"]), ("refused", reason))
|
||||
self.assertEqual(self.w.carrier.consumes, 0)
|
||||
self.assertEqual(self.w.calls, [])
|
||||
self.assertEqual(self.w.events, [])
|
||||
|
||||
|
||||
class HappyPathTests(Base):
|
||||
def test_completes_with_started_and_receipt(self):
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "completed", result)
|
||||
self.assertEqual(self.w.carrier.consumes, 1)
|
||||
self.assertEqual(self.ops(), ["generate", "set-directory", "deliver", "restart"])
|
||||
self.assertEqual([e["type"] for e in self.w.events], [X.STARTED, X.RECEIPT])
|
||||
started, receipt = self.w.events
|
||||
self.assertEqual(started["id"], f"run:{result['run_id']}:started")
|
||||
data = receipt["data"]
|
||||
self.assertEqual(data["pdp"], "not-evaluated")
|
||||
self.assertEqual({r["label"] for r in data["decision_refs"]}, {"admission", "admission_digest", "approval"})
|
||||
self.assertEqual(data["initiator"]["sub"], ADMIN_SUB)
|
||||
self.assertEqual(data["artifact_digest"], self.doc["spec"]["plan_digest"])
|
||||
self.assertFalse(data["reverted"])
|
||||
self.assertNotIn(SECRET, json.dumps(self.w.events))
|
||||
|
||||
def test_consumed_with_admission_digest_and_run_id(self):
|
||||
result = self.run_it()
|
||||
consumption = self.w.carrier.approval["consumption"]
|
||||
self.assertEqual(consumption["request_digest"], bea.admission_digest(self.doc))
|
||||
self.assertEqual(consumption["decision_id"], result["run_id"])
|
||||
|
||||
|
||||
class RefusalTests(Base):
|
||||
def test_invalid_admission(self):
|
||||
doc = copy.deepcopy(self.doc)
|
||||
doc["spec"]["plan"]["steps"][0]["operation"] = "tampered"
|
||||
self.w = World(doc, self.clock)
|
||||
self.assertEqual(self.run_it(doc)["reason"], "admission_invalid:BEA-2")
|
||||
self.assertEqual(self.w.carrier.consumes, 0)
|
||||
|
||||
def test_refusals_before_any_effect(self):
|
||||
cases = {
|
||||
"not_this_executor": lambda: setattr(self.cfg, "executor_identity", "service:other"),
|
||||
"source_drift": lambda: (self.root / "tools/service-binds/bind-lane.py").write_text("changed"),
|
||||
"trust_not_satisfied:runtime_secret_trust": lambda: setattr(self.w, "trust_ok", False),
|
||||
"approval_binding": lambda: self.w.carrier.approval["binding"]["target"].update(admission_digest="sha256:" + "0" * 64),
|
||||
"approval_status": lambda: self.w.carrier.approval.update(status="requested"),
|
||||
"approval_entry": lambda: self.w.carrier.approval["entries"][0].update(assurance="aal1"),
|
||||
"approval_expires_before_window_end": lambda: self.w.carrier.approval["validity"].update(expires_at="2026-11-03T10:00:00Z"),
|
||||
}
|
||||
for reason, mutate in cases.items():
|
||||
with self.subTest(reason=reason):
|
||||
self.setUp()
|
||||
mutate()
|
||||
self.assert_no_effect(self.run_it(), reason)
|
||||
|
||||
def test_approval_by_someone_not_listed_or_not_human(self):
|
||||
for entry in ({"subject_id": "kc-sub-other", "assurance": "aal2", "principal_type": "human"},
|
||||
{"subject_id": ADMIN_SUB, "assurance": "aal2", "principal_type": "service"}):
|
||||
with self.subTest(entry=entry):
|
||||
self.setUp()
|
||||
self.w.carrier.approval["entries"] = [entry]
|
||||
self.assert_no_effect(self.run_it(), "approval_entry")
|
||||
|
||||
def test_actor_must_be_this_executor(self):
|
||||
self.w.carrier.approval["binding"]["actor"] = "service:someone-else"
|
||||
self.assert_no_effect(self.run_it(), "approval_binding")
|
||||
|
||||
def test_outside_window(self):
|
||||
self.w.deps = (lambda orig: (lambda doc: (lambda d: (setattr(d, "wall", lambda: NOW + dt.timedelta(hours=3)), d)[1])(orig(doc))))(self.w.deps)
|
||||
self.assert_no_effect(self.run_it(), "outside_window")
|
||||
|
||||
def test_window_too_short_for_the_budget(self):
|
||||
late = dt.datetime(2026, 11, 3, 10, 50, tzinfo=dt.UTC) # 10 min left, budget 20 min
|
||||
orig = self.w.deps
|
||||
self.w.deps = lambda doc: (lambda d: (setattr(d, "wall", lambda: late), d)[1])(orig(doc))
|
||||
self.assert_no_effect(self.run_it(), "window_shorter_than_budget")
|
||||
|
||||
def test_already_consumed_is_never_reused(self):
|
||||
self.w.carrier.approval["status"] = "consumed"
|
||||
self.w.carrier.approval["consumption"] = {"request_digest": bea.admission_digest(self.doc), "decision_id": "earlier-run"}
|
||||
self.assert_no_effect(self.run_it(), "approval_status")
|
||||
|
||||
def test_unknown_operation(self):
|
||||
d = self.w.deps(self.doc)
|
||||
d.operations.pop("eso.force-sync")
|
||||
result = X.Run(self.doc, "apr-1", self.cfg, d).execute()
|
||||
self.assert_no_effect(result, "unknown_operation")
|
||||
|
||||
|
||||
class ConsumeTests(Base):
|
||||
def test_unclear_consume_resolved_by_reading_back(self):
|
||||
self.w.carrier.raise_on_consume = True # consume landed, response lost
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "completed")
|
||||
self.assertEqual(self.w.carrier.consumes, 1) # never consumed twice
|
||||
|
||||
def test_unclear_consume_that_did_not_land_stops(self):
|
||||
self.w.carrier.raise_on_consume = True
|
||||
self.w.carrier.consume_lands = False
|
||||
result = self.run_it()
|
||||
self.assertEqual((result["outcome"], result["reason"]), ("refused", "consume_not_confirmed"))
|
||||
self.assertEqual((self.w.calls, self.w.events), ([], []))
|
||||
self.assertIn("consume_unconfirmed", self.w.alerts)
|
||||
|
||||
def test_idempotent_consume_is_not_a_fresh_run(self):
|
||||
original = self.w.carrier.consume
|
||||
self.w.carrier.consume = lambda a, d, r: dict(original(a, d, r), idempotent=True)
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["reason"], "consume_response")
|
||||
self.assertEqual(self.w.calls, [])
|
||||
|
||||
def test_no_custody_no_run(self):
|
||||
self.w.audit_down = True
|
||||
result = self.run_it()
|
||||
self.assertEqual((result["outcome"], result["reason"]), ("stopped", "audit_unavailable"))
|
||||
self.assertEqual(self.w.calls, [])
|
||||
self.assertIn("started_not_in_custody", self.w.alerts)
|
||||
|
||||
|
||||
class FailureTests(Base):
|
||||
def test_failed_check_reverts_in_reverse_and_verifies(self):
|
||||
self.doc = synthetic_admission(self.root, lambda s: s["budget"].update(attempts=1))
|
||||
self.w = World(self.doc, self.clock)
|
||||
self.w.fail_check["eso-synced"] = 1
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "reverted", result)
|
||||
self.assertEqual(self.ops(), ["generate", "set-directory", "deliver", "redeliver", "restore-directory", "restore-kv"])
|
||||
data = result["receipt"]["data"]
|
||||
self.assertTrue(data["reverted"])
|
||||
reverts = [x for x in data["steps"] if x["kind"] == "revert"]
|
||||
self.assertEqual([x["checks"][0]["id"] for x in reverts], ["eso-synced", "bind-signs-in", "kv-advanced"])
|
||||
self.assertTrue(all(x["outcome"] == "done" for x in reverts))
|
||||
self.assertEqual(data["attempts"], 1)
|
||||
|
||||
def test_clean_revert_may_retry_within_attempts(self):
|
||||
self.doc = synthetic_admission(self.root, lambda s: s["budget"].update(attempts=2))
|
||||
self.w = World(self.doc, self.clock)
|
||||
self.w.fail_check["synthetic-journey"] = 1
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "completed")
|
||||
self.assertEqual(result["receipt"]["data"]["attempts"], 2)
|
||||
self.assertTrue(result["receipt"]["data"]["reverted"])
|
||||
|
||||
def test_revert_whose_check_fails_counts_as_failed(self):
|
||||
self.doc = synthetic_admission(self.root, lambda s: s["budget"].update(attempts=1))
|
||||
self.w = World(self.doc, self.clock)
|
||||
self.w.fail_check["eso-synced"] = 2 # the forward check and the revert's own check
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "revert_failed")
|
||||
self.assertNotIn("restore-directory", self.ops())
|
||||
|
||||
def test_failed_revert_stops_and_alerts(self):
|
||||
self.w.fail_check["eso-synced"] = 1
|
||||
self.w.raise_op = {"restore-directory"}
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "revert_failed")
|
||||
self.assertIn("revert_failed", self.w.alerts)
|
||||
self.assertNotIn("restore-kv", self.ops()) # stop at the failed revert, do not continue blindly
|
||||
|
||||
def test_unknown_outcome_stops_without_revert(self):
|
||||
self.w.unknown_op = {"eso.force-sync"}
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "unknown")
|
||||
self.assertEqual(self.ops(), ["generate", "set-directory"])
|
||||
self.assertFalse(result["receipt"]["data"]["reverted"])
|
||||
self.assertIn("unknown", self.w.alerts)
|
||||
|
||||
def test_budget_exhausted_reverts(self):
|
||||
self.w.slow_op = 700.0 # wall_time_seconds is 1200
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "budget")
|
||||
self.assertTrue(result["receipt"]["data"]["reverted"])
|
||||
|
||||
def test_kill_switch_reverts(self):
|
||||
self.w.stop_after = 2
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "stopped")
|
||||
self.assertIn("restore-kv", self.ops())
|
||||
self.assertNotIn("deliver", self.ops())
|
||||
|
||||
def test_settle_time_is_waited_and_counted(self):
|
||||
self.doc = synthetic_admission(self.root, lambda s: s["checks"][3].update(settle_seconds=125))
|
||||
self.w = World(self.doc, self.clock)
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["outcome"], "completed")
|
||||
self.assertIn(125, self.clock.sleeps)
|
||||
|
||||
def test_receipt_custody_failure_is_reported(self):
|
||||
original = self.w.emit
|
||||
self.w.emit = lambda e: (original(e) if e["type"] == X.STARTED else (_ for _ in ()).throw(ConnectionError()))
|
||||
result = self.run_it()
|
||||
self.assertEqual(result["reason"], "receipt_not_in_custody")
|
||||
self.assertIn("receipt_not_in_custody", self.w.alerts)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -104,7 +104,7 @@ for up to 120 s, so a check run sooner proves nothing.
|
|||
| BEA-6 | Least privilege: every step and revert step is covered by a ceiling grant of the same mode, and every ceiling grant covers at least one step |
|
||||
| BEA-7 | No `human-intrinsic` step. A `root-class` step needs `high` risk |
|
||||
| BEA-8 | Every step's consumer is in the blast radius. `medium` and `high` risk change one consumer at a time |
|
||||
| BEA-9 | Every mutating step names checks. Every check is used and pinned: its `ref` and `sha256` appear in `plan.source.files` |
|
||||
| BEA-9 | Every mutating step names checks, revert steps included: a revert must prove the restored state with its own checks. Every check is used and pinned: its `ref` and `sha256` appear in `plan.source.files` |
|
||||
| BEA-10 | Every mutating step has a revert step, or is `irreversible` at `high` risk. Revert steps mutate and do not nest. Step ids are unique |
|
||||
| BEA-11 | A revertable plan reverts on check failure. A plan with nothing to revert stops |
|
||||
| BEA-12 | The wall time fits the window, the checks (timeouts plus settle times) fit the wall time, and attempts are at most 3 (1 at `high` risk) |
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ spec:
|
|||
plan:
|
||||
source:
|
||||
repo: net-kingdom
|
||||
commit: c9ab01e23e92
|
||||
commit: 3b1abc405423
|
||||
files:
|
||||
- path: tools/service-binds/bind-lane.py
|
||||
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
|
||||
|
|
@ -114,6 +114,8 @@ spec:
|
|||
requires:
|
||||
mode: OPERATE
|
||||
scope: openbao/platform/workloads/net-kingdom/directory-binds/svc-authelia
|
||||
checks_after:
|
||||
- kv-advanced
|
||||
- id: restore-directory
|
||||
operation: lldap.user.change-own-password-from-kv
|
||||
target: lldap:svc-authelia
|
||||
|
|
@ -123,6 +125,8 @@ spec:
|
|||
requires:
|
||||
mode: OPERATE
|
||||
scope: lldap/people/svc-authelia
|
||||
checks_after:
|
||||
- bind-signs-in
|
||||
- id: redeliver
|
||||
operation: eso.force-sync
|
||||
target: k8s:sso/externalsecret/authelia-secrets
|
||||
|
|
@ -132,6 +136,8 @@ spec:
|
|||
requires:
|
||||
mode: OPERATE
|
||||
scope: k8s/sso/externalsecret/authelia-secrets
|
||||
checks_after:
|
||||
- eso-synced
|
||||
- id: restart-again
|
||||
operation: k8s.rollout-restart
|
||||
target: k8s:sso/deployment/authelia
|
||||
|
|
@ -141,7 +147,9 @@ spec:
|
|||
requires:
|
||||
mode: OPERATE
|
||||
scope: k8s/sso/deployment/authelia
|
||||
plan_digest: sha256:36328096076c71ade582173092511be6aee546281ef5edb3c2acaadaf7c9e1fb
|
||||
checks_after:
|
||||
- synthetic-journey
|
||||
plan_digest: sha256:ce9253adf5084dc1b40c1004258d9f8480b236d8ed26fabbb2a983c853a4dc21
|
||||
checks:
|
||||
- id: kv-advanced
|
||||
description: KV svc-authelia has exactly one new version, CAS on the previous one
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ spec:
|
|||
plan:
|
||||
source:
|
||||
repo: net-kingdom
|
||||
commit: c9ab01e23e92
|
||||
commit: 3b1abc405423
|
||||
files:
|
||||
- path: sso-mfa/k8s/lldap/bind-split.py
|
||||
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
||||
|
|
@ -84,6 +84,8 @@ spec:
|
|||
requires:
|
||||
mode: ADMINISTER
|
||||
scope: lldap
|
||||
checks_after:
|
||||
- accounts-read-only
|
||||
- id: destroy-kv
|
||||
operation: openbao.kv.destroy-created
|
||||
target: openbao:platform/workloads/net-kingdom/directory-binds
|
||||
|
|
@ -93,7 +95,9 @@ spec:
|
|||
requires:
|
||||
mode: OPERATE
|
||||
scope: openbao/platform/workloads/net-kingdom/directory-binds
|
||||
plan_digest: sha256:139e7b874d8d8ba9419f141bf9e10341a9a0057bb75fa47eee32c2ee19b396ce
|
||||
checks_after:
|
||||
- accounts-sign-in
|
||||
plan_digest: sha256:a4c60ad06ae93b7ebc4d12417678a7bf8e7507f6670d8861cb5b30c2cd517b68
|
||||
checks:
|
||||
- id: accounts-read-only
|
||||
description: each account exists in lldap_strict_readonly only and cannot write
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ spec:
|
|||
plan:
|
||||
source:
|
||||
repo: net-kingdom
|
||||
commit: c9ab01e23e92
|
||||
commit: 3b1abc405423
|
||||
files:
|
||||
- path: sso-mfa/k8s/lldap/bind-split.py
|
||||
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
||||
|
|
@ -73,7 +73,7 @@ spec:
|
|||
scope: k8s/sso
|
||||
checks_after:
|
||||
- split-shape
|
||||
plan_digest: sha256:0459999b93bb8aee0dacb1b36ea95d114ac74d0ed775095f3ee5dc2ee66203cd
|
||||
plan_digest: sha256:8c2195390ce140d2600667f2787c9fa2685a62ff918fb298d17e3b3aa3762bc5
|
||||
checks:
|
||||
- id: split-shape
|
||||
description: every consumer binds as its own svc- account; no service binds as admin
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ spec:
|
|||
plan:
|
||||
source:
|
||||
repo: net-kingdom
|
||||
commit: c9ab01e23e92
|
||||
commit: 3b1abc405423
|
||||
files:
|
||||
- path: sso-mfa/k8s/lldap/bind-split.py
|
||||
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
||||
|
|
@ -84,6 +84,8 @@ spec:
|
|||
requires:
|
||||
mode: ADMINISTER
|
||||
scope: lldap
|
||||
checks_after:
|
||||
- ld-admin-administers
|
||||
- id: restart-lldap-again
|
||||
operation: k8s.rollout-restart
|
||||
target: k8s:sso/deployment/lldap
|
||||
|
|
@ -93,7 +95,9 @@ spec:
|
|||
requires:
|
||||
mode: OPERATE
|
||||
scope: k8s/sso/deployment/lldap
|
||||
plan_digest: sha256:5f51601ce96a07da5fa56b5b00c2066c572e3b19a4677b1a16cee3178295cae0
|
||||
checks_after:
|
||||
- synthetic-journey
|
||||
plan_digest: sha256:78e9b24a77955c0b7372d6733e8a6b4da17656eb5a9100a416c4d73eef981d6f
|
||||
checks:
|
||||
- id: ld-admin-administers
|
||||
description: ld-admin signs in and administers; admin keeps lldap_admin membership
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ spec:
|
|||
plan:
|
||||
source:
|
||||
repo: net-kingdom
|
||||
commit: c9ab01e23e92
|
||||
commit: 3b1abc405423
|
||||
files:
|
||||
- path: sso-mfa/k8s/lldap/bind-split.py
|
||||
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
||||
|
|
@ -95,6 +95,8 @@ spec:
|
|||
requires:
|
||||
mode: ADMINISTER
|
||||
scope: lldap
|
||||
checks_after:
|
||||
- ld-admin-administers
|
||||
- id: restart-lldap-again
|
||||
operation: k8s.rollout-restart
|
||||
target: k8s:sso/deployment/lldap
|
||||
|
|
@ -104,7 +106,9 @@ spec:
|
|||
requires:
|
||||
mode: OPERATE
|
||||
scope: k8s/sso/deployment/lldap
|
||||
plan_digest: sha256:139f0ca7564184290c7cb1f6b20bdb9807db98e5609a79083ac6f7aea13bb50c
|
||||
checks_after:
|
||||
- synthetic-journey
|
||||
plan_digest: sha256:80c14028288caf0994c1c5295e423527969c91f174f0ab2f7e08900f71283887
|
||||
checks:
|
||||
- id: ld-admin-administers
|
||||
description: ld-admin signs in and administers; admin keeps lldap_admin membership
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ rotation = doc(
|
|||
step("set-directory", "lldap.user.change-own-password-from-kv", "lldap:svc-authelia", "authelia", "delegable", True, "OPERATE", "lldap/people/svc-authelia", ["bind-signs-in"], "restore-directory"),
|
||||
step("deliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets", ["eso-synced"], "redeliver"),
|
||||
step("restart", "k8s.rollout-restart", "k8s:sso/deployment/authelia", "authelia", "delegable", True, "OPERATE", "k8s/sso/deployment/authelia", ["synthetic-journey"], "restart-again")],
|
||||
[step("restore-kv", "openbao.kv.write-previous-value-as-next-version", "openbao:" + KV[len("openbao/"):], "authelia", "delegable", True, "OPERATE", KV),
|
||||
step("restore-directory", "lldap.user.change-own-password-from-kv", "lldap:svc-authelia", "authelia", "delegable", True, "OPERATE", "lldap/people/svc-authelia"),
|
||||
step("redeliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets"),
|
||||
step("restart-again", "k8s.rollout-restart", "k8s:sso/deployment/authelia", "authelia", "delegable", True, "OPERATE", "k8s/sso/deployment/authelia")],
|
||||
[step("restore-kv", "openbao.kv.write-previous-value-as-next-version", "openbao:" + KV[len("openbao/"):], "authelia", "delegable", True, "OPERATE", KV, ["kv-advanced"]),
|
||||
step("restore-directory", "lldap.user.change-own-password-from-kv", "lldap:svc-authelia", "authelia", "delegable", True, "OPERATE", "lldap/people/svc-authelia", ["bind-signs-in"]),
|
||||
step("redeliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets", ["eso-synced"]),
|
||||
step("restart-again", "k8s.rollout-restart", "k8s:sso/deployment/authelia", "authelia", "delegable", True, "OPERATE", "k8s/sso/deployment/authelia", ["synthetic-journey"])],
|
||||
[check("kv-advanced", "KV svc-authelia has exactly one new version, CAS on the previous one", LANE, 30),
|
||||
check("bind-signs-in", "the new KV value signs in to LLDAP as svc-authelia", LANE, 30),
|
||||
check("eso-synced", "the ExternalSecret is Ready with a refresh after the step started; the executor never reads the Secret", LANE, 120),
|
||||
|
|
@ -85,8 +85,8 @@ create = doc(
|
|||
[SPLIT, LANE],
|
||||
[step("create-accounts", "lldap.user.create-read-only-binds", "lldap:svc-authelia,svc-keycape,svc-privacyidea", "lldap", "root-class", True, "ADMINISTER", "lldap", ["accounts-read-only"], "delete-accounts"),
|
||||
step("hold-passwords", "openbao.kv.write-cas0", "openbao:platform/workloads/net-kingdom/directory-binds", "lldap", "root-class", True, "OPERATE", "openbao/platform/workloads/net-kingdom/directory-binds", ["accounts-sign-in"], "destroy-kv")],
|
||||
[step("delete-accounts", "lldap.user.delete-created", "lldap:svc-authelia,svc-keycape,svc-privacyidea", "lldap", "root-class", True, "ADMINISTER", "lldap"),
|
||||
step("destroy-kv", "openbao.kv.destroy-created", "openbao:platform/workloads/net-kingdom/directory-binds", "lldap", "root-class", True, "OPERATE", "openbao/platform/workloads/net-kingdom/directory-binds")],
|
||||
[step("delete-accounts", "lldap.user.delete-created", "lldap:svc-authelia,svc-keycape,svc-privacyidea", "lldap", "root-class", True, "ADMINISTER", "lldap", ["accounts-read-only"]),
|
||||
step("destroy-kv", "openbao.kv.destroy-created", "openbao:platform/workloads/net-kingdom/directory-binds", "lldap", "root-class", True, "OPERATE", "openbao/platform/workloads/net-kingdom/directory-binds", ["accounts-sign-in"])],
|
||||
[check("accounts-read-only", "each account exists in lldap_strict_readonly only and cannot write", SPLIT, 60),
|
||||
check("accounts-sign-in", "each KV value signs in to LLDAP as its account", LANE, 60)],
|
||||
{"on_check_failure": "revert", "on_budget_exhausted": "revert", "on_unknown": "stop",
|
||||
|
|
@ -104,8 +104,8 @@ superadmin = doc(
|
|||
[SPLIT, JOURNEY],
|
||||
[step("switch-user-dn", "lldap.config.set-ldap-user-dn", "k8s:sso/secret/lldap-secrets#LLDAP_LDAP_USER_DN", "lldap", "root-class", True, "ADMINISTER", "lldap", ["ld-admin-administers"], "switch-back"),
|
||||
step("restart-lldap", "k8s.rollout-restart", "k8s:sso/deployment/lldap", "lldap", "root-class", True, "OPERATE", "k8s/sso/deployment/lldap", ["synthetic-journey"], "restart-lldap-again")],
|
||||
[step("switch-back", "lldap.config.set-ldap-user-dn", "k8s:sso/secret/lldap-secrets#LLDAP_LDAP_USER_DN", "lldap", "root-class", True, "ADMINISTER", "lldap"),
|
||||
step("restart-lldap-again", "k8s.rollout-restart", "k8s:sso/deployment/lldap", "lldap", "root-class", True, "OPERATE", "k8s/sso/deployment/lldap")],
|
||||
[step("switch-back", "lldap.config.set-ldap-user-dn", "k8s:sso/secret/lldap-secrets#LLDAP_LDAP_USER_DN", "lldap", "root-class", True, "ADMINISTER", "lldap", ["ld-admin-administers"]),
|
||||
step("restart-lldap-again", "k8s.rollout-restart", "k8s:sso/deployment/lldap", "lldap", "root-class", True, "OPERATE", "k8s/sso/deployment/lldap", ["synthetic-journey"])],
|
||||
[check("ld-admin-administers", "ld-admin signs in and administers; admin keeps lldap_admin membership", SPLIT, 60),
|
||||
check("synthetic-journey", "all three binds still serve sign-in: syn-journey reaches aal2", JOURNEY, 90, settle=125)],
|
||||
{"on_check_failure": "revert", "on_budget_exhausted": "revert", "on_unknown": "stop",
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ def evaluate(document: Any, schema: dict, as_of: dt.datetime | None = None) -> d
|
|||
if len(checks) != len(spec["checks"]):
|
||||
fail("BEA-9", "spec/checks:duplicate-id")
|
||||
used = set()
|
||||
for step in steps:
|
||||
for step in every_step: # revert steps verify the restored state with their own checks
|
||||
if step["mutates"] and not step.get("checks_after"):
|
||||
fail("BEA-9", f"step/{step['id']}/checks_after:missing")
|
||||
for ref in step.get("checks_after", []):
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ against a deliberately broken bind on a synthetic directory (T08).
|
|||
|
||||
```task
|
||||
id: NK-WP-0056-T11
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
assignee: claude-code
|
||||
depends_on: [NK-WP-0056-T01, NK-WP-0056-T03]
|
||||
|
|
@ -432,6 +432,40 @@ Tested against in-process fakes for the carrier, operations and audit sink.
|
|||
**Done when:** the runner passes its tests, including revert on a failed check, budget exhaustion, a stale or
|
||||
reused approval, and a step outside the ceiling. Every refusal must happen before any effect.
|
||||
|
||||
**Done 2026-10-31.** `bounded-executor/executor.py` with 22 tests (`bounded-executor/tests/test_executor.py`; fakes
|
||||
for carrier, operations, checks, trust, audit, clocks), modelled on approval-engine's real object and consume
|
||||
response. Refusals before any effect cover:
|
||||
|
||||
- an invalid admission;
|
||||
- another executor;
|
||||
- source drift;
|
||||
- unsatisfied trust;
|
||||
- an unknown operation;
|
||||
- outside the window, or a window shorter than the budget;
|
||||
- a binding digest or actor mismatch;
|
||||
- a non-human, aal1 or unlisted approver;
|
||||
- an already-consumed approval.
|
||||
|
||||
Consume:
|
||||
|
||||
- an unclear consume is read back and never repeated;
|
||||
- an idempotent response is refused;
|
||||
- with no audit custody, no run.
|
||||
|
||||
Failure handling:
|
||||
|
||||
- a failed check reverts in reverse order, each revert proving the restored state with its own checks;
|
||||
- a clean revert retries within `attempts`;
|
||||
- a failed revert, or a revert check that fails, stops and alerts;
|
||||
- an unknown outcome stops without reverting;
|
||||
- budget exhaustion and the kill switch revert;
|
||||
- settle times are waited and counted;
|
||||
- a receipt-custody failure is reported.
|
||||
|
||||
Receipts carry no operation values.
|
||||
|
||||
Standard and validator change: BEA-9 now applies to revert steps, which name their own checks; examples regenerated.
|
||||
|
||||
## Executor runtime: pilot operations and packaging
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue