Start user-engine implementation scaffold
This commit is contained in:
parent
e618b4e286
commit
58d9de26d3
14 changed files with 763 additions and 7 deletions
53
src/user_engine/domain/__init__.py
Normal file
53
src/user_engine/domain/__init__.py
Normal 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",
|
||||
]
|
||||
276
src/user_engine/domain/models.py
Normal file
276
src/user_engine/domain/models.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue