Assembly Management Rework - Stage 3 Complete

This commit is contained in:
2025-11-02 21:15:56 -07:00
parent 9b5074ed75
commit fdd95bad23
6 changed files with 672 additions and 83 deletions

View File

@@ -0,0 +1,14 @@
# ======================================================
# Data\Engine\services\auth\__init__.py
# Description: Exposes shared authentication helpers for Engine REST services.
#
# API Endpoints (if applicable): None
# ======================================================
"""Authentication utilities for Borealis Engine services."""
from .context import RequestAuthContext, PermissionResult
from .dev_mode import DevModeEntry, DevModeManager
__all__ = ["RequestAuthContext", "PermissionResult", "DevModeEntry", "DevModeManager"]

View File

@@ -0,0 +1,199 @@
# ======================================================
# Data\Engine\services\auth\context.py
# Description: Provides request-scoped authentication helpers with Dev Mode state integration.
#
# API Endpoints (if applicable): None
# ======================================================
"""Shared authentication helpers for Engine REST services."""
from __future__ import annotations
import logging
import os
import uuid
from typing import Any, Dict, Mapping, Optional, Tuple
from flask import Request, request, session
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
from .dev_mode import DevModeManager
PermissionResult = Tuple[Dict[str, Any], int]
def _coerce_positive_int(value: Any, default: int) -> int:
try:
candidate = int(value)
if candidate > 0:
return candidate
except (TypeError, ValueError):
pass
return default
class RequestAuthContext:
"""Resolves the current operator and Dev Mode state for the active request."""
def __init__(
self,
*,
app,
dev_mode_manager: DevModeManager,
config: Optional[Mapping[str, Any]] = None,
logger: Optional[logging.Logger] = None,
) -> None:
self._app = app
self._dev_mode_manager = dev_mode_manager
self._config = config or {}
self._logger = logger or logging.getLogger(__name__)
secret = app.secret_key or "borealis-dev-secret"
self._serializer = URLSafeTimedSerializer(secret, salt="borealis-auth")
default_token_ttl = int(os.environ.get("BOREALIS_TOKEN_TTL_SECONDS", 60 * 60 * 24 * 30))
self._token_ttl = _coerce_positive_int(
self._config.get("auth_token_ttl_seconds"), default_token_ttl
)
default_dev_mode_ttl = int(os.environ.get("BOREALIS_DEV_MODE_TTL_SECONDS", 900))
self._dev_mode_ttl = _coerce_positive_int(
self._config.get("assemblies_dev_mode_ttl_seconds"), default_dev_mode_ttl
)
# ------------------------------------------------------------------
# Session helpers
# ------------------------------------------------------------------
def ensure_session_token(self) -> str:
"""Ensure an opaque session token exists for Dev Mode tracking."""
token = session.get("dev_mode_session")
if not token:
token = uuid.uuid4().hex
session["dev_mode_session"] = token
session.modified = True
return token
def current_user(self) -> Optional[Dict[str, Any]]:
"""Return the authenticated operator profile, if any."""
username = session.get("username")
role = session.get("role") or "User"
if username:
return {"username": username, "role": role}
token = self._bearer_token(request)
if not token:
return None
try:
data = self._serializer.loads(token, max_age=self._token_ttl)
except (BadSignature, SignatureExpired):
self._logger.debug("Bearer token invalid or expired.")
return None
except Exception: # pragma: no cover - defensive logging
self._logger.debug("Unexpected error validating bearer token.", exc_info=True)
return None
username = (data.get("u") or "").strip()
role = (data.get("r") or "User").strip() or "User"
if not username:
return None
return {"username": username, "role": role}
def dev_mode_enabled(self, *, user: Optional[Dict[str, Any]] = None) -> bool:
"""Return whether Dev Mode is currently active for the operator."""
profile = user or self.current_user()
if not profile:
return False
token = self.ensure_session_token()
return self._dev_mode_manager.is_enabled(username=profile["username"], session_token=token)
def set_dev_mode(self, enabled: bool, *, ttl_seconds: Optional[int] = None) -> bool:
"""Enable or disable Dev Mode for the active operator session."""
profile = self.current_user()
if not profile:
raise PermissionError("Authentication required to modify Dev Mode state.") # type: ignore[return-value]
token = self.ensure_session_token()
if enabled:
ttl = ttl_seconds if ttl_seconds and ttl_seconds > 0 else self._dev_mode_ttl
self._dev_mode_manager.enable(
username=profile["username"],
role=profile.get("role") or "User",
session_token=token,
ttl_seconds=ttl,
)
else:
self._dev_mode_manager.disable(username=profile["username"], session_token=token)
return self.dev_mode_enabled(user=profile)
def clear_dev_mode(self) -> None:
"""Explicitly disable Dev Mode for the active operator."""
profile = self.current_user()
if not profile:
return
token = self.ensure_session_token()
self._dev_mode_manager.disable(username=profile["username"], session_token=token)
# ------------------------------------------------------------------
# Permission helpers
# ------------------------------------------------------------------
def require_user(self) -> Tuple[Optional[Dict[str, Any]], Optional[PermissionResult]]:
user = self.current_user()
if not user:
return None, (
{
"error": "unauthorized",
"message": "Authentication required. Please sign in and retry.",
},
401,
)
return user, None
def require_admin(self, *, dev_mode_required: bool = False) -> Optional[PermissionResult]:
user = self.current_user()
if not user:
return (
{
"error": "unauthorized",
"message": "Authentication required. Please sign in and retry.",
},
401,
)
if not self.is_admin(user):
return (
{
"error": "forbidden",
"message": "Administrator permissions are required for this action.",
},
403,
)
if dev_mode_required and not self.dev_mode_enabled(user=user):
return (
{
"error": "dev_mode_required",
"message": "Enable Dev Mode from the Assemblies admin controls to continue.",
},
403,
)
return None
# ------------------------------------------------------------------
# Static helpers
# ------------------------------------------------------------------
@staticmethod
def is_admin(user: Dict[str, Any]) -> bool:
role = (user.get("role") or "").strip().lower()
return role == "admin"
@staticmethod
def _bearer_token(req: Request) -> Optional[str]:
header = (req.headers.get("Authorization") or "").strip()
if header.lower().startswith("bearer "):
return header.split(" ", 1)[1].strip()
cookie_token = req.cookies.get("borealis_auth")
if cookie_token:
return cookie_token
return None
__all__ = ["RequestAuthContext", "PermissionResult"]

View File

@@ -0,0 +1,187 @@
# ======================================================
# Data\Engine\services\auth\dev_mode.py
# Description: Manages in-memory Dev Mode state for operator sessions with TTL enforcement.
#
# API Endpoints (if applicable): None
# ======================================================
"""Server-side Dev Mode state tracking for operator sessions."""
from __future__ import annotations
import datetime as _dt
import logging
import threading
from dataclasses import dataclass
from typing import Dict, Optional, Tuple
def _utcnow() -> _dt.datetime:
return _dt.datetime.utcnow().replace(microsecond=0)
def _normalize_username(value: str) -> str:
return (value or "").strip().lower()
@dataclass(frozen=True)
class DevModeEntry:
"""Represents a Dev Mode grant for an operator."""
username: str
role: str
session_token: str
enabled_at: _dt.datetime
expires_at: _dt.datetime
def is_expired(self, *, now: Optional[_dt.datetime] = None) -> bool:
reference = now or _utcnow()
return reference >= self.expires_at
class DevModeManager:
"""Tracks Dev Mode enablement for operator sessions."""
def __init__(self, *, logger: Optional[logging.Logger] = None, default_ttl_seconds: int = 900) -> None:
ttl = int(default_ttl_seconds or 0)
if ttl < 60:
ttl = 60
self._default_ttl = ttl
self._logger = logger or logging.getLogger(__name__)
self._entries: Dict[Tuple[str, str], DevModeEntry] = {}
self._lock = threading.RLock()
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def enable(
self,
*,
username: str,
role: str,
session_token: str,
ttl_seconds: Optional[int] = None,
) -> DevModeEntry:
"""Enable Dev Mode for the specified operator session."""
normalized = _normalize_username(username)
if not normalized:
raise ValueError("username required for Dev Mode enablement")
token = (session_token or "").strip()
if not token:
raise ValueError("session token required for Dev Mode enablement")
ttl = ttl_seconds if ttl_seconds and ttl_seconds > 0 else self._default_ttl
if ttl < 60:
ttl = 60
now = _utcnow()
entry = DevModeEntry(
username=normalized,
role=(role or "User").strip() or "User",
session_token=token,
enabled_at=now,
expires_at=now + _dt.timedelta(seconds=ttl),
)
with self._lock:
self._prune_locked(now=now)
self._entries[(normalized, token)] = entry
self._logger.info(
"Dev Mode enabled for user='%s' role='%s' session_suffix='%s' ttl_seconds=%s",
normalized,
entry.role,
token[-6:],
ttl,
)
return entry
def disable(self, *, username: str, session_token: str) -> bool:
"""Disable Dev Mode for the specified operator session."""
normalized = _normalize_username(username)
token = (session_token or "").strip()
if not normalized or not token:
return False
removed = False
with self._lock:
removed = self._entries.pop((normalized, token), None) is not None
if removed:
self._logger.info(
"Dev Mode disabled for user='%s' session_suffix='%s'",
normalized,
token[-6:],
)
return removed
def is_enabled(self, *, username: str, session_token: str) -> bool:
"""Return whether Dev Mode is currently enabled for the operator session."""
normalized = _normalize_username(username)
token = (session_token or "").strip()
if not normalized or not token:
return False
now = _utcnow()
with self._lock:
entry = self._entries.get((normalized, token))
if not entry:
self._prune_locked(now=now)
return False
if entry.is_expired(now=now):
self._entries.pop((normalized, token), None)
self._logger.info(
"Dev Mode expired for user='%s' session_suffix='%s'",
normalized,
token[-6:],
)
return False
return True
def describe(self) -> Dict[str, Dict[str, str]]:
"""Return a serialisable snapshot of active Dev Mode entries."""
snapshot: Dict[str, Dict[str, str]] = {}
now = _utcnow()
with self._lock:
self._prune_locked(now=now)
for entry in self._entries.values():
key = f"{entry.username}:{entry.session_token[-6:]}"
snapshot[key] = {
"username": entry.username,
"role": entry.role,
"enabled_at": entry.enabled_at.isoformat(),
"expires_at": entry.expires_at.isoformat(),
}
return snapshot
def clean_expired(self) -> None:
"""Remove expired Dev Mode entries."""
with self._lock:
self._prune_locked(now=_utcnow())
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _prune_locked(self, *, now: Optional[_dt.datetime] = None) -> None:
reference = now or _utcnow()
expired_keys = [
key for key, entry in self._entries.items() if entry.is_expired(now=reference)
]
for key in expired_keys:
entry = self._entries.pop(key, None)
if entry:
self._logger.info(
"Dev Mode expired for user='%s' session_suffix='%s' (prune)",
entry.username,
entry.session_token[-6:],
)
__all__ = ["DevModeManager", "DevModeEntry"]