Implement user-engine portal foundation
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

This commit is contained in:
tegwick 2026-07-27 22:45:42 +02:00
parent 60446e8b40
commit 0980d1fd41
12 changed files with 676 additions and 6 deletions

View file

@ -5,9 +5,11 @@ from user_engine.adapters.local import (
LocalAuthorizationCheckPort,
)
from user_engine.adapters.postgres import PostgresUserEngineStore
from user_engine.adapters.claims import VerifiedIdentityClaimsAdapter
__all__ = [
"InMemoryUserEngineStore",
"LocalAuthorizationCheckPort",
"PostgresUserEngineStore",
"VerifiedIdentityClaimsAdapter",
]

View file

@ -0,0 +1,57 @@
"""Normalization for claims already cryptographically verified at the edge."""
from __future__ import annotations
from typing import Mapping
from user_engine.domain import Actor, PrincipalType
from user_engine.errors import ValidationError
class VerifiedIdentityClaimsAdapter:
def __init__(self, *, expected_issuer: str, expected_audience: str) -> None:
self.expected_issuer = expected_issuer.rstrip("/")
self.expected_audience = expected_audience
def normalize(self, claims: Mapping[str, object]) -> Actor:
for required in ("iss", "sub", "tenant", "principal_type"):
if not claims.get(required):
raise ValidationError(f"{required} claim is required")
if str(claims["iss"]).rstrip("/") != self.expected_issuer:
raise ValidationError("unexpected token issuer")
audience = _strings(claims.get("aud", ()))
if self.expected_audience not in audience:
raise ValidationError("required token audience is missing")
scopes = claims.get("scope", ())
if isinstance(scopes, str):
scopes = tuple(item for item in scopes.split() if item)
return Actor(
issuer=str(claims["iss"]),
subject=str(claims["sub"]),
tenant=str(claims["tenant"]),
principal_type=PrincipalType(str(claims["principal_type"])),
audience=audience,
roles=_strings(claims.get("roles", ())),
groups=_strings(claims.get("groups", ())),
scopes=_strings(scopes),
assurance=dict(claims.get("assurance", {})),
authorized_party=_optional(claims.get("azp") or claims.get("client_id")),
preferred_username=_optional(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
def _strings(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(value: object) -> str | None:
return None if value is None else str(value)