from __future__ import annotations import hashlib import json import re import uuid from typing import Any from sqlmodel import Session, select from ..core.time import utc_now from ..db.models import DeploymentJob, Policy from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1 from .audit_logger import log_event from .deployment_broker import DeploymentBrokerClient from .enrollment_plane import EnrollmentPlane from .forge_bootstrap_apply import ( _FORGE_SOURCE_URL, _protected_forge_private_package_owner, canonical_preflight_receipt, forge_oidc_attestation_matches, ) from .policy_engine import evaluate_policy from .secret_store import get_secret, has_secret _API_VERSION = "itworx.forge-bootstrap-preflight/v1" _APPLICATION_ID = "itworx-forge" _REPOSITORY = "jens/itworx-forge" _IDEMPOTENCY = re.compile(r"^[A-Za-z0-9._:-]{8,128}$") _CONTROLLER_CHECKS = { "exact_main", "source_manifest", "managed_appdata", "proxy_network", "resource_collisions", "push_validation", "image_registry", "oidc", "secret_delivery", } _DETAILS = { "deployment_apply_policy": "An explicit allowed exact-target deployment policy is required before apply.", "controller_trust": "Stored controller trust and bootstrap secret are required for host checks.", "managed_appdata_root_selection": "The managed Docker-root backup acknowledgement and storage selection are required before setup.", "exact_main": "Requested Forge revision must equal the current main head.", "source_manifest": "The source manifest must match the fixed Forge six-service topology.", "managed_appdata": "The fixed managed PostgreSQL directory must have safe owner-only metadata.", "proxy_network": "The required external proxy network must be present.", "resource_collisions": "Fixed Forge container names must be free for first installation.", "push_validation": "Exact-main CI validation cannot be verified from trusted controller configuration.", "image_registry": "Registry publication and pull authorization require external verification.", "oidc": "Dedicated Forge Authentik provider, application, and role groups require external verification.", "secret_delivery": "Forge database and application secret files require external verification.", } _STATUS_DETAILS = { "exact_main": { "passed": "Requested revision equals the current protected Gitea main head.", "blocked": "Requested revision is not the current protected Gitea main head.", "unknown": "Current protected Gitea main head could not be verified.", }, "source_manifest": { "passed": "Committed Forge manifest matches the pinned production topology contract.", "blocked": "Committed Forge manifest differs from the pinned production topology contract.", "unknown": "Committed Forge manifest could not be read from the controller mirror.", }, "managed_appdata": { "passed": "Selected managed root and fixed PostgreSQL directory have safe metadata.", "blocked": "Managed root or fixed PostgreSQL directory is missing or unsafe.", "unknown": "Managed storage metadata could not be verified.", }, "proxy_network": { "passed": "The required external proxy network exists.", "blocked": "The required external proxy network is missing.", "unknown": "Docker network inventory is unavailable.", }, "resource_collisions": { "passed": "No fixed Forge container-name collisions were found.", "blocked": "One or more fixed Forge container names already exist.", "unknown": "Docker container inventory is unavailable.", }, "push_validation": { "passed": "Configured required push validation contexts passed for this exact commit.", "blocked": "A configured required push validation context rejected this exact commit.", "unknown": "Exact-commit push validation is not configured or cannot be verified.", }, "image_registry": { "passed": "The fixed private Forge image owner and both immutable image digests are authenticated and pullable.", "blocked": "One or both immutable Forge image digests are unavailable from the registry.", "unknown": "Registry publication or pull authorization could not be verified.", }, "oidc": { "passed": "The fixed Forge OIDC provider, application, and role groups are ready.", "blocked": "The Forge OIDC proof is missing, mismatched, or differs from the protected AppOps mapping and signing-key IDs.", "unknown": "The fixed Forge OIDC configuration could not be verified.", }, "secret_delivery": { "passed": "The fixed Forge secret files have the required safe metadata.", "blocked": "One or more fixed Forge secret files are missing or unsafe.", "unknown": "The fixed Forge secret-file metadata could not be verified.", }, } class ForgeBootstrapPreflightError(ValueError): """A fixed-target Forge preflight request failed closed.""" def _canonical_digest(value: dict[str, Any]) -> str: return hashlib.sha256( json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") ).hexdigest() def _idempotency_digest(value: str) -> str: normalized = value.strip() if not _IDEMPOTENCY.fullmatch(normalized): raise ForgeBootstrapPreflightError("Idempotency-Key must contain 8 to 128 safe characters.") return hashlib.sha256(f"forge-bootstrap-preflight:{normalized}".encode()).hexdigest() def _check(name: str, status: str) -> dict[str, str]: detail = _STATUS_DETAILS.get(name, {}).get(status, _DETAILS[name]) return {"name": name, "status": status, "detail": detail} def _checked_controller_result( result: object, profile: ForgeBootstrapPreflightV1 ) -> tuple[list[dict[str, str]], dict[str, Any]] | None: if not isinstance(result, dict) or ( result.get("apiVersion") != _API_VERSION or result.get("revision") != profile.revision or result.get("workspaceImageDigest") != profile.workspace_image_digest or result.get("frontdoorImageDigest") != profile.frontdoor_image_digest ): return None raw_checks = result.get("checks") if not isinstance(raw_checks, dict) or set(raw_checks) != _CONTROLLER_CHECKS: return None checks: list[dict[str, str]] = [] for name in sorted(_CONTROLLER_CHECKS): raw = raw_checks.get(name) if not isinstance(raw, dict) or set(raw) != {"status"} or raw.get("status") not in {"passed", "blocked", "unknown"}: return None checks.append(_check(name, raw["status"])) statuses = [item["status"] for item in checks] expected_status = "preflight_blocked" if "blocked" in statuses else "preflight_unknown" if "unknown" in statuses else "preflight_ok" if result.get("status") != expected_status: return None raw_evidence = result.get("evidence", {}) if not isinstance(raw_evidence, dict) or set(raw_evidence) - { "mainRevision", "sourceManifestSha256", "resourceCollisionCount", "workspaceImagePresent", "frontdoorImagePresent", "attestations", }: return None evidence: dict[str, Any] = {} for name in ("mainRevision", "sourceManifestSha256"): value = raw_evidence.get(name) if value is not None: pattern = r"^[0-9a-f]{40}$" if name == "mainRevision" else r"^[0-9a-f]{64}$" if not isinstance(value, str) or not re.fullmatch(pattern, value): return None evidence[name] = value count = raw_evidence.get("resourceCollisionCount") if count is not None: if isinstance(count, bool) or not isinstance(count, int) or not 0 <= count <= 1000: return None evidence["resourceCollisionCount"] = count for name in ("workspaceImagePresent", "frontdoorImagePresent"): value = raw_evidence.get(name) if value is not None: if not isinstance(value, bool): return None evidence[name] = value attestations = raw_evidence.get("attestations") oidc_rejected = False validated_attestations: dict[str, dict[str, Any]] = {} if attestations is not None: if not isinstance(attestations, dict) or set(attestations) - { "push_validation", "image_registry", "oidc", "secret_delivery", }: return None push = attestations.get("push_validation") if push is not None and ( not isinstance(push, dict) or set(push) != {"context", "conclusion", "revision"} or push.get("context") != "Managed validation / full (push)" or push.get("conclusion") != "success" or push.get("revision") != profile.revision ): return None if push is not None: validated_attestations["push_validation"] = dict(push) registry = attestations.get("image_registry") private_owner = _protected_forge_private_package_owner() if registry is not None and ( not isinstance(registry, dict) or private_owner is None or registry.get("workspacePullable") is not True or registry.get("frontdoorPullable") is not True or registry != { "owner": private_owner, "ownerVisibility": "private", "workspaceDigest": profile.workspace_image_digest, "frontdoorDigest": profile.frontdoor_image_digest, "workspacePullable": True, "frontdoorPullable": True, "workspaceSource": _FORGE_SOURCE_URL, "workspaceRevision": profile.revision, "frontdoorSource": _FORGE_SOURCE_URL, "frontdoorRevision": profile.revision, } ): return None if registry is not None: validated_attestations["image_registry"] = dict(registry) oidc = attestations.get("oidc") if oidc is not None: if raw_checks["oidc"]["status"] != "passed": return None if forge_oidc_attestation_matches(oidc): validated_attestations["oidc"] = dict(oidc) else: oidc_rejected = True elif raw_checks["oidc"]["status"] == "passed": oidc_rejected = True secret_delivery = attestations.get("secret_delivery") if secret_delivery is not None and ( not isinstance(secret_delivery, dict) or set(secret_delivery) != {"requiredFilesReady", "metadataSha256"} or secret_delivery.get("requiredFilesReady") is not True or not isinstance(secret_delivery.get("metadataSha256"), str) or not re.fullmatch(r"^[0-9a-f]{64}$", secret_delivery["metadataSha256"]) ): return None if secret_delivery is not None: validated_attestations["secret_delivery"] = dict(secret_delivery) if any( raw_checks[name]["status"] == "passed" and (not isinstance(attestations, dict) or name not in attestations) for name in ("push_validation", "image_registry", "secret_delivery") ): return None if raw_checks["oidc"]["status"] == "passed" and "oidc" not in validated_attestations: oidc_rejected = True if oidc_rejected: for check in checks: if check["name"] == "oidc": check.update(_check("oidc", "blocked")) break if validated_attestations: evidence["attestations"] = validated_attestations return checks, evidence class ForgeBootstrapPreflightService: """Read-only fixed Forge checks; this profile has no apply or rollback path.""" def __init__(self, *, broker: DeploymentBrokerClient | None = None): self.broker = broker or DeploymentBrokerClient() def preflight( self, session: Session, profile: ForgeBootstrapPreflightV1, *, idempotency_key: str, ) -> dict[str, Any]: payload = profile.model_dump(mode="json") plan_digest = _canonical_digest(payload) key_digest = _idempotency_digest(idempotency_key) existing = session.exec( select(DeploymentJob).where(DeploymentJob.idempotency_sha256 == key_digest) ).first() if existing: if existing.plan_sha256 != plan_digest: raise ForgeBootstrapPreflightError( "Idempotency-Key was already used for a different Forge profile." ) return self.serialize_job(existing, replay=True) exact = session.exec( select(Policy).where( (Policy.action == "deployment.apply") & (Policy.target == _APPLICATION_ID) ) ).all() decision = evaluate_policy( session, action="deployment.apply", target=_APPLICATION_ID, source="manual" ) policy_ready = bool( len(exact) == 1 and exact[0].manual == "allowed" and exact[0].voice == "blocked" and exact[0].risk == "high" and decision.get("decision") == "allowed" and decision.get("policy_id") == exact[0].id ) checks = [_check("deployment_apply_policy", "passed" if policy_ready else "blocked")] trust = EnrollmentPlane.get_trust_config(session) expected_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data" secret = None if trust is not None: try: if has_secret(session, trust.bootstrap_secret_ref): secret = get_secret(session, trust.bootstrap_secret_ref, touch=False) except Exception: # noqa: BLE001 - fail closed without exposing secret-store errors. secret = None bootstrap_ready = bool( trust and isinstance(secret, str) and len(secret) >= 32 and secret.isascii() and secret.isprintable() ) checks.append(_check("controller_trust", "passed" if bootstrap_ready else "blocked")) root_ready = bool( trust and trust.trusted_appdata_root == expected_root and trust.managed_appdata_backup_acknowledged ) checks.append( _check("managed_appdata_root_selection", "passed" if root_ready else "blocked") ) controller_result = None if bootstrap_ready: try: controller_result = self.broker.forge_bootstrap_preflight( profile, secret=secret or "" ) except Exception: # noqa: BLE001 - transport details stay out of the preflight receipt. controller_result = None parsed_controller = _checked_controller_result(controller_result, profile) if parsed_controller is None: host_checks = [_check(name, "unknown") for name in sorted(_CONTROLLER_CHECKS)] controller_evidence: dict[str, Any] = {} else: host_checks, controller_evidence = parsed_controller checks.extend(host_checks) blockers = [item["name"] for item in checks if item["status"] == "blocked"] unknowns = [item["name"] for item in checks if item["status"] == "unknown"] outcome = "blocked" if blockers else "unknown" if unknowns else "passed" job_status = "preflight_succeeded" if outcome == "passed" else "preflight_failed" now = utc_now() job = DeploymentJob( id=f"forge-preflight-{uuid.uuid4().hex[:16]}", application_id=_APPLICATION_ID, repository=_REPOSITORY, revision=profile.revision, plan_sha256=plan_digest, idempotency_sha256=key_digest, status=job_status, stage="preflight_complete" if outcome == "passed" else "preflight_incomplete", plan_json=payload, preflight_json={ "apiVersion": _API_VERSION, "status": outcome, "blockers": blockers, "unknowns": unknowns, "checks": checks, }, evidence_json={ "profile": "forge-production-bootstrap", "policyId": exact[0].id if policy_ready else None, "workspaceImageDigest": profile.workspace_image_digest, "frontdoorImageDigest": profile.frontdoor_image_digest, "controller": controller_evidence, }, result_summary=( "Forge bootstrap checks passed; production apply remains separately gated." if outcome == "passed" else "Forge bootstrap preflight is incomplete; see blocked and unknown checks." ), created_at=now, updated_at=now, completed_at=now, ) session.add(job) session.commit() session.refresh(job) log_event( session, source="deployment-control-plane", identity="operator", action_id="deployment.preflight", target=_APPLICATION_ID, target_type="deployment", risk="high", policy_decision="allowed" if policy_ready else "blocked", reason="Fixed Forge bootstrap preflight recorded without requesting a deployment action.", result="success" if outcome == "passed" else "blocked", execution_plan={"planSha256": plan_digest, "profile": "forge-production-bootstrap"}, post_checks={"preflight": job.preflight_json}, ) return self.serialize_job(job) @staticmethod def serialize_job(job: DeploymentJob, *, replay: bool = False) -> dict[str, Any]: return { "id": job.id, "applicationId": job.application_id, "repository": job.repository, "revision": job.revision, "status": job.status, "stage": job.stage, "preflight": job.preflight_json, "preflightSha256": canonical_preflight_receipt(job), "evidence": job.evidence_json, "result": job.result_summary, "brokerRequestId": job.broker_request_id, "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 refresh(session: Session, job_id: str) -> dict[str, Any] | None: job = session.get(DeploymentJob, job_id) if not job or not job.id.startswith("forge-preflight-"): return None return ForgeBootstrapPreflightService.serialize_job(job) @staticmethod def list_jobs(session: Session, *, limit: int = 100) -> list[dict[str, Any]]: rows = session.exec( select(DeploymentJob) .where(DeploymentJob.id.startswith("forge-preflight-")) .order_by(DeploymentJob.created_at.desc()) .limit(max(1, min(limit, 250))) ).all() return [ForgeBootstrapPreflightService.serialize_job(row) for row in rows]