"""Bounded enrollment orchestration for a new managed application identity. The plane is intentionally not a generic infrastructure API. It creates a single controller target from a trusted profile, generates write-only credentials, and only then creates exact-target deployment policies. Every resource identifier and error returned from this module is sanitized. """ from __future__ import annotations import hashlib import json import re import secrets import uuid from typing import Any, Literal from urllib.parse import urlsplit, urlunsplit from sqlalchemy.exc import IntegrityError from sqlmodel import Session, select from ..connectors.authentik import AuthentikConnector from ..connectors.nginx_proxy_manager import NginxProxyManagerConnector from ..connectors.portainer import PortainerConnector from ..core.time import utc_now from ..db.models import AppSetting, Enrollment, EnrollmentJob, Policy from ..schemas.enrollment import ( LEGACY_APPDATA_ROOT, MANAGED_DOCKER_APPDATA_ROOT, EnrollmentPlanV1, EnrollmentTrustConfigV1, ) from .audit_logger import log_event from .enrollment_oidc_settings import bind_template from .deployment_broker import DeploymentBrokerClient from .enrollment_profiles import PROFILE_VERSION, profile_contract, profile_summary from .secret_store import ( delete_secret, get_secret, has_secret, secret_storage_status, set_secret, ) TRUST_SETTING_KEY = "deployment_enrollment:trust" _IDEMPOTENCY_KEY = re.compile(r"^[A-Za-z0-9._:-]{8,128}$") _TERMINAL = {"ready", "failed", "compensation_required", "preflight_failed"} _CONTROLLER_EVIDENCE = {"status", "targetId", "repository", "applicationId", "appdataPath"} _PROXY_EVIDENCE = {"success", "status", "hostId", "hostname"} _OIDC_EVIDENCE = {"success", "status", "applicationId", "providerId", "clientSecretCreated"} _APPDATA_ROOT_STATUSES = {"ready", "needs_initialization", "unavailable"} _CAPABILITY_REASON = re.compile(r"^[a-z][a-z0-9_]{0,79}$") _RUNTIME_SECURITY_FEATURES = ("exactTargetPolicy", "hmacV2", "replayProtection") def _safe_capability_reason(value: object) -> str: reason = str(value or "") return reason if _CAPABILITY_REASON.fullmatch(reason) else "" def _features_ready(features: object, names: tuple[str, ...]) -> bool: return bool( isinstance(features, dict) and all(features.get(name) is True for name in names) ) def _trusted_appdata_roots_readiness( controller_capability: dict[str, Any], ) -> list[dict[str, str]]: roots = (LEGACY_APPDATA_ROOT, MANAGED_DOCKER_APPDATA_ROOT) unavailable = {root: "unavailable" for root in roots} reported = controller_capability.get("availableTrustedAppdataRoots") if isinstance(reported, list): if len(reported) != len(roots): return [{"root": root, "status": unavailable[root]} for root in roots] statuses: dict[str, dict[str, str]] = {} for item in reported: if not isinstance(item, dict): return [{"root": root, "status": unavailable[root]} for root in roots] root = item.get("root") status = item.get("status") if ( not isinstance(root, str) or root not in unavailable or root in statuses or not isinstance(status, str) or status not in _APPDATA_ROOT_STATUSES ): return [{"root": expected, "status": unavailable[expected]} for expected in roots] value = {"root": root, "status": status} reason = _safe_capability_reason(item.get("reason")) if reason: value["reason"] = reason statuses[root] = value if set(statuses) != set(roots): return [{"root": root, "status": unavailable[root]} for root in roots] return [statuses[root] for root in roots] # A legacy V1 controller reports only its configured root. It cannot attest # independent managed-volume readiness, so only its exact legacy root can # be marked ready through this compatibility path. if "availableTrustedAppdataRoots" not in controller_capability: if ( controller_capability.get("status") == "ready" and controller_capability.get("trustedAppdataRoot") == LEGACY_APPDATA_ROOT ): unavailable[LEGACY_APPDATA_ROOT] = "ready" return [{"root": root, "status": unavailable[root]} for root in roots] class EnrollmentError(ValueError): pass def _canonical(value: EnrollmentPlanV1) -> tuple[dict[str, Any], str]: payload = value.model_dump(mode="json") digest = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest() return payload, digest def _idempotency_digest(operation: str, value: str) -> str: if not _IDEMPOTENCY_KEY.fullmatch(value): raise EnrollmentError("idempotency key must contain 8 to 128 safe characters") return hashlib.sha256(f"{operation}:{value}".encode()).hexdigest() def _check( code: str, status: Literal["passed", "blocked", "warning", "unknown"], detail: str, ) -> dict[str, str]: return {"code": code, "status": status, "detail": detail} def _secret_ref(application_id: str, kind: str) -> str: return f"ENROLL_{application_id.upper().replace('-', '_')}_{kind}" def _credential() -> str: # 64 URL-safe characters provide a high-entropy printable HMAC key while # remaining suitable for the controller's mode-0600 credential file. return secrets.token_urlsafe(48) def _fingerprint(value: str) -> str: return hashlib.sha256(value.encode("utf-8")).hexdigest()[:16] def _enrollment_secret_store_ready(storage: dict[str, Any]) -> bool: """Enrollment always needs a configured production-grade key. The general secret store intentionally supports a development fallback for offline UI work. Enrollment generates controller and OIDC credentials, so it must never accept that deterministic fallback as a write target. """ return bool(storage.get("productionReady")) def _application_oidc_issuer(issuer: str | None, application_id: str | None = None) -> str | None: """Bind Authentik's per-provider issuer to this application's own client.""" if not issuer: return None parsed = urlsplit(issuer) path = parsed.path.rstrip('/') if application_id is None: if path not in {'', '/application/o'} and not re.fullmatch(r'/application/o/[a-z][a-z0-9-]{1,62}', path): return None return urlunsplit((parsed.scheme, parsed.netloc, path + '/', '', '')) expected_path = f"/application/o/{application_id}" if parsed.path.rstrip("/") not in {"", "/application/o", expected_path}: return None return urlunsplit((parsed.scheme, parsed.netloc, expected_path + "/", "", "")) class EnrollmentPlane: def __init__( self, *, broker: DeploymentBrokerClient | None = None, portainer: PortainerConnector | None = None, npm: NginxProxyManagerConnector | None = None, authentik: AuthentikConnector | None = None, ): self.broker = broker or DeploymentBrokerClient() self.portainer = portainer or PortainerConnector() self.npm = npm or NginxProxyManagerConnector() self.authentik = authentik or AuthentikConnector() @staticmethod def get_trust_config(session: Session) -> EnrollmentTrustConfigV1 | None: row = session.get(AppSetting, TRUST_SETTING_KEY) if not row or row.is_secret: return None try: return EnrollmentTrustConfigV1.model_validate(row.value_json) except ValueError: return None @staticmethod def set_trust_config(session: Session, value: EnrollmentTrustConfigV1) -> dict[str, Any]: row = session.get(AppSetting, TRUST_SETTING_KEY) or AppSetting( key=TRUST_SETTING_KEY, is_secret=False ) row.value_json = value.model_dump(mode="json") row.is_secret = False row.updated_at = utc_now() session.add(row) session.commit() return EnrollmentPlane.trust_status(session) @staticmethod def trust_status(session: Session) -> dict[str, Any]: trust = EnrollmentPlane.get_trust_config(session) storage = secret_storage_status() if trust is None: return { "status": "needs_setup", "blockers": ["enrollment_trust_not_configured"], "configured": False, "trustedAppdataRoot": LEGACY_APPDATA_ROOT, "managedAppdataBackupAcknowledged": False, "secretStore": storage, } return { "status": "ready" if _enrollment_secret_store_ready(storage) else "blocked", "blockers": [] if _enrollment_secret_store_ready(storage) else ["secret_store_not_production_ready"], "configured": True, "controllerIdentity": trust.controller_identity, "trustedAppdataRoot": trust.trusted_appdata_root, "managedAppdataBackupAcknowledged": trust.managed_appdata_backup_acknowledged, "approvedNetworks": list(trust.approved_networks), "allowedDomainSuffixes": list(trust.allowed_domain_suffixes), "autonomousProfiles": list(trust.autonomous_profiles), "repopulseProxyAccessListId": trust.repopulse_proxy_access_list_id, "oidcConfigured": bool(trust.oidc_issuer and trust.administrator_group), "bootstrapSecretConfigured": has_secret(session, trust.bootstrap_secret_ref), "secretStore": storage, } def capabilities(self, session: Session) -> dict[str, Any]: trust = self.get_trust_config(session) if isinstance(self.authentik, AuthentikConnector): bind_template(session, self.authentik) storage = secret_storage_status() configured = trust is not None bootstrap = bool(trust and has_secret(session, trust.bootstrap_secret_ref)) controller_capability: dict[str, Any] = {} if bootstrap and self.broker.is_configured() and trust is not None: bootstrap_secret = get_secret(session, trust.bootstrap_secret_ref, touch=False) if bootstrap_secret: controller_capability = self.broker.enrollment_capabilities(secret=bootstrap_secret) available_roots = _trusted_appdata_roots_readiness(controller_capability) if trust is not None: selected_root = next( ( item for item in available_roots if item["root"] == trust.trusted_appdata_root ), None, ) controller_root_matches = bool(selected_root and selected_root.get("status") == "ready") else: controller_root_matches = False controller_core_ready = bool( bootstrap and self.broker.is_configured() and controller_capability.get("status") == "ready" ) features = controller_capability.get("features") runtime_security_ready = _features_ready(features, _RUNTIME_SECURITY_FEATURES) apply_ready = bool( controller_core_ready and runtime_security_ready and isinstance(features, dict) and features.get("deploymentApply") is True ) verify_ready = bool( controller_core_ready and runtime_security_ready and isinstance(features, dict) and features.get("deploymentVerify") is True ) rollback_ready = bool( controller_core_ready and runtime_security_ready and isinstance(features, dict) and features.get("rollback") is True ) runtime_deployment_ready = apply_ready and verify_ready enrollment_features_ready = _features_ready( features, ("enrollment", "targetRegistration", "runtimeSecretSealing"), ) target_enrollment_ready = bool( controller_core_ready and controller_root_matches and enrollment_features_ready ) controller_managed_storage = controller_capability.get("managedStorage") if not isinstance(controller_managed_storage, dict): controller_managed_storage = {} if trust is None or trust.trusted_appdata_root != MANAGED_DOCKER_APPDATA_ROOT: managed_storage_status = "not_selected" managed_storage_blocker = "" elif not trust.managed_appdata_backup_acknowledged: managed_storage_status = "blocked" managed_storage_blocker = "managed_appdata_backup_acknowledgement_required" elif controller_root_matches and controller_managed_storage.get("status") == "ready": managed_storage_status = "ready" managed_storage_blocker = "" elif controller_managed_storage.get("initializationAvailable") is True: managed_storage_status = "initialization_required" managed_storage_blocker = "managed_appdata_initialization_required" else: managed_storage_status = "unavailable" managed_storage_blocker = "managed_appdata_volume_unavailable" controller_roots_by_path = { str(item.get("root") or ""): item for item in available_roots if isinstance(item, dict) } selected_root_readiness = ( controller_roots_by_path.get(trust.trusted_appdata_root) if trust is not None else None ) controller_enrollment = controller_capability.get("enrollmentReadiness") if not isinstance(controller_enrollment, dict): controller_enrollment = {} root_reason = _safe_capability_reason( selected_root_readiness.get("reason") if isinstance(selected_root_readiness, dict) else None ) if not root_reason and trust is not None and trust.trusted_appdata_root == MANAGED_DOCKER_APPDATA_ROOT: root_reason = _safe_capability_reason(controller_managed_storage.get("reason")) enrollment_reason = ( root_reason or _safe_capability_reason(controller_enrollment.get("reason")) or ( "enrollment_trust_not_configured" if trust is None else "trusted_appdata_root_unavailable" if not controller_root_matches else "controller_enrollment_feature_unavailable" if not enrollment_features_ready else "deployment_executor_unavailable" ) ) if target_enrollment_ready: enrollment_reason = "" npm_ready = self.npm.is_configured() oidc_configured = bool( trust and trust.oidc_issuer and _application_oidc_issuer(trust.oidc_issuer) and trust.administrator_group and trust.support_agent_group and trust.read_only_group and self.authentik.is_configured() ) if oidc_configured and trust is not None: template = self.authentik.validate_deployment_oidc_template( role_claim=trust.oidc_role_claim, role_groups=(trust.administrator_group, trust.support_agent_group, trust.read_only_group), ) groups = self.authentik.groups() found_groups = { str(group.get("name") or "") for group in groups.get("items", []) if isinstance(group, dict) } oidc_configured = bool( template.get("status") == "ready" and groups.get("success") is True and { trust.administrator_group, trust.support_agent_group, trust.read_only_group, }.issubset(found_groups) ) certificate_ready = bool(npm_ready) def item(ready: bool, code: str) -> dict[str, str]: return {"status": "ready" if ready else "unavailable", "blockerCode": "" if ready else code} if not controller_core_ready: enrollment_blocker = "deployment_executor_unavailable" elif not controller_root_matches: enrollment_blocker = "trusted_appdata_root_unavailable" else: enrollment_blocker = "controller_enrollment_feature_unavailable" registry_report = controller_capability.get("registry") registry_projection: dict[str, Any] = {"status": "unavailable"} if isinstance(registry_report, dict): registry_status = registry_report.get("status") if registry_status in {"ready", "unavailable"}: registry_projection["status"] = registry_status count = registry_report.get("registeredProjectCount") if ( registry_status == "ready" and type(count) is int and 0 <= count <= 500 ): registry_projection["registeredProjectCount"] = count controller_reason = ( "" if controller_core_ready else _safe_capability_reason(controller_capability.get("reason")) or "deployment_executor_unavailable" ) return { "apiVersion": "itworx.enrollment-capabilities/v1", "trust": item(configured, "enrollment_trust_not_configured"), "targetEnrollment": item(target_enrollment_ready, enrollment_blocker), "secretBootstrap": item(_enrollment_secret_store_ready(storage), "secret_store_not_production_ready"), "runtimeDeployment": item(runtime_deployment_ready, "deployment_executor_unavailable"), "reverseProxyProvisioning": item(npm_ready, "npm_credentials_missing"), "tlsProvisioning": item(certificate_ready, "certificate_provisioning_unavailable"), "oidcProvisioning": item(oidc_configured, "oidc_profile_unavailable"), "policyProvisioning": item(configured, "policy_template_unavailable"), "rollback": item(rollback_ready, "deployment_executor_unavailable"), "verification": item(verify_ready, "deployment_executor_unavailable"), "availableTrustedAppdataRoots": available_roots, "managedStorage": { "status": managed_storage_status, "blockerCode": managed_storage_blocker, "root": MANAGED_DOCKER_APPDATA_ROOT, "backupScope": "dockerRootDir", "backupAcknowledged": bool( trust and trust.managed_appdata_backup_acknowledged ), **( {"reason": _safe_capability_reason(controller_managed_storage.get("reason"))} if trust is not None and trust.trusted_appdata_root == MANAGED_DOCKER_APPDATA_ROOT and _safe_capability_reason(controller_managed_storage.get("reason")) else {} ), }, "enrollmentReadiness": { "status": "ready" if target_enrollment_ready else "unavailable", "reason": enrollment_reason, }, "registry": registry_projection, "controller": { "status": "ready" if controller_core_ready else "unavailable", "reason": controller_reason, }, "profiles": [profile_summary(name) for name in ("internal-webapp", "internal-webapp-oidc", "internal-service", "repopulse-authenticated")], } @staticmethod def _allowed_hostname(hostname: str, trust: EnrollmentTrustConfigV1) -> bool: return any(hostname == suffix or hostname.endswith(f".{suffix}") for suffix in trust.allowed_domain_suffixes) @staticmethod def _existing(session: Session, application_id: str, repository: str) -> tuple[Enrollment | None, Enrollment | None]: by_application = session.exec( select(Enrollment).where(Enrollment.application_id == application_id) ).first() by_repository = session.exec( select(Enrollment).where(Enrollment.repository == repository) ).first() return by_application, by_repository def _controller_payload( self, plan: EnrollmentPlanV1, *, enrollment_id: str, target_credential: str | None = None, ) -> dict[str, Any]: profile = profile_contract(plan.profile) assert profile is not None # validated before this method is called payload: dict[str, Any] = { "api_version": "itworx.controller-enrollment/v1", "enrollment_id": enrollment_id, "application_id": plan.application_id, "repository": plan.repository, "revision": plan.revision, "profile": plan.profile, "container_name": plan.target.container_name, "compose_file": plan.target.compose_file, "appdata_path": plan.appdata_path, "trusted_appdata_root": plan.trusted_appdata_root, "data_path": plan.data_path, "runtime_uid": plan.storage.runtime_uid, "runtime_gid": plan.storage.runtime_gid, "network": plan.network.docker_network, "internal_port": plan.network.internal_port, "proxy_hostname": plan.exposure.hostname if plan.exposure else None, "image_repository": plan.artifact.image_repository, "image_tag": plan.artifact.image_tag, "health_path": plan.artifact.health_path, "runtime_environment_keys": list(profile["runtimeEnvironmentKeys"]), } if target_credential is not None: payload["target_credential"] = target_credential return payload def preflight(self, session: Session, plan: EnrollmentPlanV1) -> dict[str, Any]: checks: list[dict[str, str]] = [] trust = self.get_trust_config(session) if isinstance(self.authentik, AuthentikConnector): bind_template(session, self.authentik) profile = profile_contract(plan.profile) if trust is None: checks.append(_check("enrollment_trust_not_configured", "blocked", "Root enrollment trust boundaries are not configured.")) else: checks.append(_check("enrollment_trust", "passed", "Root-owned enrollment boundaries are configured.")) if plan.target.target_identity == trust.controller_identity: checks.append(_check("controller_identity", "passed", "Requested controller matches the trusted enrollment target.")) else: checks.append(_check("deployment_executor_unavailable", "blocked", "Requested controller is not the configured enrollment target.")) if plan.network.docker_network in trust.approved_networks: checks.append(_check("network", "passed", "Requested Docker network is within the trusted profile boundary.")) else: checks.append(_check("network_unavailable", "blocked", "Requested Docker network is not approved for enrollment.")) # RepoPulse is an established, fixed legacy-root exception; the # operator's selected root applies to other new targets only. expected_appdata_root = ( LEGACY_APPDATA_ROOT if plan.profile == "repopulse-authenticated" else trust.trusted_appdata_root ) if plan.trusted_appdata_root == expected_appdata_root: checks.append(_check("trusted_appdata_root", "passed", "Requested appdata root matches its fixed profile trust boundary.")) else: checks.append(_check("trusted_appdata_root_mismatch", "blocked", "Requested appdata root does not match the explicit operator trust boundary.")) if plan.profile in trust.autonomous_profiles: checks.append(_check("autonomous_profile", "passed", "Deployment profile is approved for autonomous enrollment.")) else: checks.append(_check("policy_template_unavailable", "blocked", "Deployment profile is not approved for autonomous enrollment.")) if profile is None: checks.append(_check("policy_template_unavailable", "blocked", "Requested server-side deployment profile is unavailable.")) else: checks.append(_check("deployment_profile", "passed", "Server-side deployment profile is available.")) if plan.profile == "repopulse-authenticated": gitea_token = get_secret(session, "REPOPULSE_GITEA_TOKEN", touch=False) checks.append(_check( "repopulse_gitea_token_unavailable", "passed" if gitea_token else "blocked", "The existing encrypted RepoPulse Gitea token is available by reference." if gitea_token else "The existing encrypted RepoPulse Gitea token is unavailable.", )) if has_secret(session, _secret_ref("repopulse", "MAINTENANCE_TOKEN")): checks.append(_check( "repopulse_maintenance_token_collision", "blocked", "An existing write-only RepoPulse maintenance token cannot be adopted by a new enrollment.", )) else: checks.append(_check( "repopulse_maintenance_token", "passed", "A new write-only RepoPulse maintenance token can be generated for this enrollment.", )) if trust is None or trust.repopulse_proxy_access_list_id is None: checks.append(_check( "repopulse_access_list_unavailable", "blocked", "A positive root-owned RepoPulse NPM access-list ID is not configured.", )) else: verify_access_list = getattr(self.npm, "validate_managed_access_list", None) access_list = verify_access_list(trust.repopulse_proxy_access_list_id) if callable(verify_access_list) else {} verified = ( access_list.get("success") is True and access_list.get("status") == "verified" and access_list.get("accessListId") == trust.repopulse_proxy_access_list_id ) checks.append(_check( "repopulse_access_list_unavailable", "passed" if verified else "blocked", "The root-owned RepoPulse NPM access list has active authorization entries and requires all checks." if verified else "The root-owned RepoPulse NPM access list could not be verified.", )) by_application, by_repository = self._existing(session, plan.application_id, plan.repository) if by_application: status = "warning" if by_application.plan_sha256 == _canonical(plan)[1] else "blocked" code = "enrollment_replay" if status == "warning" else "target_already_exists" checks.append(_check(code, status, "This application identity is already enrolled.")) else: checks.append(_check("application_identity", "passed", "Application identity is not yet enrolled.")) if by_repository and (not by_application or by_repository.id != by_application.id): checks.append(_check("target_already_exists", "blocked", "Repository is already bound to a different managed application.")) policy_ids = ( f"enrollment-{plan.application_id}-deployment-apply", f"enrollment-{plan.application_id}-deployment-rollback", ) policy_collisions = [policy_id for policy_id in policy_ids if session.get(Policy, policy_id) is not None] checks.append(_check( "exact_policy_id_collision", "blocked" if policy_collisions else "passed", "An enrollment-generated exact policy identifier is already owned and cannot be adopted." if policy_collisions else "Both enrollment-generated exact policy identifiers are available.", )) containers = self.portainer.list_containers() if containers.get("status") != "ok": checks.append(_check("container_inventory_unavailable", "blocked", "Portainer inventory is unavailable for collision protection.")) else: names = {str(item.get("name") or "").casefold() for item in containers.get("containers", [])} if plan.target.container_name.casefold() in names: checks.append(_check("unmanaged_resource_collision", "blocked", "Container identity already exists and cannot be claimed by enrollment.")) else: checks.append(_check("container_identity", "passed", "Container identity is free.")) if plan.exposure: if trust and self._allowed_hostname(plan.exposure.hostname, trust): checks.append(_check("hostname_suffix", "passed", "Hostname is inside an allowed domain suffix.")) else: checks.append(_check("hostname_not_allowed", "blocked", "Hostname is outside the configured allowed suffixes.")) hosts = self.npm.proxy_hosts() if not hosts.get("success"): checks.append(_check("npm_credentials_missing" if not self.npm.is_configured() else "npm_executor_unavailable", "blocked", "NPM inventory is unavailable for takeover protection.")) else: existing = [item for item in hosts.get("items", []) if plan.exposure.hostname in {str(value).lower() for value in item.get("domainNames", [])}] checks.append(_check("hostname_collision", "blocked" if existing else "passed", "Hostname is already owned and cannot be taken over." if existing else "Hostname is free in NPM.")) if plan.authentication.mode == "native-oidc": groups_ready = bool(trust and trust.administrator_group and trust.support_agent_group and trust.read_only_group and trust.oidc_issuer) if not groups_ready or not self.authentik.is_configured(): checks.append(_check("oidc_profile_unavailable", "blocked", "Configured OIDC profile or Authentik role groups are unavailable.")) else: assert trust is not None and plan.authentication.roles is not None issuer_matches = _application_oidc_issuer(trust.oidc_issuer, plan.application_id) checks.append(_check( "oidc_issuer" if issuer_matches else "oidc_issuer_mismatch", "passed" if issuer_matches else "blocked", "Authentik issuer is bound to this application's own provider." if issuer_matches else "Configured issuer belongs to a different application or unsupported issuer path.", )) expected_roles = { "administrator": trust.administrator_group, "support_agent": trust.support_agent_group, "read_only": trust.read_only_group, } requested_roles = plan.authentication.roles.model_dump(mode="json") checks.append(_check( "oidc_role_model", "passed" if requested_roles == expected_roles else "blocked", "Requested application roles match the root-owned Authentik role model." if requested_roles == expected_roles else "Requested application roles do not match the root-owned Authentik role model.", )) auth_status = self.authentik.test_connection() if auth_status.get("status") != "connected": checks.append(_check("oidc_provider", "blocked", "Authentik is unavailable.")) else: template = self.authentik.validate_deployment_oidc_template( role_claim=trust.oidc_role_claim, role_groups=(trust.administrator_group, trust.support_agent_group, trust.read_only_group), ) if template.get("status") != "ready": checks.append(_check("oidc_profile_unavailable", "blocked", "The server-owned Authentik OIDC template is incomplete.")) applications = self.authentik.applications() groups = self.authentik.groups() if not applications.get("success") or not groups.get("success"): checks.append(_check("oidc_provider", "blocked", "Authentik applications cannot be inspected for takeover protection.")) elif any(str(item.get("slug") or "") == plan.application_id for item in applications.get("items", [])): checks.append(_check("unmanaged_resource_collision", "blocked", "An existing Authentik application cannot be claimed by a new enrollment.")) else: checks.append(_check("oidc_provider", "passed", "Authentik is available and the application identity is free.")) required_groups = { trust.administrator_group if trust else "", trust.support_agent_group if trust else "", trust.read_only_group if trust else "", } found_groups = {str(item.get("name") or "") for item in groups.get("items", [])} checks.append(_check( "oidc_role_groups", "passed" if required_groups.issubset(found_groups) else "blocked", "Configured Authentik role groups are available." if required_groups.issubset(found_groups) else "One or more configured Authentik role groups are missing.", )) storage = secret_storage_status() storage_ready = _enrollment_secret_store_ready(storage) checks.append(_check("secret_store", "passed" if storage_ready else "blocked", "Production-grade write-only secret storage is ready." if storage_ready else "Enrollment requires APPOPS_SECRET_KEY from a protected environment variable or file; the development fallback is not permitted.")) if trust: checks.append(_check("controller_bootstrap_secret", "passed" if has_secret(session, trust.bootstrap_secret_ref) else "blocked", "Controller bootstrap secret is available by reference." if has_secret(session, trust.bootstrap_secret_ref) else "Controller bootstrap secret reference is not configured.")) expected_appdata_root = ( LEGACY_APPDATA_ROOT if plan.profile == "repopulse-authenticated" else trust.trusted_appdata_root ) root_matches_trust = plan.trusted_appdata_root == expected_appdata_root if not root_matches_trust: checks.append(_check("controller_target", "blocked", "Controller preflight was skipped because the requested appdata root is outside the profile trust boundary.")) elif has_secret(session, trust.bootstrap_secret_ref): controller = self.broker.enrollment_preflight( self._controller_payload(plan, enrollment_id="enr-00000000000000000000"), secret=get_secret(session, trust.bootstrap_secret_ref, touch=False) or "", ) code = str(controller.get("reason") or "deployment_executor_unavailable") checks.append(_check("controller_target", "passed" if controller.get("status") in {"preflight_ok", "ready"} else "blocked", "Controller accepted the bounded enrollment preflight." if controller.get("status") in {"preflight_ok", "ready"} else f"Controller enrollment preflight is unavailable ({code}).")) else: checks.append(_check("controller_target", "blocked", "Controller bootstrap credential is unavailable.")) blockers = [item["code"] for item in checks if item["status"] == "blocked"] warnings = [item["code"] for item in checks if item["status"] == "warning"] planned_resources = { "applicationId": plan.application_id, "controllerTarget": plan.target.target_identity, "appdataPath": plan.appdata_path, "dataPath": plan.data_path, "network": plan.network.docker_network, "hostname": plan.exposure.hostname if plan.exposure else None, "oidc": plan.authentication.mode == "native-oidc", } return { "status": "ready" if not blockers else "blocked", "blockers": blockers, "warnings": warnings, "checks": checks, "plannedResources": planned_resources, "plannedMutations": [] if blockers else ["controller_target_register", "secret_generate", *( ["reverse_proxy_ensure"] if plan.exposure else []), *( ["oidc_application_ensure"] if plan.authentication.mode == "native-oidc" else []), *( ["runtime_environment_seal"] if profile and profile["runtimeEnvironmentKeys"] else []), "exact_policy_install"], "plannedSecretRefs": [ _secret_ref(plan.application_id, "TARGET_HMAC"), *( [_secret_ref(plan.application_id, "OIDC_CLIENT_SECRET")] if plan.authentication.mode == "native-oidc" else []), *( ["REPOPULSE_GITEA_TOKEN", _secret_ref("repopulse", "MAINTENANCE_TOKEN")] if plan.profile == "repopulse-authenticated" else []), ], "plannedPolicies": [f"enrollment-{plan.application_id}-deployment-apply", f"enrollment-{plan.application_id}-deployment-rollback"], } def _create_enrollment(self, session: Session, plan: EnrollmentPlanV1, plan_sha256: str, trust: EnrollmentTrustConfigV1) -> Enrollment: enrollment = Enrollment( id=f"enr-{uuid.uuid4().hex[:20]}", application_id=plan.application_id, repository=plan.repository, revision=plan.revision, profile=plan.profile, controller_identity=trust.controller_identity, plan_sha256=plan_sha256, status="planned", target_secret_ref=_secret_ref(plan.application_id, "TARGET_HMAC"), oidc_secret_ref=_secret_ref(plan.application_id, "OIDC_CLIENT_SECRET") if plan.authentication.mode == "native-oidc" else None, plan_json=plan.model_dump(mode="json"), provenance_json={"profileVersion": PROFILE_VERSION, "createdBy": "enrollment-plane/v1", "sourceRevision": plan.revision}, ) session.add(enrollment) session.commit() session.refresh(enrollment) return enrollment def _create_job(self, session: Session, enrollment: Enrollment, idempotency_key: str) -> EnrollmentJob: digest = _idempotency_digest("enroll", idempotency_key) existing = session.exec(select(EnrollmentJob).where(EnrollmentJob.idempotency_sha256 == digest)).first() if existing: if existing.enrollment_id != enrollment.id: raise EnrollmentError("idempotency key was already used for a different enrollment") return existing job = EnrollmentJob( id=f"enroll-{uuid.uuid4().hex[:16]}", enrollment_id=enrollment.id, application_id=enrollment.application_id, idempotency_sha256=digest, status="queued", stage="planned", ) session.add(job) session.commit() session.refresh(job) return job @staticmethod def serialize_enrollment(enrollment: Enrollment) -> dict[str, Any]: plan = dict(enrollment.plan_json or {}) exposure = plan.get("exposure") if isinstance(plan.get("exposure"), dict) else {} authentication = plan.get("authentication") if isinstance(plan.get("authentication"), dict) else {} return { "id": enrollment.id, "applicationId": enrollment.application_id, "repository": enrollment.repository, "revision": enrollment.revision, "profile": enrollment.profile, "controllerIdentity": enrollment.controller_identity, "status": enrollment.status, "publicUrl": f"https://{exposure.get('hostname')}" if exposure.get("hostname") else None, "authentication": authentication.get("mode", "none"), "resources": enrollment.resources_json or {}, "targetSecretRef": enrollment.target_secret_ref, "oidcSecretRef": enrollment.oidc_secret_ref, "maintenanceSecretRef": ( _secret_ref("repopulse", "MAINTENANCE_TOKEN") if enrollment.profile == "repopulse-authenticated" else None ), "lastVerifiedAt": enrollment.last_verified_at.isoformat() if enrollment.last_verified_at else None, "createdAt": enrollment.created_at.isoformat(), "updatedAt": enrollment.updated_at.isoformat(), "completedAt": enrollment.completed_at.isoformat() if enrollment.completed_at else None, } @staticmethod def serialize_job(job: EnrollmentJob, *, replay: bool = False) -> dict[str, Any]: return { "id": job.id, "enrollmentId": job.enrollment_id, "applicationId": job.application_id, "operation": job.operation, "status": job.status, "stage": job.stage, "preflight": job.preflight_json or {}, "evidence": job.evidence_json or {}, "result": job.result_summary, "createdAt": job.created_at.isoformat(), "updatedAt": job.updated_at.isoformat(), "completedAt": job.completed_at.isoformat() if job.completed_at else None, "idempotentReplay": replay, } @staticmethod def _checkpoint( session: Session, enrollment: Enrollment, job: EnrollmentJob, *, stage: str, evidence: dict[str, Any], ) -> None: """Durably record intent and receipts around every external boundary. This is deliberately conservative: after an interrupted external call, a same-plan request is surfaced as recovery-required instead of being replayed against NPM, Authentik or the controller without proof of its prior effect. Values in ``evidence`` are identifiers/status only. """ now = utc_now() enrollment.status = "provisioning" enrollment.resources_json = {**dict(enrollment.resources_json or {}), **evidence} enrollment.updated_at = now job.status = "running" job.stage = stage job.evidence_json = {**dict(job.evidence_json or {}), **evidence} job.updated_at = now session.add(enrollment) session.add(job) session.commit() def _install_policies(self, session: Session, enrollment: Enrollment, plan: EnrollmentPlanV1) -> list[str]: provenance = { "enrollmentId": enrollment.id, "profileVersion": PROFILE_VERSION, "createdBy": "enrollment-plane/v1", "policyTemplateVersion": "exact-target/v1", "target": plan.application_id, "sourceRevision": plan.revision, "createdAt": utc_now().isoformat(), } policy_specs = ( ( "deployment.apply", f"enrollment-{plan.application_id}-deployment-apply", ), ( "deployment.rollback", f"enrollment-{plan.application_id}-deployment-rollback", ), ) identifiers = [policy_id for _action, policy_id in policy_specs] # Check the complete pair before adding either row. The database flush # below is one transaction so a concurrent uniqueness collision cannot # leave the first insert committed by _fail(). if any(session.get(Policy, policy_id) is not None for policy_id in identifiers): raise EnrollmentError("exact policy identifier is already owned and cannot be adopted") for action, policy_id in policy_specs: policy = Policy( id=policy_id, action=action, target=plan.application_id, voice="blocked", manual="allowed", risk="high", reason="Enrollment-generated exact-target deployment policy.", ) policy.provenance_json = provenance session.add(policy) try: # Force uniqueness errors inside this boundary; on failure discard # the uncommitted unit of work before _fail() persists its outcome. session.flush() except IntegrityError as exc: session.rollback() raise EnrollmentError("exact policy identifier is already owned and cannot be adopted") from exc # Deliberately do not commit here: the caller commits these rows together # with the enrollment/job ready state after policy installation succeeds. return identifiers def _compensate( self, session: Session, enrollment: Enrollment, evidence: dict[str, Any], ) -> dict[str, Any]: """Reverse only resources this execution proved it created. A failed compensation stays visible as ``compensation_required``. It never attempts to adopt, overwrite or delete an earlier resource. """ plan = EnrollmentPlanV1.model_validate(enrollment.plan_json) outcomes: dict[str, str] = {} oidc = evidence.get("oidc") if isinstance(evidence.get("oidc"), dict) else {} if oidc.get("status") == "created": remove = getattr(self.authentik, "remove_deployment_oidc_application", None) result = remove( application_id=oidc.get("applicationId"), provider_id=oidc.get("providerId"), application_slug=plan.application_id, launch_url=plan.public_url or "", ) if callable(remove) else {"success": False, "status": "unsupported"} outcomes["oidc"] = str(result.get("status") or "failed") elif oidc.get("status") == "partial_failure": outcomes["oidc"] = "ownership_unverified" proxy = evidence.get("proxy") if isinstance(evidence.get("proxy"), dict) else {} if proxy.get("status") == "created": remove = getattr(self.npm, "remove_managed_proxy_host", None) result = remove(host_id=proxy.get("hostId"), ownership_id=enrollment.id) if callable(remove) else {"success": False, "status": "unsupported"} outcomes["proxy"] = str(result.get("status") or "failed") elif proxy.get("status") == "ownership_unverified": outcomes["proxy"] = "ownership_unverified" controller = evidence.get("controller") if isinstance(evidence.get("controller"), dict) else {} if controller.get("status") in {"enrolled", "already_enrolled"}: trust = self.get_trust_config(session) bootstrap = get_secret(session, trust.bootstrap_secret_ref, touch=False) if trust else None if bootstrap: result = self.broker.unenroll_target( {"enrollment_id": enrollment.id, "application_id": enrollment.application_id, "repository": enrollment.repository}, secret=bootstrap, ) outcomes["controller"] = str(result.get("status") or "failed") else: outcomes["controller"] = "bootstrap_secret_unavailable" elif controller.get("status") == "partial_failure": outcomes["controller"] = "ownership_unverified" completed = all(value in {"removed", "rolled_back"} for value in outcomes.values()) if completed and outcomes: delete_secret(session, enrollment.target_secret_ref) if enrollment.oidc_secret_ref: delete_secret(session, enrollment.oidc_secret_ref) if plan.profile == "repopulse-authenticated": delete_secret(session, _secret_ref("repopulse", "MAINTENANCE_TOKEN")) return {"completed": completed, "outcomes": outcomes} def _fail( self, session: Session, enrollment: Enrollment, job: EnrollmentJob, *, stage: str, message: str, evidence: dict[str, Any], ) -> dict[str, Any]: compensation = ( self._compensate(session, enrollment, evidence) if evidence.get("controller") else {"completed": True, "outcomes": {}} ) compensation_required = not bool(compensation.get("completed")) enrollment.status = "compensation_required" if compensation_required else "failed" enrollment.resources_json = {**dict(enrollment.resources_json or {}), **evidence, "compensation": compensation} enrollment.updated_at = utc_now() enrollment.completed_at = utc_now() job.status = "compensation_required" if compensation_required else "failed" job.stage = stage job.evidence_json = {**dict(job.evidence_json or {}), **evidence, "compensation": compensation} job.result_summary = message job.updated_at = utc_now() job.completed_at = utc_now() session.add(enrollment) session.add(job) session.commit() log_event( session, source="enrollment-plane", identity="operator", action_id="target.enroll", target=enrollment.application_id, target_type="enrollment", risk="high", policy_decision="blocked", reason=message, result="failed", execution_plan={"enrollmentId": enrollment.id, "planSha256": enrollment.plan_sha256}, after_state={"stage": stage, "status": enrollment.status}, ) return {"enrollment": self.serialize_enrollment(enrollment), "job": self.serialize_job(job)} def enroll(self, session: Session, plan: EnrollmentPlanV1, *, idempotency_key: str) -> dict[str, Any]: _, plan_digest = _canonical(plan) existing, repository_existing = self._existing(session, plan.application_id, plan.repository) if existing and existing.plan_sha256 == plan_digest: prior = session.exec( select(EnrollmentJob).where(EnrollmentJob.enrollment_id == existing.id).order_by(EnrollmentJob.created_at.desc()) ).first() if prior is None or prior.status in _TERMINAL: return { "enrollment": self.serialize_enrollment(existing), "job": self.serialize_job(prior, replay=True) if prior else None, "idempotentReplay": True, } # A process may have stopped after an external request was sent but # before its response was durably checkpointed. Do not infer the # effect and do not send the request again: retain the exact intent # and require an explicit reconciliation flow. existing.status = "recovery_required" existing.updated_at = utc_now() prior.status = "recovery_required" prior.result_summary = "Enrollment stopped during an external stage; reconcile recorded intent and receipts before retrying." prior.updated_at = utc_now() session.add(existing) session.add(prior) session.commit() return { "enrollment": self.serialize_enrollment(existing), "job": self.serialize_job(prior), "recoveryRequired": True, } if existing or repository_existing: raise EnrollmentError("application identity or repository is already enrolled") trust = self.get_trust_config(session) if isinstance(self.authentik, AuthentikConnector): bind_template(session, self.authentik) if trust is None: raise EnrollmentError("enrollment trust boundaries are not configured") enrollment = self._create_enrollment(session, plan, plan_digest, trust) job = self._create_job(session, enrollment, idempotency_key) if job.status != "queued": return {"enrollment": self.serialize_enrollment(enrollment), "job": self.serialize_job(job, replay=True)} preflight = self.preflight(session, plan) job.preflight_json = preflight job.updated_at = utc_now() session.add(job) session.commit() if preflight["status"] != "ready": enrollment.status = "preflight_failed" enrollment.completed_at = utc_now() enrollment.updated_at = utc_now() job.status = "preflight_failed" job.stage = "preflight" job.result_summary = "Enrollment preflight was blocked." job.completed_at = utc_now() session.add(enrollment) session.add(job) session.commit() return {"enrollment": self.serialize_enrollment(enrollment), "job": self.serialize_job(job)} repopulse_gitea_token = ( get_secret(session, "REPOPULSE_GITEA_TOKEN", touch=False) if plan.profile == "repopulse-authenticated" else None ) if plan.profile == "repopulse-authenticated" and not repopulse_gitea_token: return self._fail( session, enrollment, job, stage="preflight", message="The existing encrypted RepoPulse Gitea token became unavailable after preflight.", evidence={}, ) target_secret = _credential() set_secret( session, enrollment.target_secret_ref, target_secret, used_by=[f"enrollment:{enrollment.id}", "deployment-controller"], metadata={"enrollmentId": enrollment.id, "credentialFingerprint": _fingerprint(target_secret), "rotationState": "active"}, ) oidc_secret: str | None = None if enrollment.oidc_secret_ref: oidc_secret = _credential() set_secret( session, enrollment.oidc_secret_ref, oidc_secret, used_by=[f"enrollment:{enrollment.id}", "authentik", "deployment-controller"], metadata={"enrollmentId": enrollment.id, "credentialFingerprint": _fingerprint(oidc_secret), "rotationState": "active"}, ) repopulse_maintenance_token: str | None = None if plan.profile == "repopulse-authenticated": repopulse_maintenance_token = _credential() set_secret( session, _secret_ref("repopulse", "MAINTENANCE_TOKEN"), repopulse_maintenance_token, used_by=[f"enrollment:{enrollment.id}", "repopulse-maintenance", "deployment-controller"], metadata={ "enrollmentId": enrollment.id, "credentialFingerprint": _fingerprint(repopulse_maintenance_token), "rotationState": "active", }, ) bootstrap_secret = get_secret(session, trust.bootstrap_secret_ref, touch=False) if not bootstrap_secret: return self._fail(session, enrollment, job, stage="creating_secrets", message="Controller bootstrap secret disappeared after preflight.", evidence={}) evidence: dict[str, Any] = { "intent": { "controller": { "enrollmentId": enrollment.id, "applicationId": plan.application_id, "repository": plan.repository, } } } self._checkpoint( session, enrollment, job, stage="registering_target", evidence=evidence ) controller_result = self.broker.enroll_target( self._controller_payload(plan, enrollment_id=enrollment.id, target_credential=target_secret), secret=bootstrap_secret, idempotency_key=idempotency_key, ) evidence["controller"] = { key: controller_result[key] for key in _CONTROLLER_EVIDENCE if key in controller_result } self._checkpoint( session, enrollment, job, stage="registering_target", evidence=evidence ) if controller_result.get("status") not in {"enrolled", "already_enrolled"}: return self._fail(session, enrollment, job, stage="registering_target", message="Controller rejected the bounded target enrollment.", evidence=evidence) if plan.exposure: evidence["intent"] = { **dict(evidence.get("intent") or {}), "proxy": {"hostname": plan.exposure.hostname, "ownershipId": enrollment.id}, } self._checkpoint( session, enrollment, job, stage="provisioning_proxy", evidence=evidence ) proxy_arguments = { "hostname": plan.exposure.hostname, "forward_host": plan.application_id, "forward_port": plan.network.internal_port, "certificate_mode": "managed", "force_ssl": True, "http2": True, "upload_limit_mb": 1024 if plan.exposure.upload_profile == "large-upload" else 128, "read_timeout_seconds": 900 if plan.exposure.timeout_profile == "long-running" else 120, "ownership_id": enrollment.id, } if plan.profile == "repopulse-authenticated": access_list_id = trust.repopulse_proxy_access_list_id if trust else None if not access_list_id or access_list_id <= 0: return self._fail( session, enrollment, job, stage="provisioning_proxy", message="RepoPulse requires a verified, positive NPM access-list ID.", evidence=evidence, ) proxy_arguments["access_list_id"] = access_list_id proxy_result = self.npm.ensure_managed_proxy_host( **proxy_arguments, ) evidence["proxy"] = {key: proxy_result[key] for key in _PROXY_EVIDENCE if key in proxy_result} self._checkpoint( session, enrollment, job, stage="provisioning_proxy", evidence=evidence ) if not proxy_result.get("success") or proxy_result.get("status") != "created": return self._fail(session, enrollment, job, stage="provisioning_proxy", message="Managed reverse-proxy provisioning was not verified.", evidence=evidence) profile = profile_contract(plan.profile) runtime_values: dict[str, str] = {} if plan.public_url and profile and "APPOPS_PUBLIC_URL" in profile["runtimeEnvironmentKeys"]: runtime_values["APPOPS_PUBLIC_URL"] = plan.public_url if plan.authentication.mode == "native-oidc": assert oidc_secret is not None and enrollment.oidc_secret_ref is not None evidence["intent"] = { **dict(evidence.get("intent") or {}), "oidc": {"applicationSlug": plan.application_id, "launchUrl": plan.public_url}, } self._checkpoint( session, enrollment, job, stage="provisioning_identity", evidence=evidence ) oidc_result = self.authentik.ensure_deployment_oidc_application( application_slug=plan.application_id, launch_url=plan.public_url or "", redirect_uri=f"{plan.public_url}{plan.authentication.redirect_path}", client_secret=oidc_secret, administrator_group=trust.administrator_group or "", support_agent_group=trust.support_agent_group or "", read_only_group=trust.read_only_group or "", ) evidence["oidc"] = {key: oidc_result[key] for key in _OIDC_EVIDENCE if key in oidc_result} self._checkpoint( session, enrollment, job, stage="provisioning_identity", evidence=evidence ) if not oidc_result.get("success") or oidc_result.get("status") != "created": return self._fail(session, enrollment, job, stage="provisioning_identity", message="Bounded OIDC application provisioning was not verified.", evidence=evidence) runtime_values.update({ "OIDC_CLIENT_ID": plan.application_id, "OIDC_CLIENT_SECRET": oidc_secret, "OIDC_ISSUER": _application_oidc_issuer(trust.oidc_issuer, plan.application_id) or "", "OIDC_ROLE_CLAIM": trust.oidc_role_claim, }) if plan.profile == "repopulse-authenticated": assert repopulse_gitea_token is not None and repopulse_maintenance_token is not None runtime_values.update({ "REPOPULSE_GITEA_URL": "https://gitea.example.com", "REPOPULSE_GITEA_TOKEN": repopulse_gitea_token, "REPOPULSE_MAINTENANCE_TOKEN": repopulse_maintenance_token, }) if profile and profile["runtimeEnvironmentKeys"]: evidence["intent"] = { **dict(evidence.get("intent") or {}), "runtime": {"applicationId": plan.application_id, "keys": list(runtime_values)}, } self._checkpoint( session, enrollment, job, stage="sealing_runtime", evidence=evidence ) runtime_result = self.broker.configure_enrollment_runtime( { "enrollment_id": enrollment.id, "application_id": plan.application_id, "repository": plan.repository, "values": runtime_values, }, secret=target_secret, ) evidence["runtime"] = {key: runtime_result[key] for key in ("status", "enrollmentId", "applicationId") if key in runtime_result} self._checkpoint( session, enrollment, job, stage="sealing_runtime", evidence=evidence ) if runtime_result.get("status") not in {"configured", "already_configured"}: return self._fail(session, enrollment, job, stage="sealing_runtime", message="Controller could not seal profile-owned runtime values.", evidence=evidence) evidence["intent"] = { **dict(evidence.get("intent") or {}), "policies": [ f"enrollment-{plan.application_id}-deployment-apply", f"enrollment-{plan.application_id}-deployment-rollback", ], } self._checkpoint( session, enrollment, job, stage="installing_policy", evidence=evidence ) try: policy_ids = self._install_policies(session, enrollment, plan) except EnrollmentError as exc: return self._fail(session, enrollment, job, stage="installing_policy", message=str(exc), evidence=evidence) evidence["policies"] = policy_ids enrollment.status = "ready" enrollment.resources_json = { "appdataPath": plan.appdata_path, "dataPath": plan.data_path, "network": plan.network.docker_network, "hostname": plan.exposure.hostname if plan.exposure else None, **evidence, } enrollment.last_verified_at = utc_now() enrollment.updated_at = utc_now() enrollment.completed_at = utc_now() job.status = "ready" job.stage = "ready" job.evidence_json = evidence job.result_summary = "Bounded target, profile resources and exact policies are ready for deployment." job.updated_at = utc_now() job.completed_at = utc_now() session.add(enrollment) session.add(job) session.commit() log_event( session, source="enrollment-plane", identity="operator", action_id="target.enroll", target=plan.application_id, target_type="enrollment", risk="high", policy_decision="allowed", reason="Bounded application identity was enrolled from an autonomous profile.", result="success", execution_plan={"enrollmentId": enrollment.id, "planSha256": plan_digest, "profile": plan.profile}, after_state={"status": "ready", "policyIds": policy_ids}, ) return {"enrollment": self.serialize_enrollment(enrollment), "job": self.serialize_job(job)} def list_enrollments(self, session: Session, *, limit: int = 100) -> list[dict[str, Any]]: rows = session.exec(select(Enrollment).order_by(Enrollment.created_at.desc()).limit(max(1, min(limit, 250)))).all() return [self.serialize_enrollment(row) for row in rows] def get_enrollment(self, session: Session, enrollment_id: str) -> dict[str, Any]: row = session.get(Enrollment, enrollment_id) if row is None: raise EnrollmentError("enrollment was not found") jobs = session.exec( select(EnrollmentJob).where(EnrollmentJob.enrollment_id == enrollment_id).order_by(EnrollmentJob.created_at.desc()).limit(20) ).all() return {"enrollment": self.serialize_enrollment(row), "jobs": [self.serialize_job(job) for job in jobs]}