from __future__ import annotations from sqlmodel import Session, SQLModel, create_engine from backend.app.db.models import Policy from backend.app.schemas.deployment import ( DeploymentPlanV1, validate_compose_runtime_contract, ) from backend.app.services.deployment_control_plane import DeploymentControlPlane from backend.app.services.secret_store import set_secret def _plan(**overrides): payload = { "application_id": "wsbc-portal", "repository": "Jens/Windows-Support-Bundle-Collector", "revision": "a" * 40, "release_version": "0.23.0", "image_repository": "wsbc-portal", "image_tag": "0.23.0", "topology": { "compose_project": "wsbc-portal", "containers": ["wsbc-portal"], "proxy_network": "itworx-npm", "internal_port": 8080, }, "proxy": {"hostname": "support.example.com"}, "oidc": { "application_slug": "wsbc-portal", "client_secret_ref": "WSBC_PORTAL_OIDC_CLIENT_SECRET", "roles": { "administrator_group": "Administrator", "support_agent_group": "SupportAgent", "read_only_group": "ReadOnly", }, }, "broker_secret_ref": "WSBC_PORTAL_DEPLOYMENT_BROKER", } payload.update(overrides) return DeploymentPlanV1.model_validate(payload) class _Broker: def preflight(self, plan, *, secret): assert secret == "a" * 32 return {"status": "preflight_ok"} def apply(self, plan, *, secret, idempotency_key): assert secret == "a" * 32 return {"status": "accepted", "request": "b" * 64} def rollback(self, plan, *, secret, idempotency_key): return {"status": "accepted", "request": "c" * 64} def status(self, request_id, *, secret): return {"status": "deployed", "request": request_id} class _Portainer: def list_containers(self): return {"status": "ok", "containers": []} class _Npm: def proxy_hosts(self): return {"success": True, "items": []} def ensure_managed_proxy_host(self, **kwargs): assert kwargs["forward_host"] == "wsbc-portal" return {"success": True, "status": "created", "hostId": 42} class _Authentik: def test_connection(self): return {"status": "connected"} def applications(self): return {"success": True, "items": []} def ensure_deployment_oidc_application(self, **kwargs): assert kwargs["application_slug"] == "wsbc-portal" assert kwargs["client_secret"] == "b" * 32 return {"success": True, "status": "created", "applicationId": "app-1"} class _Unraid: def get_share_summary(self): return {"status": "ok"} def _session(tmp_path): engine = create_engine(f"sqlite:///{tmp_path / 'deployment.db'}") SQLModel.metadata.create_all(engine) return Session(engine) def _plane(): return DeploymentControlPlane( broker=_Broker(), portainer=_Portainer(), npm=_Npm(), authentik=_Authentik(), unraid=_Unraid(), ) def test_plan_rejects_latest_tag_and_raw_secret_like_fields(): raw = _plan().model_dump() raw["image_tag"] = "latest" try: DeploymentPlanV1.model_validate(raw) except ValueError as exc: assert "non-latest" in str(exc) else: # pragma: no cover - assertion clarity raise AssertionError("latest tag must be rejected") raw = _plan().model_dump() raw["deployment_secret"] = "not-allowed" try: DeploymentPlanV1.model_validate(raw) except ValueError as exc: assert "Extra inputs" in str(exc) else: # pragma: no cover - assertion clarity raise AssertionError("raw secret fields must be rejected") raw = _plan().model_dump() raw["proxy"]["force_ssl"] = False try: DeploymentPlanV1.model_validate(raw) except ValueError as exc: assert "force HTTPS" in str(exc) else: # pragma: no cover - assertion clarity raise AssertionError("managed proxy must force HTTPS") def test_preflight_fails_closed_without_exact_target_policy(tmp_path): plan = _plan(oidc=None) with _session(tmp_path) as session: set_secret(session, "WSBC_PORTAL_DEPLOYMENT_BROKER", "a" * 32) result = _plane().execute( session, plan=plan, idempotency_key="preflight-without-policy-0001", operation="preflight", ) assert result["status"] == "preflight_failed" assert "deployment_policy" in result["preflight"]["blockers"] def test_preflight_rejects_wildcard_deployment_policy(tmp_path): plan = _plan(oidc=None) with _session(tmp_path) as session: set_secret(session, "WSBC_PORTAL_DEPLOYMENT_BROKER", "a" * 32) session.add(Policy( id="wildcard-deploy", action="deployment.apply", target="*", voice="blocked", manual="allowed", risk="high", reason="Wildcard must not authorize a production deployment.", )) session.commit() result = _plane().execute( session, plan=plan, idempotency_key="preflight-wildcard-policy-0001", operation="preflight", ) assert result["status"] == "preflight_failed" assert "deployment_policy" in result["preflight"]["blockers"] def test_compose_contract_rejects_docker_socket_and_unowned_mounts(): plan = _plan(oidc=None) unsafe = """ services: wsbc-portal: image: wsbc-portal:0.23.0 restart: unless-stopped read_only: true cap_drop: [ALL] volumes: - /var/run/docker.sock:/var/run/docker.sock networks: [itworx-npm] networks: itworx-npm: external: true """ try: validate_compose_runtime_contract(plan, unsafe) except ValueError as exc: assert "docker socket" in str(exc) else: # pragma: no cover - assertion clarity raise AssertionError("docker socket mount must be rejected") def test_compose_contract_rejects_path_traversal_outside_owned_appdata(): plan = _plan(oidc=None) unsafe = """ services: wsbc-portal: image: wsbc-portal:0.23.0 restart: unless-stopped read_only: true cap_drop: [ALL] volumes: - /srv/example/appdata/wsbc-portal/../../other-app:/data networks: [itworx-npm] networks: itworx-npm: external: true """ try: validate_compose_runtime_contract(plan, unsafe) except ValueError as exc: assert "normalized" in str(exc) else: # pragma: no cover - assertion clarity raise AssertionError("path traversal mount must be rejected") def test_compose_contract_requires_an_owned_persistent_bind_mount(): plan = _plan(oidc=None) unsafe = """ services: wsbc-portal: image: wsbc-portal:0.23.0 restart: unless-stopped read_only: true cap_drop: [ALL] networks: [itworx-npm] networks: itworx-npm: external: true """ try: validate_compose_runtime_contract(plan, unsafe) except ValueError as exc: assert "persistent" in str(exc) else: # pragma: no cover - assertion clarity raise AssertionError("persistent bind mount must be required") def test_preflight_and_apply_are_durable_and_idempotent(tmp_path): plan = _plan() with _session(tmp_path) as session: set_secret(session, "WSBC_PORTAL_DEPLOYMENT_BROKER", "a" * 32) set_secret(session, "WSBC_PORTAL_OIDC_CLIENT_SECRET", "b" * 32) session.add(Policy( id="allow-wsbc-deploy", action="deployment.apply", target="wsbc-portal", voice="blocked", manual="allowed", risk="high", reason="Test exact-target deployment policy.", )) session.commit() plane = _plane() result = plane.execute(session, plan=plan, idempotency_key="apply-wsbc-0001", operation="apply") assert not result["preflight"]["blockers"], result["preflight"] assert result["status"] == "applying", result assert result["brokerRequestId"] == "b" * 64 replay = plane.execute(session, plan=plan, idempotency_key="apply-wsbc-0001", operation="apply") assert replay["idempotentReplay"] is True assert replay["id"] == result["id"] def test_verified_controller_apply_completes_only_after_typed_oidc_and_proxy(tmp_path): class DirectBroker(_Broker): def apply(self, plan, *, secret, idempotency_key): return {"status": "deployed", "request": "b" * 64} plan = _plan() with _session(tmp_path) as session: set_secret(session, "WSBC_PORTAL_DEPLOYMENT_BROKER", "a" * 32) set_secret(session, "WSBC_PORTAL_OIDC_CLIENT_SECRET", "b" * 32) session.add(Policy( id="allow-wsbc-deploy-direct", action="deployment.apply", target="wsbc-portal", voice="blocked", manual="allowed", risk="high", reason="Test exact-target deployment policy.", )) session.commit() plane = DeploymentControlPlane( broker=DirectBroker(), portainer=_Portainer(), npm=_Npm(), authentik=_Authentik(), unraid=_Unraid(), ) result = plane.execute(session, plan=plan, idempotency_key="apply-wsbc-direct-0001", operation="apply") assert result["status"] == "succeeded" assert result["stage"] == "controller_and_edge_verified" assert result["evidence"]["externalIntegration"]["oidc"]["status"] == "created" assert result["evidence"]["externalIntegration"]["proxy"]["status"] == "created" def test_deployment_evidence_uses_allowlists_and_never_persists_connector_secrets(tmp_path): class DirectBroker(_Broker): def apply(self, plan, *, secret, idempotency_key): return {"status": "deployed", "request": "b" * 64, "token": "broker-secret"} class SecretEchoNpm(_Npm): def ensure_managed_proxy_host(self, **kwargs): return {"success": True, "status": "created", "hostId": 42, "meta": {"token": "npm-secret"}} class SecretEchoAuthentik(_Authentik): def ensure_deployment_oidc_application(self, **kwargs): return {"success": True, "status": "created", "applicationId": "app-1", "client_secret_value": "oidc-secret"} plan = _plan() with _session(tmp_path) as session: set_secret(session, "WSBC_PORTAL_DEPLOYMENT_BROKER", "a" * 32) set_secret(session, "WSBC_PORTAL_OIDC_CLIENT_SECRET", "b" * 32) session.add(Policy( id="allow-wsbc-deploy-evidence", action="deployment.apply", target="wsbc-portal", voice="blocked", manual="allowed", risk="high", reason="Test exact-target deployment policy.", )) session.commit() result = DeploymentControlPlane( broker=DirectBroker(), portainer=_Portainer(), npm=SecretEchoNpm(), authentik=SecretEchoAuthentik(), unraid=_Unraid(), ).execute(session, plan=plan, idempotency_key="apply-wsbc-evidence-0001", operation="apply") assert result["status"] == "succeeded" assert "broker-secret" not in str(result) assert "npm-secret" not in str(result) assert "oidc-secret" not in str(result)