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
152 lines
4.7 KiB
Python
152 lines
4.7 KiB
Python
"""Local fixtures used by early user-engine tests and examples."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Iterable, Mapping
|
|
|
|
from user_engine.domain import (
|
|
Actor,
|
|
Application,
|
|
ApplicationBinding,
|
|
AttributeDefinition,
|
|
AuthorizationDecision,
|
|
AuthorizationEffect,
|
|
AuthorizationRequest,
|
|
Catalog,
|
|
CatalogLifecycle,
|
|
Mutability,
|
|
PrincipalType,
|
|
ProfileScope,
|
|
ProjectionType,
|
|
Sensitivity,
|
|
Visibility,
|
|
)
|
|
from user_engine.ports import AuthorizationCheckPort, IdentityClaimsAdapter
|
|
|
|
|
|
class FixtureIdentityClaimsAdapter:
|
|
"""Normalize dictionary fixtures that already passed token verification."""
|
|
|
|
def normalize(self, claims: Mapping[str, object]) -> Actor:
|
|
scopes = claims.get("scope", ())
|
|
if isinstance(scopes, str):
|
|
scopes = tuple(part for part in scopes.split(" ") if part)
|
|
return Actor(
|
|
issuer=str(claims["iss"]),
|
|
subject=str(claims["sub"]),
|
|
tenant=str(claims["tenant"]),
|
|
principal_type=PrincipalType(str(claims["principal_type"])),
|
|
audience=tuple(_as_tuple(claims.get("aud", ()))),
|
|
roles=tuple(_as_tuple(claims.get("roles", ()))),
|
|
tenant_roles=tuple(_as_tuple(claims.get("tenant_roles", ()))),
|
|
groups=tuple(_as_tuple(claims.get("groups", ()))),
|
|
scopes=tuple(_as_tuple(scopes)),
|
|
assurance=dict(claims.get("assurance", {})),
|
|
authorized_party=_optional_str(claims.get("azp") or claims.get("client_id")),
|
|
preferred_username=_optional_str(claims.get("preferred_username")),
|
|
claims=dict(claims),
|
|
agent=dict(claims.get("agent", {})),
|
|
)
|
|
|
|
def identity_key(self, actor: Actor) -> tuple[str, str]:
|
|
return actor.identity_key
|
|
|
|
|
|
class StaticAuthorizationCheckPort:
|
|
"""Deterministic authorization adapter for tests and local examples."""
|
|
|
|
def __init__(self, effect: AuthorizationEffect = AuthorizationEffect.ALLOW):
|
|
self.effect = effect
|
|
self.requests: list[AuthorizationRequest] = []
|
|
|
|
def check(self, request: AuthorizationRequest) -> AuthorizationDecision:
|
|
self.requests.append(request)
|
|
return AuthorizationDecision.for_standalone(self.effect, reason="fixture")
|
|
|
|
def batch_check(
|
|
self, requests: Iterable[AuthorizationRequest]
|
|
) -> tuple[AuthorizationDecision, ...]:
|
|
return tuple(self.check(request) for request in requests)
|
|
|
|
|
|
def human_actor_claims(
|
|
*,
|
|
issuer: str = "https://issuer.example.test",
|
|
subject: str = "user-123",
|
|
tenant: str = "tenant:coulomb",
|
|
) -> dict[str, object]:
|
|
return {
|
|
"iss": issuer,
|
|
"sub": subject,
|
|
"aud": ["user-engine"],
|
|
"tenant": tenant,
|
|
"principal_type": "human",
|
|
"groups": ["tenant:coulomb:users"],
|
|
"roles": ["user"],
|
|
"scope": "openid profile email",
|
|
"preferred_username": "sample.user",
|
|
"email": "sample.user@example.test",
|
|
"assurance": {
|
|
"level": "aal2",
|
|
"methods": ["pwd", "otp"],
|
|
"mfa": True,
|
|
"source": "fixture",
|
|
},
|
|
}
|
|
|
|
|
|
def sample_application() -> Application:
|
|
return Application(
|
|
application_id="app.demo",
|
|
display_name="Demo Application",
|
|
owner="team:demo",
|
|
allowed_profile_scopes=(ProfileScope.GLOBAL, ProfileScope.APPLICATION),
|
|
allowed_projection_types=(ProjectionType.APPLICATION_RUNTIME,),
|
|
)
|
|
|
|
|
|
def sample_application_binding() -> ApplicationBinding:
|
|
return ApplicationBinding(
|
|
application_id="app.demo",
|
|
oidc_client_id="demo-client",
|
|
protected_system_id="user-engine.demo",
|
|
catalog_namespaces=("demo",),
|
|
event_source="user-engine.demo",
|
|
deployment_ref="local",
|
|
)
|
|
|
|
|
|
def sample_catalog() -> Catalog:
|
|
return Catalog(
|
|
catalog_id="demo-profile",
|
|
namespace="demo",
|
|
version="0.1.0",
|
|
owning_application_id="app.demo",
|
|
lifecycle=CatalogLifecycle.ACTIVE,
|
|
attributes=(
|
|
AttributeDefinition(
|
|
key="demo.display_density",
|
|
value_type="string",
|
|
scope=ProfileScope.APPLICATION,
|
|
sensitivity=Sensitivity.INTERNAL,
|
|
visibility=(Visibility.USER, Visibility.APPLICATION),
|
|
mutability=(Mutability.USER,),
|
|
default="comfortable",
|
|
validation={"enum": ["compact", "comfortable"]},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def _as_tuple(value: object) -> tuple[str, ...]:
|
|
if value is None:
|
|
return ()
|
|
if isinstance(value, str):
|
|
return (value,)
|
|
return tuple(str(item) for item in value)
|
|
|
|
|
|
def _optional_str(value: object) -> str | None:
|
|
if value is None:
|
|
return None
|
|
return str(value)
|