Implement isolated user-engine MVP
This commit is contained in:
parent
b77324d7b1
commit
5203d9f45f
13 changed files with 1196 additions and 13 deletions
11
src/user_engine/adapters/__init__.py
Normal file
11
src/user_engine/adapters/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"""Local adapters for standalone user-engine setups."""
|
||||
|
||||
from user_engine.adapters.local import (
|
||||
InMemoryUserEngineStore,
|
||||
LocalAuthorizationCheckPort,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"InMemoryUserEngineStore",
|
||||
"LocalAuthorizationCheckPort",
|
||||
]
|
||||
123
src/user_engine/adapters/local.py
Normal file
123
src/user_engine/adapters/local.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue