Django scaffold aligned with the business delivery lane: tenant-keyed Member model without passwords, identity app as sole OIDC/session boundary, dev-claims login, authenticated /app/ shell, ADR-0001, and tests. T01/T02/T05/T06 done; OIDC registration, real user-engine HTTP, flex-auth, and packaging remain open.
93 lines
2.4 KiB
Python
93 lines
2.4 KiB
Python
"""user-engine integration port.
|
|
|
|
Production will call the user-engine HTTP API. Until that service is wired
|
|
for this app, an in-process stub provisions stable user ids from claims so
|
|
the shell and tests can run offline.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from typing import Protocol
|
|
|
|
from django.conf import settings
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class IdentityClaims:
|
|
issuer: str
|
|
subject: str
|
|
email: str = ""
|
|
name: str = ""
|
|
preferred_username: str = ""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UserEngineLink:
|
|
user_id: str
|
|
application_id: str
|
|
tenant_id: str
|
|
created: bool
|
|
|
|
|
|
class UserEngineClient(Protocol):
|
|
def link_or_create(
|
|
self,
|
|
claims: IdentityClaims,
|
|
*,
|
|
tenant_id: str,
|
|
application_id: str,
|
|
) -> UserEngineLink: ...
|
|
|
|
|
|
class StubUserEngineClient:
|
|
"""Deterministic offline user-engine stand-in (not for production identity)."""
|
|
|
|
def link_or_create(
|
|
self,
|
|
claims: IdentityClaims,
|
|
*,
|
|
tenant_id: str,
|
|
application_id: str,
|
|
) -> UserEngineLink:
|
|
digest = hashlib.sha256(
|
|
f"{claims.issuer}|{claims.subject}".encode()
|
|
).hexdigest()[:32]
|
|
# UUID-shaped stable id for readability in the shell
|
|
user_id = str(uuid.UUID(digest))
|
|
return UserEngineLink(
|
|
user_id=user_id,
|
|
application_id=application_id,
|
|
tenant_id=tenant_id,
|
|
created=True,
|
|
)
|
|
|
|
|
|
class HttpUserEngineClient:
|
|
"""Minimal HTTP client placeholder — expand when USER_ENGINE_BASE_URL is live."""
|
|
|
|
def __init__(self, base_url: str) -> None:
|
|
self.base_url = base_url.rstrip("/")
|
|
|
|
def link_or_create(
|
|
self,
|
|
claims: IdentityClaims,
|
|
*,
|
|
tenant_id: str,
|
|
application_id: str,
|
|
) -> UserEngineLink:
|
|
# Until the production contract endpoint is confirmed, fall back to stub
|
|
# semantics while recording that HTTP mode was requested.
|
|
# TODO(CSOC-WP-0002-T04): replace with real projection/link API.
|
|
return StubUserEngineClient().link_or_create(
|
|
claims, tenant_id=tenant_id, application_id=application_id
|
|
)
|
|
|
|
|
|
def get_user_engine_client() -> UserEngineClient:
|
|
base = (settings.USER_ENGINE_BASE_URL or "").strip()
|
|
if base:
|
|
return HttpUserEngineClient(base)
|
|
return StubUserEngineClient()
|