Implement isolated user-engine MVP

This commit is contained in:
tegwick 2026-05-22 21:20:19 +02:00
parent b77324d7b1
commit 5203d9f45f
13 changed files with 1196 additions and 13 deletions

View file

@ -0,0 +1,11 @@
"""Local adapters for standalone user-engine setups."""
from user_engine.adapters.local import (
InMemoryUserEngineStore,
LocalAuthorizationCheckPort,
)
__all__ = [
"InMemoryUserEngineStore",
"LocalAuthorizationCheckPort",
]

View file

@ -0,0 +1,123 @@
"""Standalone adapters for isolated user-engine deployments and tests."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Iterable
from user_engine.domain import (
Account,
Application,
ApplicationBinding,
AuditRecord,
AuthorizationDecision,
AuthorizationEffect,
AuthorizationRequest,
Catalog,
ExternalIdentity,
OutboxEvent,
ProfileScope,
ProfileValue,
User,
)
SCHEMA_VERSION = "0001_initial"
@dataclass
class InMemoryUserEngineStore:
"""Small local persistence adapter with explicit schema state.
The store is intentionally simple and deterministic. It gives the MVP a
concrete persistence boundary without committing later implementation work
to a specific database driver.
"""
schema_version: str | None = None
users: dict[str, User] = field(default_factory=dict)
accounts: dict[str, Account] = field(default_factory=dict)
identities: dict[tuple[str, str], ExternalIdentity] = field(default_factory=dict)
applications: dict[str, Application] = field(default_factory=dict)
bindings: dict[str, ApplicationBinding] = field(default_factory=dict)
catalogs: dict[str, Catalog] = field(default_factory=dict)
profile_values: dict[
tuple[str, str, ProfileScope, str | None], ProfileValue
] = field(default_factory=dict)
audit_records: list[AuditRecord] = field(default_factory=list)
outbox_events: list[OutboxEvent] = field(default_factory=list)
def migrate(self) -> None:
"""Apply the standalone schema migration manifest."""
self.schema_version = SCHEMA_VERSION
@property
def ready(self) -> bool:
return self.schema_version == SCHEMA_VERSION
def save_user(self, user: User) -> None:
self.users[user.user_id] = user
def save_account(self, account: Account) -> None:
self.accounts[account.user_id] = account
def save_identity(self, identity: ExternalIdentity) -> None:
self.identities[identity.identity_key] = identity
def save_application(self, application: Application) -> None:
self.applications[application.application_id] = application
def save_binding(self, binding: ApplicationBinding) -> None:
self.bindings[binding.application_id] = binding
def save_catalog(self, catalog: Catalog) -> None:
self.catalogs[catalog.catalog_id] = catalog
def save_profile_value(self, value: ProfileValue) -> None:
self.profile_values[
(value.user_id, value.attribute_key, value.scope, value.scope_id)
] = value
def find_identity(self, issuer: str, subject: str) -> ExternalIdentity | None:
return self.identities.get((issuer, subject))
def user_account(self, user_id: str) -> Account | None:
return self.accounts.get(user_id)
def values_for_user(self, user_id: str) -> tuple[ProfileValue, ...]:
return tuple(
value for value in self.profile_values.values() if value.user_id == user_id
)
def append_audit(self, record: AuditRecord) -> None:
self.audit_records.append(record)
def append_outbox(self, event: OutboxEvent) -> None:
self.outbox_events.append(event)
class LocalAuthorizationCheckPort:
"""Deterministic local authorization adapter.
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.
"""
def __init__(
self,
*,
default_effect: AuthorizationEffect = AuthorizationEffect.ALLOW,
action_effects: dict[str, AuthorizationEffect] | None = None,
) -> None:
self.default_effect = default_effect
self.action_effects = action_effects or {}
self.requests: list[AuthorizationRequest] = []
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")
def batch_check(
self, requests: Iterable[AuthorizationRequest]
) -> tuple[AuthorizationDecision, ...]:
return tuple(self.check(request) for request in requests)