Record the verified principal type on approver entries (schema v4)
An entry stored subject_id, assurance and evidence_ref but nothing about what kind of principal bound the approval, and subject_id is a naming convention rather than a verified claim. /entries is not restricted by principal type — only /consume is — and the approval-engine-operator client holds approval:approve, so a service can supply approver evidence today. Whether it may is gate-house doctrine; that it is legible is ours. Add entries.principal_type, populate it from the verified token, surface it on the object and the audit evidence path (not the claim, which stays least-disclosure), and migrate v3 stores leaving legacy rows null rather than back-filling a claim nobody made. Also corrects two statements in the requirements issued to informed-decision: agent tokens are not barred from approval:approve, and an empty assurance object is accepted rather than refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyybaE7DUXrWYrhbnESCTe Assistant: claude-code Assistant-Model: opus Assistant-Process: 1275879@bnt-lap001 Assistant-Session: eb464208-f821-41b2-bc5a-a6c33d92a8ad
This commit is contained in:
parent
8b8ada6c4a
commit
31da1af5e4
6 changed files with 160 additions and 20 deletions
|
|
@ -145,6 +145,7 @@ class App:
|
|||
identity.subject,
|
||||
assurance=json.dumps(identity.assurance, sort_keys=True),
|
||||
evidence_ref=identity.evidence_ref,
|
||||
principal_type=identity.principal_type,
|
||||
)
|
||||
return 200, obj.as_dict()
|
||||
if rest == ["revoke"] and method == "POST":
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ AUDIT_SCHEMA = "audit-core.event.v1alpha1"
|
|||
SOURCE = "approval-engine"
|
||||
SCOPE = "netkingdom-approvals"
|
||||
EVENT_CLASSES = ("issuance", "use", "supersession", "revocation", "heartbeat")
|
||||
LATEST_SCHEMA_VERSION = 3
|
||||
LATEST_SCHEMA_VERSION = 4
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS approvals (
|
||||
|
|
@ -66,6 +66,7 @@ CREATE TABLE IF NOT EXISTS entries (
|
|||
approved_at TEXT NOT NULL,
|
||||
assurance TEXT,
|
||||
evidence_ref TEXT,
|
||||
principal_type TEXT,
|
||||
PRIMARY KEY (approval_id, subject_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS outbox (
|
||||
|
|
@ -230,6 +231,15 @@ class Engine:
|
|||
conn.execute(
|
||||
"ALTER TABLE approvals ADD COLUMN pdp_path INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
entry_columns = {
|
||||
row["name"] for row in conn.execute("PRAGMA table_info(entries)").fetchall()
|
||||
}
|
||||
if "principal_type" not in entry_columns:
|
||||
# v4. Legacy entries stay NULL. An entry recorded before this
|
||||
# column existed carries no verified statement about what kind
|
||||
# of principal bound it, and defaulting it to 'human' would
|
||||
# manufacture approver evidence that was never presented.
|
||||
conn.execute("ALTER TABLE entries ADD COLUMN principal_type TEXT")
|
||||
outbox_columns = {
|
||||
row["name"] for row in conn.execute("PRAGMA table_info(outbox)").fetchall()
|
||||
}
|
||||
|
|
@ -423,10 +433,11 @@ class Engine:
|
|||
"approved_at": e["approved_at"],
|
||||
"assurance": e["assurance"],
|
||||
"evidence_ref": e["evidence_ref"],
|
||||
"principal_type": e["principal_type"],
|
||||
}
|
||||
for e in self._conn()
|
||||
.execute(
|
||||
"SELECT subject_id, approved_at, assurance, evidence_ref "
|
||||
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type "
|
||||
"FROM entries WHERE approval_id=? ORDER BY approved_at",
|
||||
(row["id"],),
|
||||
)
|
||||
|
|
@ -485,6 +496,7 @@ class Engine:
|
|||
*,
|
||||
assurance: str | None = None,
|
||||
evidence_ref: str | None = None,
|
||||
principal_type: str | None = None,
|
||||
) -> Approval:
|
||||
if not subject_id:
|
||||
raise Unprocessable("subject_id is required")
|
||||
|
|
@ -501,9 +513,10 @@ class Engine:
|
|||
raise Conflict(f"cannot add entries in status {row['status']}")
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO entries (approval_id, subject_id, approved_at, assurance, evidence_ref) "
|
||||
"VALUES (?,?,?,?,?)",
|
||||
(approval_id, subject_id, now, assurance, evidence_ref),
|
||||
"INSERT INTO entries "
|
||||
"(approval_id, subject_id, approved_at, assurance, evidence_ref, principal_type) "
|
||||
"VALUES (?,?,?,?,?,?)",
|
||||
(approval_id, subject_id, now, assurance, evidence_ref, principal_type),
|
||||
)
|
||||
except sqlite3.IntegrityError as exc:
|
||||
conn.rollback()
|
||||
|
|
@ -767,7 +780,7 @@ class Engine:
|
|||
a future schema that relaxes it.
|
||||
"""
|
||||
rows = conn.execute(
|
||||
"SELECT subject_id, approved_at, assurance, evidence_ref "
|
||||
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type "
|
||||
"FROM entries WHERE approval_id=? ORDER BY approved_at, subject_id",
|
||||
(approval_id,),
|
||||
).fetchall()
|
||||
|
|
@ -780,6 +793,7 @@ class Engine:
|
|||
"approved_at": r["approved_at"],
|
||||
**({"assurance": r["assurance"]} if r["assurance"] else {}),
|
||||
**({"evidence_ref": r["evidence_ref"]} if r["evidence_ref"] else {}),
|
||||
**({"principal_type": r["principal_type"]} if r["principal_type"] else {}),
|
||||
}
|
||||
for r in seen.values()
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue