Start user-engine implementation scaffold

This commit is contained in:
tegwick 2026-05-22 20:55:27 +02:00
parent e618b4e286
commit 58d9de26d3
14 changed files with 763 additions and 7 deletions

View file

@ -0,0 +1,53 @@
"""Domain schemas for user-engine."""
from user_engine.domain.models import (
Account,
AccountStatus,
Actor,
Application,
ApplicationBinding,
AttributeDefinition,
AuditRecord,
AuthorizationDecision,
AuthorizationEffect,
AuthorizationRequest,
Catalog,
CatalogLifecycle,
ExternalIdentity,
Membership,
Mutability,
OutboxEvent,
PrincipalType,
ProfileScope,
ProfileValue,
ProjectionType,
Sensitivity,
User,
Visibility,
)
__all__ = [
"Account",
"AccountStatus",
"Actor",
"Application",
"ApplicationBinding",
"AttributeDefinition",
"AuditRecord",
"AuthorizationDecision",
"AuthorizationEffect",
"AuthorizationRequest",
"Catalog",
"CatalogLifecycle",
"ExternalIdentity",
"Membership",
"Mutability",
"OutboxEvent",
"PrincipalType",
"ProfileScope",
"ProfileValue",
"ProjectionType",
"Sensitivity",
"User",
"Visibility",
]

View file

@ -0,0 +1,276 @@
"""Core user-engine domain schemas.
These dataclasses are deliberately persistence- and transport-neutral. API
handlers, databases, and platform adapters should translate into and out of
these shapes instead of putting domain rules in infrastructure code.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import StrEnum
from typing import Any, Mapping
from uuid import uuid4
def new_id(prefix: str) -> str:
"""Return an opaque local identifier with a readable type prefix."""
return f"{prefix}_{uuid4().hex}"
def utc_now() -> datetime:
return datetime.now(timezone.utc)
class PrincipalType(StrEnum):
HUMAN = "human"
SERVICE = "service"
AGENT = "agent"
class AccountStatus(StrEnum):
INVITED = "invited"
ACTIVE = "active"
DISABLED = "disabled"
SUSPENDED = "suspended"
DELETION_PENDING = "deletion_pending"
DELETED = "deleted"
class ManagementMode(StrEnum):
LOCAL = "local"
EXTERNALLY_PROVISIONED = "externally_provisioned"
FEDERATED = "federated"
SERVICE_MANAGED = "service_managed"
class ProfileScope(StrEnum):
GLOBAL = "global"
TENANT = "tenant"
APPLICATION = "application"
MEMBERSHIP = "membership"
class ProjectionType(StrEnum):
SELF_SERVICE = "self_service"
ADMIN = "admin"
APPLICATION_RUNTIME = "application_runtime"
AUDIT = "audit"
AGENT_CONTEXT = "agent_context"
CLAIMS_ENRICHMENT = "claims_enrichment"
class Sensitivity(StrEnum):
PUBLIC = "public"
INTERNAL = "internal"
PERSONAL = "personal"
SENSITIVE = "sensitive"
SECRET = "secret"
class Visibility(StrEnum):
USER = "user"
ADMIN = "admin"
APPLICATION = "application"
SYSTEM = "system"
class Mutability(StrEnum):
USER = "user"
ADMIN = "admin"
APPLICATION = "application"
SYSTEM = "system"
READ_ONLY = "read_only"
class CatalogLifecycle(StrEnum):
DRAFT = "draft"
ACTIVE = "active"
DEPRECATED = "deprecated"
RETIRED = "retired"
class AuthorizationEffect(StrEnum):
ALLOW = "allow"
DENY = "deny"
REDACT = "redact"
AUDIT_ONLY = "audit_only"
NOT_APPLICABLE = "not_applicable"
@dataclass(frozen=True)
class Actor:
issuer: str
subject: str
tenant: str
principal_type: PrincipalType
audience: tuple[str, ...]
roles: tuple[str, ...] = ()
groups: tuple[str, ...] = ()
scopes: tuple[str, ...] = ()
assurance: Mapping[str, Any] = field(default_factory=dict)
authorized_party: str | None = None
preferred_username: str | None = None
claims: Mapping[str, Any] = field(default_factory=dict)
agent: Mapping[str, Any] = field(default_factory=dict)
@property
def identity_key(self) -> tuple[str, str]:
return (self.issuer, self.subject)
@dataclass(frozen=True)
class User:
user_id: str = field(default_factory=lambda: new_id("usr"))
display_name: str | None = None
primary_email: str | None = None
created_at: datetime = field(default_factory=utc_now)
@dataclass(frozen=True)
class Account:
account_id: str
user_id: str
status: AccountStatus = AccountStatus.INVITED
management_mode: ManagementMode = ManagementMode.LOCAL
created_at: datetime = field(default_factory=utc_now)
@dataclass(frozen=True)
class ExternalIdentity:
identity_id: str
user_id: str
issuer: str
subject: str
provider: str | None = None
linked_at: datetime = field(default_factory=utc_now)
@property
def identity_key(self) -> tuple[str, str]:
return (self.issuer, self.subject)
@dataclass(frozen=True)
class Application:
application_id: str
display_name: str
owner: str
allowed_profile_scopes: tuple[ProfileScope, ...] = (ProfileScope.GLOBAL,)
allowed_projection_types: tuple[ProjectionType, ...] = (
ProjectionType.APPLICATION_RUNTIME,
)
@dataclass(frozen=True)
class ApplicationBinding:
application_id: str
oidc_client_id: str | None = None
protected_system_id: str | None = None
catalog_namespaces: tuple[str, ...] = ()
event_source: str | None = None
deployment_ref: str | None = None
@dataclass(frozen=True)
class AttributeDefinition:
key: str
value_type: str
scope: ProfileScope
sensitivity: Sensitivity
visibility: tuple[Visibility, ...]
mutability: tuple[Mutability, ...]
required: bool = False
default: Any = None
validation: Mapping[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class Catalog:
catalog_id: str
namespace: str
version: str
owning_application_id: str
lifecycle: CatalogLifecycle = CatalogLifecycle.DRAFT
attributes: tuple[AttributeDefinition, ...] = ()
def attribute_keys(self) -> set[str]:
return {attribute.key for attribute in self.attributes}
@dataclass(frozen=True)
class ProfileValue:
user_id: str
attribute_key: str
value: Any
scope: ProfileScope = ProfileScope.GLOBAL
scope_id: str | None = None
source: str = "user-engine"
updated_at: datetime = field(default_factory=utc_now)
@dataclass(frozen=True)
class Membership:
membership_id: str
user_id: str
tenant: str
scope_type: str
scope_id: str
kind: str
source_system: str = "user-engine"
owning_system: str = "user-engine"
freshness_version: str | None = None
created_at: datetime = field(default_factory=utc_now)
@dataclass(frozen=True)
class AuthorizationRequest:
actor: Actor
resource_type: str
resource_id: str
action: str
tenant: str
correlation_id: str
application_id: str | None = None
target_user_id: str | None = None
context: Mapping[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class AuthorizationDecision:
effect: AuthorizationEffect
decision_id: str = field(default_factory=lambda: new_id("dec"))
reason: str | None = None
obligations: tuple[str, ...] = ()
@property
def allowed(self) -> bool:
return self.effect in {
AuthorizationEffect.ALLOW,
AuthorizationEffect.AUDIT_ONLY,
}
@dataclass(frozen=True)
class AuditRecord:
audit_id: str
actor: Actor
action: str
subject: str
tenant: str
correlation_id: str
decision_id: str | None = None
application_id: str | None = None
summary: str | None = None
recorded_at: datetime = field(default_factory=utc_now)
@dataclass(frozen=True)
class OutboxEvent:
event_id: str
event_type: str
aggregate_id: str
payload: Mapping[str, Any]
tenant: str
correlation_id: str
occurred_at: datetime = field(default_factory=utc_now)