APPROVAL-WP-0007-T01 done: required_assurance floor; executor ingress; alignment answer
- Optional required_assurance {"level": aal1|aal2|aal3} at issue, schema v7.
Enforced at add_entry (403), claim (assurance_unsatisfied) and consume (409)
from the verified token level only; inherited on supersession; legacy rows
NULL. Served only when declared so undeclared claims keep exact bytes.
- deploy/networkpolicies.yaml: pod-scoped ingress for sso +
app.kubernetes.io/name=bounded-executor on 8080 (hub 39a3bffa). Not applied.
- docs/decision-services-alignment-answer.md: owner answer to the NetKingdom
decision services review (hub 81cafbf3): roles accepted, T02 split proposed,
presentation_ref at create declined with an entry-level counter-proposal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
parent
e5f31477d4
commit
e9e9f93317
14 changed files with 540 additions and 13 deletions
|
|
@ -133,6 +133,7 @@ class App:
|
|||
pdp_digest=data.get("pdp_digest"),
|
||||
pdp_path=bool(data.get("pdp_path", False)),
|
||||
human_control=data.get("human_control", False),
|
||||
required_assurance=data.get("required_assurance"),
|
||||
approval_id=data.get("id"),
|
||||
)
|
||||
return 201, obj.as_dict()
|
||||
|
|
|
|||
|
|
@ -43,7 +43,47 @@ EVENT_CLASSES = ("issuance", "use", "supersession", "revocation", "heartbeat")
|
|||
# class and says nothing about the quiet one — here that is `revocation`, which
|
||||
# is the class whose silence actually matters.
|
||||
HEARTBEAT_CLASSES = ("issuance", "use", "supersession", "revocation")
|
||||
LATEST_SCHEMA_VERSION = 6
|
||||
LATEST_SCHEMA_VERSION = 7
|
||||
# APPROVAL-WP-0007-T01: authenticator assurance levels, weakest first. A level
|
||||
# outside this list is unknown, and an unknown level never meets a floor.
|
||||
ASSURANCE_LEVELS = ("aal1", "aal2", "aal3")
|
||||
|
||||
|
||||
def assurance_rank(level: Any) -> int | None:
|
||||
if isinstance(level, str) and level in ASSURANCE_LEVELS:
|
||||
return ASSURANCE_LEVELS.index(level)
|
||||
return None
|
||||
|
||||
|
||||
def require_assurance_floor(value: Any) -> str | None:
|
||||
"""Validate an issue-time `required_assurance` and return its level.
|
||||
|
||||
The shape mirrors the token's `assurance` claim: `{"level": "aal2"}`.
|
||||
Absent or null means no floor (the pre-v7 behavior).
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, dict) or set(value) != {"level"}:
|
||||
raise Unprocessable('required_assurance must be {"level": "aal1|aal2|aal3"}')
|
||||
if assurance_rank(value["level"]) is None:
|
||||
raise Unprocessable("required_assurance.level must be one of aal1, aal2, aal3")
|
||||
return value["level"]
|
||||
|
||||
|
||||
def entry_meets_floor(assurance: str | None, floor: str | None) -> bool:
|
||||
"""Whether a stored entry's verified assurance meets `floor`.
|
||||
|
||||
`assurance` is the JSON the HTTP boundary stored from the verified token.
|
||||
A missing, unparsable or unknown level fails a declared floor.
|
||||
"""
|
||||
if floor is None:
|
||||
return True
|
||||
try:
|
||||
level = json.loads(assurance).get("level") if assurance else None
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
level = None
|
||||
rank = assurance_rank(level)
|
||||
return rank is not None and rank >= assurance_rank(floor)
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS approvals (
|
||||
|
|
@ -66,6 +106,7 @@ CREATE TABLE IF NOT EXISTS approvals (
|
|||
consumed_at TEXT,
|
||||
pdp_path INTEGER NOT NULL DEFAULT 0,
|
||||
human_control INTEGER NOT NULL DEFAULT 0 CHECK (human_control IN (0, 1)),
|
||||
required_assurance TEXT CHECK (required_assurance IS NULL OR required_assurance IN ('aal1', 'aal2', 'aal3')),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
|
@ -123,6 +164,7 @@ class Approval:
|
|||
pdp_digest: str | None
|
||||
pdp_path: bool
|
||||
human_control: bool
|
||||
required_assurance: str | None
|
||||
actor: str
|
||||
principal: str
|
||||
action: str
|
||||
|
|
@ -157,6 +199,10 @@ class Approval:
|
|||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
if self.required_assurance:
|
||||
# APPROVAL-WP-0007-T01. Stated only when declared, so undeclared
|
||||
# objects keep their exact pre-v7 shape for strict readers.
|
||||
result["binding"]["required_assurance"] = {"level": self.required_assurance}
|
||||
if self.consumed_digest:
|
||||
result["consumption"] = {
|
||||
"request_digest": self.consumed_digest,
|
||||
|
|
@ -251,6 +297,14 @@ class Engine:
|
|||
"ALTER TABLE approvals ADD COLUMN human_control "
|
||||
"INTEGER NOT NULL DEFAULT 0 CHECK (human_control IN (0, 1))"
|
||||
)
|
||||
if "required_assurance" not in approval_columns:
|
||||
# v7, APPROVAL-WP-0007-T01. Legacy rows stay NULL: no floor was
|
||||
# declared at their issue, and inferring one from the entries'
|
||||
# recorded levels would manufacture a declaration nobody made.
|
||||
conn.execute(
|
||||
"ALTER TABLE approvals ADD COLUMN required_assurance TEXT CHECK "
|
||||
"(required_assurance IS NULL OR required_assurance IN ('aal1', 'aal2', 'aal3'))"
|
||||
)
|
||||
entry_columns = {
|
||||
row["name"] for row in conn.execute("PRAGMA table_info(entries)").fetchall()
|
||||
}
|
||||
|
|
@ -372,6 +426,7 @@ class Engine:
|
|||
pdp_digest: str | None = None,
|
||||
pdp_path: bool = False,
|
||||
human_control: bool = False,
|
||||
required_assurance: dict[str, Any] | None = None,
|
||||
approval_id: str | None = None,
|
||||
) -> Approval:
|
||||
if required_count < 1:
|
||||
|
|
@ -380,6 +435,7 @@ class Engine:
|
|||
raise Unprocessable("pdp_path must be a boolean")
|
||||
if not isinstance(human_control, bool):
|
||||
raise Unprocessable("human_control must be a boolean")
|
||||
floor = require_assurance_floor(required_assurance)
|
||||
canon = canonical_binding(binding)
|
||||
digest = binding_digest(canon)
|
||||
pdp = require_digest(pdp_digest)
|
||||
|
|
@ -408,8 +464,8 @@ class Engine:
|
|||
id, status, binding_json, binding_digest, pdp_digest,
|
||||
actor, principal, action, purpose, target_json,
|
||||
not_before, expires_at, required_count, superseded_by,
|
||||
pdp_path, human_control, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
pdp_path, human_control, required_assurance, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
aid,
|
||||
"requested",
|
||||
|
|
@ -427,6 +483,7 @@ class Engine:
|
|||
None,
|
||||
1 if pdp_path else 0,
|
||||
1 if human_control else 0,
|
||||
floor,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
|
|
@ -485,6 +542,7 @@ class Engine:
|
|||
pdp_digest=row["pdp_digest"],
|
||||
pdp_path=bool(row["pdp_path"]),
|
||||
human_control=bool(row["human_control"]),
|
||||
required_assurance=row["required_assurance"],
|
||||
actor=row["actor"],
|
||||
principal=row["principal"],
|
||||
action=row["action"],
|
||||
|
|
@ -550,6 +608,12 @@ class Engine:
|
|||
# This is the bind boundary. A service may draft a requested
|
||||
# approval, but cannot contribute evidence for human judgment.
|
||||
raise Forbidden("human_control requires a verified human approver")
|
||||
if not entry_meets_floor(assurance, row["required_assurance"]):
|
||||
# The level comes only from the verified token (api.py); a
|
||||
# weaker or missing level cannot count toward this object.
|
||||
raise Forbidden(
|
||||
f"required_assurance {row['required_assurance']} not met by the approver's verified assurance"
|
||||
)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO entries "
|
||||
|
|
@ -654,10 +718,13 @@ class Engine:
|
|||
conn.rollback()
|
||||
raise Conflict("supersession lost the compare-and-swap")
|
||||
existing = conn.execute(
|
||||
"SELECT id, human_control FROM approvals WHERE id=?", (successor_id,)
|
||||
"SELECT id, human_control, required_assurance FROM approvals WHERE id=?",
|
||||
(successor_id,),
|
||||
).fetchone()
|
||||
if existing is not None and existing["human_control"] != row["human_control"]:
|
||||
raise Conflict("successor must inherit the human_control declaration")
|
||||
if existing is not None and existing["required_assurance"] != row["required_assurance"]:
|
||||
raise Conflict("successor must inherit the required_assurance declaration")
|
||||
created_successor = False
|
||||
if existing is None:
|
||||
conn.execute(
|
||||
|
|
@ -665,8 +732,8 @@ class Engine:
|
|||
id, status, binding_json, binding_digest, pdp_digest,
|
||||
actor, principal, action, purpose, target_json,
|
||||
not_before, expires_at, required_count, superseded_by,
|
||||
pdp_path, human_control, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
pdp_path, human_control, required_assurance, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
successor_id,
|
||||
"requested",
|
||||
|
|
@ -684,6 +751,7 @@ class Engine:
|
|||
None,
|
||||
row["pdp_path"],
|
||||
row["human_control"],
|
||||
row["required_assurance"],
|
||||
now,
|
||||
now,
|
||||
),
|
||||
|
|
@ -808,6 +876,14 @@ class Engine:
|
|||
).fetchone()
|
||||
if counts["total"] < row["required_count"] or counts["humans"] != counts["total"]:
|
||||
raise Conflict("cannot consume without the required human evidence")
|
||||
if row["required_assurance"]:
|
||||
levels = conn.execute(
|
||||
"SELECT assurance FROM entries WHERE approval_id=?", (approval_id,)
|
||||
).fetchall()
|
||||
if len(levels) < row["required_count"] or not all(
|
||||
entry_meets_floor(e["assurance"], row["required_assurance"]) for e in levels
|
||||
):
|
||||
raise Conflict("cannot consume without entries meeting required_assurance")
|
||||
cur = conn.execute(
|
||||
"UPDATE approvals SET status='consumed', consumed_digest=?, "
|
||||
"consumed_decision_id=?, consumed_at=?, updated_at=? "
|
||||
|
|
@ -928,10 +1004,13 @@ class Engine:
|
|||
if approval_id:
|
||||
details["approval_id"] = approval_id
|
||||
row = conn.execute(
|
||||
"SELECT human_control FROM approvals WHERE id=?", (approval_id,)
|
||||
"SELECT human_control, required_assurance FROM approvals WHERE id=?",
|
||||
(approval_id,),
|
||||
).fetchone()
|
||||
if row is not None:
|
||||
details["human_control"] = bool(row["human_control"])
|
||||
if row["required_assurance"]:
|
||||
details["required_assurance"] = {"level": row["required_assurance"]}
|
||||
payload = {
|
||||
"schema_version": AUDIT_SCHEMA,
|
||||
"event_id": event_id,
|
||||
|
|
@ -977,6 +1056,11 @@ class Engine:
|
|||
# happens to be present.
|
||||
binding["pdp_path"] = obj.pdp_path
|
||||
binding["human_control"] = obj.human_control
|
||||
# APPROVAL-WP-0007-T01: stated only when a floor was declared. The
|
||||
# access fact reader rejects unknown members, so undeclared claims keep
|
||||
# their exact pre-v7 bytes; absence means no floor was declared.
|
||||
if obj.required_assurance:
|
||||
binding["required_assurance"] = {"level": obj.required_assurance}
|
||||
return {
|
||||
"schema_version": CLAIM_SCHEMA,
|
||||
"kind": "approval-claim",
|
||||
|
|
@ -1013,6 +1097,10 @@ class Engine:
|
|||
return "approved", False, False, "not_yet_valid"
|
||||
if obj.human_control and any(e["principal_type"] != "human" for e in obj.entries):
|
||||
return "approved", False, False, "human_control_unsatisfied"
|
||||
if obj.required_assurance and not all(
|
||||
entry_meets_floor(e["assurance"], obj.required_assurance) for e in obj.entries
|
||||
):
|
||||
return "approved", False, False, "assurance_unsatisfied"
|
||||
if len({e["subject_id"] for e in obj.entries}) < obj.required_count:
|
||||
return "approved", False, False, "insufficient_approvers"
|
||||
return "valid", True, False, "ok"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue