Implement user-engine portal foundation
This commit is contained in:
parent
60446e8b40
commit
0980d1fd41
12 changed files with 676 additions and 6 deletions
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
57
src/user_engine/adapters/claims.py
Normal file
57
src/user_engine/adapters/claims.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue