Implement USER-WP-0024 security layer conformance
Declare Engine/PIP machine-readably, publish a total fail-closed PEP stance map, stop minting local decision ids on engine-unavailable DENY, bind allows to a 30s request lifetime, confine the local authorization double, classify evidence and emit a denial/revocation heartbeat, and prove access-control facts remain claims. Assistant: grok Assistant-Session: 01a04cea-f0d6-7ab3-9ffd-881eb6bea6cb
This commit is contained in:
parent
c7b6148a70
commit
4349758608
22 changed files with 1242 additions and 89 deletions
|
|
@ -19,6 +19,7 @@ from user_engine.domain import (
|
|||
ApplicationBinding,
|
||||
AttributeDefinition,
|
||||
AuditRecord,
|
||||
AuthorizationDecision,
|
||||
AuthorizationRequest,
|
||||
CanonEntityReference,
|
||||
CanonRelationshipReference,
|
||||
|
|
@ -68,6 +69,10 @@ from user_engine.errors import (
|
|||
NotFoundError,
|
||||
ValidationError,
|
||||
)
|
||||
from user_engine.evidence import (
|
||||
HEARTBEAT_EVENT_TYPE,
|
||||
load_bearing_counts,
|
||||
)
|
||||
from user_engine.ports import (
|
||||
AuthorizationCheckPort,
|
||||
FactorVerificationAdapter,
|
||||
|
|
@ -2894,6 +2899,37 @@ class UserEngineService:
|
|||
issues=issues,
|
||||
)
|
||||
|
||||
def record_evidence_heartbeat(
|
||||
self, *, correlation_id: str | None = None
|
||||
) -> OutboxEvent:
|
||||
"""Emit a positive evidence claim that can itself go missing.
|
||||
|
||||
Counts load-bearing denials and revocations already on the local
|
||||
trail. Does not claim the trail is complete.
|
||||
"""
|
||||
correlation_id = correlation_id or new_id("corr")
|
||||
counts = load_bearing_counts(
|
||||
self.store.audit_log(), self.store.pending_outbox()
|
||||
)
|
||||
event = OutboxEvent(
|
||||
event_id=new_id("evt"),
|
||||
event_type=HEARTBEAT_EVENT_TYPE,
|
||||
aggregate_id="user-engine:evidence",
|
||||
payload={
|
||||
"completeness_claimed": False,
|
||||
"form": "heartbeat",
|
||||
"bound": (
|
||||
"the archive proves the records it holds were not "
|
||||
"altered or truncated after arrival"
|
||||
),
|
||||
"load_bearing": counts,
|
||||
},
|
||||
tenant=PLATFORM_TENANT,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
self.store.append_outbox(event)
|
||||
return event
|
||||
|
||||
def structured_log_context(
|
||||
self,
|
||||
*,
|
||||
|
|
@ -4242,22 +4278,75 @@ class UserEngineService:
|
|||
),
|
||||
)
|
||||
decision = self.authorization.check(request)
|
||||
if not decision.allowed:
|
||||
self.store.append_audit(
|
||||
AuditRecord(
|
||||
audit_id=new_id("aud"),
|
||||
actor=actor,
|
||||
if decision.allowed:
|
||||
if decision.lifetime is None:
|
||||
self._record_authorization_denial(
|
||||
actor,
|
||||
action=action,
|
||||
subject=resource_id,
|
||||
resource_id=resource_id,
|
||||
tenant=tenant,
|
||||
correlation_id=correlation_id,
|
||||
decision_id=decision.decision_id,
|
||||
application_id=application_id,
|
||||
summary="authorization denied",
|
||||
decision=AuthorizationDecision(
|
||||
effect=decision.effect,
|
||||
reason="authorization allow has no lifetime",
|
||||
),
|
||||
summary="authorization denied (allow has no lifetime)",
|
||||
)
|
||||
raise AuthorizationDenied("authorization allow has no lifetime")
|
||||
if decision.expired():
|
||||
self._record_authorization_denial(
|
||||
actor,
|
||||
action=action,
|
||||
resource_id=resource_id,
|
||||
tenant=tenant,
|
||||
correlation_id=correlation_id,
|
||||
application_id=application_id,
|
||||
decision=decision,
|
||||
summary="authorization denied (allow expired)",
|
||||
)
|
||||
raise AuthorizationDenied("authorization allow expired")
|
||||
return decision
|
||||
summary = "authorization denied"
|
||||
if decision.stance_applied:
|
||||
summary = f"authorization denied (stance {decision.stance_applied})"
|
||||
self._record_authorization_denial(
|
||||
actor,
|
||||
action=action,
|
||||
resource_id=resource_id,
|
||||
tenant=tenant,
|
||||
correlation_id=correlation_id,
|
||||
application_id=application_id,
|
||||
decision=decision,
|
||||
summary=summary,
|
||||
)
|
||||
raise AuthorizationDenied(decision.reason or "authorization denied")
|
||||
|
||||
def _record_authorization_denial(
|
||||
self,
|
||||
actor: Actor,
|
||||
*,
|
||||
action: str,
|
||||
resource_id: str,
|
||||
tenant: str,
|
||||
correlation_id: str,
|
||||
application_id: str | None,
|
||||
decision: AuthorizationDecision,
|
||||
summary: str,
|
||||
) -> None:
|
||||
self.store.append_audit(
|
||||
AuditRecord(
|
||||
audit_id=new_id("aud"),
|
||||
actor=actor,
|
||||
action=action,
|
||||
subject=resource_id,
|
||||
tenant=tenant,
|
||||
correlation_id=correlation_id,
|
||||
decision_id=decision.decision_id,
|
||||
application_id=application_id,
|
||||
summary=summary,
|
||||
)
|
||||
raise AuthorizationDenied(decision.reason or "authorization denied")
|
||||
return decision
|
||||
)
|
||||
|
||||
def _record_mutation(
|
||||
self,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue