Retain verified tenant provenance and reconcile production blockers

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1046e-fdbf-7b93-a267-5f72b9b24526
This commit is contained in:
tegwick 2026-10-04 11:26:32 +02:00
parent 931e0b3249
commit c9ad5c2dd7
12 changed files with 274 additions and 25 deletions

View file

@ -147,6 +147,7 @@ class App:
assurance=json.dumps(identity.assurance, sort_keys=True),
evidence_ref=identity.evidence_ref,
principal_type=identity.principal_type,
tenant_source=identity.tenant_source,
)
return 200, obj.as_dict()
if rest == ["revoke"] and method == "POST":

View file

@ -28,6 +28,7 @@ class Identity:
scopes: frozenset[str]
assurance: dict[str, Any]
evidence_ref: str
tenant_source: str | None = None
def require(self, scope: str) -> "Identity":
if scope not in self.scopes:
@ -76,6 +77,12 @@ def identity_from_claims(claims: Mapping[str, Any], token: str) -> Identity:
raise Unauthenticated("token identity claims are invalid")
if not tenant or not isinstance(assurance, dict):
raise Unauthenticated("token tenant or assurance claim is invalid")
tenant_source = claims.get("tenant_source")
if "tenant_source" in claims and (
not isinstance(tenant_source, str)
or tenant_source not in {"directory", "registration", "default"}
):
raise Unauthenticated("token tenant_source claim is invalid")
audiences = _texts(claims.get("aud"), "aud")
roles = frozenset(_texts(claims.get("roles"), "roles", allow_empty=True))
scopes = frozenset(_texts(claims.get("scope", claims.get("scp")), "scope"))
@ -90,6 +97,7 @@ def identity_from_claims(claims: Mapping[str, Any], token: str) -> Identity:
scopes=scopes,
assurance=dict(assurance),
evidence_ref=f"jwt-sha256:{fingerprint}",
tenant_source=tenant_source,
)

View file

@ -42,7 +42,7 @@ 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 = 5
LATEST_SCHEMA_VERSION = 6
SCHEMA = """
CREATE TABLE IF NOT EXISTS approvals (
@ -75,6 +75,7 @@ CREATE TABLE IF NOT EXISTS entries (
assurance TEXT,
evidence_ref TEXT,
principal_type TEXT,
tenant_source TEXT,
PRIMARY KEY (approval_id, subject_id)
);
CREATE TABLE IF NOT EXISTS outbox (
@ -258,6 +259,9 @@ class Engine:
# 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")
if "tenant_source" not in entry_columns:
# v6: historical entries have no verified tenant provenance.
conn.execute("ALTER TABLE entries ADD COLUMN tenant_source TEXT")
outbox_columns = {
row["name"] for row in conn.execute("PRAGMA table_info(outbox)").fetchall()
}
@ -456,10 +460,11 @@ class Engine:
"assurance": e["assurance"],
"evidence_ref": e["evidence_ref"],
"principal_type": e["principal_type"],
"tenant_source": e["tenant_source"],
}
for e in self._conn()
.execute(
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type "
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type, tenant_source "
"FROM entries WHERE approval_id=? ORDER BY approved_at",
(row["id"],),
)
@ -520,9 +525,15 @@ class Engine:
assurance: str | None = None,
evidence_ref: str | None = None,
principal_type: str | None = None,
tenant_source: str | None = None,
) -> Approval:
if not subject_id:
raise Unprocessable("subject_id is required")
if tenant_source is not None and (
not isinstance(tenant_source, str)
or tenant_source not in {"directory", "registration", "default"}
):
raise Unprocessable("tenant_source is invalid")
conn = self._conn()
now = iso(self.now())
try:
@ -541,9 +552,9 @@ class Engine:
try:
conn.execute(
"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),
"(approval_id, subject_id, approved_at, assurance, evidence_ref, principal_type, tenant_source) "
"VALUES (?,?,?,?,?,?,?)",
(approval_id, subject_id, now, assurance, evidence_ref, principal_type, tenant_source),
)
except sqlite3.IntegrityError as exc:
conn.rollback()
@ -852,7 +863,7 @@ class Engine:
a future schema that relaxes it.
"""
rows = conn.execute(
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type "
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type, tenant_source "
"FROM entries WHERE approval_id=? ORDER BY approved_at, subject_id",
(approval_id,),
).fetchall()
@ -866,6 +877,7 @@ class Engine:
**({"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 {}),
"tenant_source": r["tenant_source"],
}
for r in seen.values()
]