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
|
|
@ -11,6 +11,13 @@ from user_engine.domain import (
|
|||
AuthorizationDecision,
|
||||
AuthorizationEffect,
|
||||
AuthorizationRequest,
|
||||
utc_now,
|
||||
)
|
||||
from user_engine.pep_stance import (
|
||||
ALLOW_BINDING,
|
||||
ALLOW_LIFETIME,
|
||||
DEFAULT_STANCE_SCOPE,
|
||||
UNREACHABLE_STANCE,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -86,16 +93,28 @@ class FlexAuthHTTPAdapter:
|
|||
for item in body.get("obligations", ())
|
||||
if isinstance(item, dict) and item.get("type")
|
||||
)
|
||||
lifetime = ALLOW_LIFETIME if effect in {
|
||||
AuthorizationEffect.ALLOW,
|
||||
AuthorizationEffect.AUDIT_ONLY,
|
||||
} else None
|
||||
issued_at = utc_now() if lifetime is not None else None
|
||||
return AuthorizationDecision(
|
||||
effect=effect,
|
||||
decision_id=decision_id,
|
||||
reason=reason,
|
||||
obligations=obligations,
|
||||
binding=ALLOW_BINDING,
|
||||
lifetime=lifetime,
|
||||
issued_at=issued_at,
|
||||
)
|
||||
except (HTTPError, URLError, TimeoutError, OSError, ValueError, KeyError, TypeError):
|
||||
return AuthorizationDecision(
|
||||
effect=AuthorizationEffect.DENY,
|
||||
decision_id=None,
|
||||
reason="authorization service unavailable",
|
||||
stance_applied=UNREACHABLE_STANCE,
|
||||
stance_scope=DEFAULT_STANCE_SCOPE,
|
||||
binding=ALLOW_BINDING,
|
||||
)
|
||||
|
||||
def batch_check(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Iterable, Iterator, Mapping, cast
|
||||
|
|
@ -426,18 +427,25 @@ class InMemoryUserEngineStore:
|
|||
|
||||
|
||||
class LocalAuthorizationCheckPort:
|
||||
"""Deterministic local authorization adapter.
|
||||
"""Test and standalone authorization double. Not a production PDP.
|
||||
|
||||
Rules are action-specific. The default is allow so isolated tests and local
|
||||
demos can focus on user-engine behavior while still exercising the port.
|
||||
Construction fails when a production flex-auth caller token is configured,
|
||||
so ``create_application()`` cannot assemble this adapter by mistake.
|
||||
"""
|
||||
|
||||
standalone_double = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
default_effect: AuthorizationEffect = AuthorizationEffect.ALLOW,
|
||||
action_effects: dict[str, AuthorizationEffect] | None = None,
|
||||
) -> None:
|
||||
if os.environ.get("USER_ENGINE_FLEX_AUTH_TOKEN_FILE"):
|
||||
raise RuntimeError(
|
||||
"LocalAuthorizationCheckPort cannot be constructed in a "
|
||||
"production runtime (USER_ENGINE_FLEX_AUTH_TOKEN_FILE is set)"
|
||||
)
|
||||
self.default_effect = default_effect
|
||||
self.action_effects = action_effects or {}
|
||||
self.requests: list[AuthorizationRequest] = []
|
||||
|
|
@ -445,7 +453,7 @@ class LocalAuthorizationCheckPort:
|
|||
def check(self, request: AuthorizationRequest) -> AuthorizationDecision:
|
||||
self.requests.append(request)
|
||||
effect = self.action_effects.get(request.action, self.default_effect)
|
||||
return AuthorizationDecision(effect=effect, reason="local")
|
||||
return AuthorizationDecision.for_standalone(effect, reason="local")
|
||||
|
||||
def batch_check(
|
||||
self, requests: Iterable[AuthorizationRequest]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue