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:
parent
931e0b3249
commit
c9ad5c2dd7
12 changed files with 274 additions and 25 deletions
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue