970 lines
40 KiB
Python
970 lines
40 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlmodel import Session, SQLModel, create_engine, select
|
|
|
|
from backend.app.db.models import Enrollment, EnrollmentJob, Policy
|
|
from backend.app.schemas.enrollment import EnrollmentPlanV1, EnrollmentTrustConfigV1
|
|
from backend.app.services import enrollment_plane as enrollment_plane_module
|
|
from backend.app.services.enrollment_plane import EnrollmentPlane
|
|
from backend.app.services.secret_store import get_secret, set_secret
|
|
|
|
|
|
def _plan(**overrides):
|
|
value = {
|
|
"application_id": "wsbc-portal",
|
|
"display_name": "Windows Support Bundle Collector",
|
|
"repository": "Jens/Windows-Support-Bundle-Collector",
|
|
"revision": "a" * 40,
|
|
"profile": "internal-webapp-oidc",
|
|
"target": {"target_identity": "unraid-controller", "container_name": "wsbc-portal"},
|
|
"storage": {"runtime_uid": 1654, "runtime_gid": 1654},
|
|
"network": {"docker_network": "itworx-npm", "internal_port": 8080},
|
|
"artifact": {"release_version": "0.23.0", "image_repository": "wsbc-portal", "image_tag": "0.23.0"},
|
|
"exposure": {"hostname": "wsbc.example.com", "upstream_service": "wsbc-portal"},
|
|
"authentication": {
|
|
"mode": "native-oidc",
|
|
"redirect_path": "/signin-oidc",
|
|
"logout_path": "/signout-callback-oidc",
|
|
"roles": {"administrator": "Administrator", "support_agent": "SupportAgent", "read_only": "ReadOnly"},
|
|
},
|
|
"secrets": {"oidc_client_secret_required": True},
|
|
}
|
|
value.update(overrides)
|
|
return EnrollmentPlanV1.model_validate(value)
|
|
|
|
|
|
def _repopulse_plan(**overrides):
|
|
value = {
|
|
"application_id": "repopulse",
|
|
"display_name": "RepoPulse",
|
|
"environment": "production",
|
|
"repository": "Jens/RepoPulse",
|
|
"revision": "c" * 40,
|
|
"profile": "repopulse-authenticated",
|
|
"target": {"target_identity": "unraid-controller", "container_name": "repopulse"},
|
|
"storage": {"runtime_uid": 10001, "runtime_gid": 10001},
|
|
"network": {"docker_network": "itworx-mcp-hub-npm", "internal_port": 8000},
|
|
"artifact": {
|
|
"release_version": "1.5.0",
|
|
"image_repository": "repopulse-managed",
|
|
"image_tag": "production",
|
|
"health_path": "/health",
|
|
},
|
|
"exposure": {"hostname": "repopulse.example.com", "upstream_service": "repopulse"},
|
|
"secrets": {"additional_secret_refs": ["REPOPULSE_GITEA_TOKEN"]},
|
|
}
|
|
value.update(overrides)
|
|
return EnrollmentPlanV1.model_validate(value)
|
|
|
|
|
|
def _trust():
|
|
return EnrollmentTrustConfigV1.model_validate({
|
|
"controller_identity": "unraid-controller",
|
|
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
|
|
"approved_networks": ["itworx-npm", "itworx-mcp-hub-npm"],
|
|
"allowed_domain_suffixes": ["example.com"],
|
|
"autonomous_profiles": ["internal-webapp-oidc", "internal-webapp", "internal-service", "repopulse-authenticated"],
|
|
"repopulse_proxy_access_list_id": 42,
|
|
"oidc_issuer": "https://auth.example.com/application/o/wsbc-portal",
|
|
"administrator_group": "Administrator",
|
|
"support_agent_group": "SupportAgent",
|
|
"read_only_group": "ReadOnly",
|
|
})
|
|
|
|
|
|
class _Broker:
|
|
appdata_root = "/srv/example/appdata"
|
|
|
|
def is_configured(self):
|
|
return True
|
|
|
|
def enrollment_capabilities(self, *, secret):
|
|
assert secret == "b" * 64
|
|
return {
|
|
"status": "ready",
|
|
"trustedAppdataRoot": self.appdata_root,
|
|
"features": {
|
|
"deploymentApply": True,
|
|
"deploymentVerify": True,
|
|
"rollback": True,
|
|
"exactTargetPolicy": True,
|
|
"hmacV2": True,
|
|
"replayProtection": True,
|
|
"enrollment": True,
|
|
"targetRegistration": True,
|
|
"runtimeSecretSealing": True,
|
|
},
|
|
}
|
|
|
|
def enrollment_preflight(self, enrollment, *, secret):
|
|
assert secret == "b" * 64
|
|
assert "target_credential" not in enrollment
|
|
return {"status": "preflight_ok"}
|
|
|
|
def enroll_target(self, enrollment, *, secret, idempotency_key):
|
|
assert secret == "b" * 64
|
|
assert len(enrollment["target_credential"]) >= 48
|
|
assert enrollment["application_id"] == "wsbc-portal"
|
|
return {"status": "enrolled", "targetId": enrollment["enrollment_id"], "applicationId": "wsbc-portal"}
|
|
|
|
def configure_enrollment_runtime(self, runtime, *, secret):
|
|
assert len(secret) >= 48
|
|
assert set(runtime["values"]) == {"APPOPS_PUBLIC_URL", "OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET", "OIDC_ISSUER", "OIDC_ROLE_CLAIM"}
|
|
return {"status": "configured", "enrollmentId": runtime["enrollment_id"], "applicationId": "wsbc-portal"}
|
|
|
|
|
|
class _Portainer:
|
|
def list_containers(self):
|
|
return {"status": "ok", "containers": []}
|
|
|
|
|
|
class _CollisionPortainer(_Portainer):
|
|
def list_containers(self):
|
|
return {"status": "ok", "containers": [{"name": "wsbc-portal"}]}
|
|
|
|
|
|
class _Npm:
|
|
def is_configured(self):
|
|
return True
|
|
|
|
def proxy_hosts(self):
|
|
return {"success": True, "items": []}
|
|
|
|
def ensure_managed_proxy_host(self, **kwargs):
|
|
assert kwargs["hostname"] == "wsbc.example.com"
|
|
assert kwargs["certificate_mode"] == "managed"
|
|
return {"success": True, "status": "created", "hostId": 10, "hostname": kwargs["hostname"]}
|
|
|
|
|
|
class _RepoPulseNpm(_Npm):
|
|
def __init__(self, *, access_list_ok=True):
|
|
self.access_list_ok = access_list_ok
|
|
self.validated_ids = []
|
|
self.created_host = None
|
|
|
|
def validate_managed_access_list(self, access_list_id):
|
|
self.validated_ids.append(access_list_id)
|
|
return {
|
|
"success": self.access_list_ok,
|
|
"status": "verified" if self.access_list_ok else "blocked",
|
|
"message": "required_access_list_verified" if self.access_list_ok else "required_access_list_unavailable",
|
|
**({"accessListId": access_list_id} if self.access_list_ok else {}),
|
|
}
|
|
|
|
def ensure_managed_proxy_host(self, **kwargs):
|
|
assert kwargs["hostname"] == "repopulse.example.com"
|
|
assert kwargs["forward_host"] == "repopulse"
|
|
assert kwargs["forward_port"] == 8000
|
|
assert kwargs["access_list_id"] == 42
|
|
self.created_host = dict(kwargs)
|
|
return {"success": True, "status": "created", "hostId": 10, "hostname": kwargs["hostname"]}
|
|
|
|
|
|
class _RepoPulseBroker(_Broker):
|
|
def __init__(self):
|
|
self.runtime_values = None
|
|
|
|
def enroll_target(self, enrollment, *, secret, idempotency_key):
|
|
assert secret == "b" * 64
|
|
assert enrollment["application_id"] == "repopulse"
|
|
assert enrollment["repository"] == "jens/repopulse"
|
|
assert enrollment["profile"] == "repopulse-authenticated"
|
|
assert enrollment["network"] == "itworx-mcp-hub-npm"
|
|
assert enrollment["runtime_uid"] == enrollment["runtime_gid"] == 10001
|
|
assert enrollment["image_repository"] == "repopulse-managed"
|
|
assert enrollment["image_tag"] == "production"
|
|
return {"status": "enrolled", "targetId": enrollment["enrollment_id"], "applicationId": "repopulse"}
|
|
|
|
def configure_enrollment_runtime(self, runtime, *, secret):
|
|
assert len(secret) >= 48
|
|
self.runtime_values = dict(runtime["values"])
|
|
assert set(self.runtime_values) == {
|
|
"REPOPULSE_GITEA_URL", "REPOPULSE_GITEA_TOKEN", "REPOPULSE_MAINTENANCE_TOKEN",
|
|
}
|
|
return {"status": "configured", "enrollmentId": runtime["enrollment_id"], "applicationId": "repopulse"}
|
|
|
|
|
|
class _Authentik:
|
|
def is_configured(self):
|
|
return True
|
|
|
|
def test_connection(self):
|
|
return {"status": "connected"}
|
|
|
|
def applications(self):
|
|
return {"success": True, "items": []}
|
|
|
|
def groups(self):
|
|
return {"success": True, "items": [{"name": "Administrator"}, {"name": "SupportAgent"}, {"name": "ReadOnly"}]}
|
|
|
|
def deployment_oidc_template_status(self, *, role_claim):
|
|
assert role_claim == "roles"
|
|
return {"status": "ready", "missing": []}
|
|
|
|
def ensure_deployment_oidc_application(self, **kwargs):
|
|
assert kwargs["application_slug"] == "wsbc-portal"
|
|
assert kwargs["client_secret"]
|
|
return {"success": True, "status": "created", "applicationId": "application-1", "providerId": "provider-1"}
|
|
|
|
|
|
def _session(tmp_path):
|
|
engine = create_engine(f"sqlite:///{tmp_path / 'enrollment.db'}")
|
|
SQLModel.metadata.create_all(engine)
|
|
return Session(engine)
|
|
|
|
|
|
def _plane(portainer=None, broker=None, npm=None, authentik=None):
|
|
return EnrollmentPlane(
|
|
broker=broker or _Broker(),
|
|
portainer=portainer or _Portainer(),
|
|
npm=npm or _Npm(),
|
|
authentik=authentik or _Authentik(),
|
|
)
|
|
|
|
|
|
class _CompensatingBroker(_Broker):
|
|
def unenroll_target(self, _target, *, secret):
|
|
assert secret == "b" * 64
|
|
return {"status": "rolled_back"}
|
|
|
|
|
|
class _CompensatingNpm(_Npm):
|
|
def remove_managed_proxy_host(self, *, host_id, ownership_id):
|
|
return {"success": True, "status": "removed"}
|
|
|
|
|
|
class _CompensatingAuthentik(_Authentik):
|
|
def remove_deployment_oidc_application(self, **_kwargs):
|
|
return {"success": True, "status": "removed"}
|
|
|
|
|
|
def _compensating_plane(broker=None):
|
|
return _plane(
|
|
broker=broker or _CompensatingBroker(),
|
|
npm=_CompensatingNpm(),
|
|
authentik=_CompensatingAuthentik(),
|
|
)
|
|
|
|
|
|
def _configure(session):
|
|
set_secret(session, "ENROLLMENT_BOOTSTRAP", "b" * 64)
|
|
EnrollmentPlane.set_trust_config(session, _trust())
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _production_grade_secret_store(monkeypatch):
|
|
"""Plane unit tests exercise enrollment behavior with a managed key store."""
|
|
monkeypatch.setattr(
|
|
enrollment_plane_module,
|
|
"secret_storage_status",
|
|
lambda: {"status": "ready", "productionReady": True},
|
|
)
|
|
|
|
|
|
def test_enrollment_schema_rejects_plaintext_secrets_and_untrusted_topology():
|
|
raw = _plan().model_dump()
|
|
raw["target"]["command"] = "id"
|
|
try:
|
|
EnrollmentPlanV1.model_validate(raw)
|
|
except ValueError as exc:
|
|
assert "Extra inputs" in str(exc)
|
|
else: # pragma: no cover - assertion clarity
|
|
raise AssertionError("arbitrary target command must be rejected")
|
|
|
|
raw = _plan().model_dump()
|
|
raw["storage"]["appdata_subpath"] = "../etc"
|
|
try:
|
|
EnrollmentPlanV1.model_validate(raw)
|
|
except ValueError as exc:
|
|
assert "subpath" in str(exc)
|
|
else: # pragma: no cover - assertion clarity
|
|
raise AssertionError("appdata traversal must be rejected")
|
|
|
|
raw = _plan().model_dump()
|
|
raw["exposure"]["hostname"] = "*.example.com"
|
|
try:
|
|
EnrollmentPlanV1.model_validate(raw)
|
|
except ValueError as exc:
|
|
assert "hostname" in str(exc)
|
|
else: # pragma: no cover - assertion clarity
|
|
raise AssertionError("wildcard hostname must be rejected")
|
|
|
|
|
|
def test_appdata_root_defaults_to_legacy_and_allows_only_the_fixed_managed_root():
|
|
plan = _plan()
|
|
assert plan.trusted_appdata_root == "/srv/example/appdata"
|
|
assert plan.appdata_path == "/srv/example/appdata/wsbc-portal"
|
|
|
|
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
managed = _plan(trusted_appdata_root=managed_root)
|
|
assert managed.appdata_path == managed_root + "/wsbc-portal"
|
|
|
|
plan_payload = _plan().model_dump()
|
|
plan_payload["trusted_appdata_root"] = "/srv/example/other"
|
|
trust_payload = _trust().model_dump()
|
|
trust_payload["trusted_appdata_root"] = "/srv/example/other"
|
|
with pytest.raises(ValueError):
|
|
EnrollmentPlanV1.model_validate(plan_payload)
|
|
with pytest.raises(ValueError):
|
|
EnrollmentTrustConfigV1.model_validate(trust_payload)
|
|
|
|
|
|
def test_repopulse_profile_accepts_only_its_fixed_authenticated_target():
|
|
plan = _repopulse_plan()
|
|
assert plan.application_id == "repopulse"
|
|
assert plan.repository == "jens/repopulse"
|
|
assert plan.network.docker_network == "itworx-mcp-hub-npm"
|
|
assert plan.secrets.additional_secret_refs == ("REPOPULSE_GITEA_TOKEN",)
|
|
|
|
for field, value in (
|
|
("application_id", "another-app"),
|
|
("repository", "Jens/another-app"),
|
|
("network", {"docker_network": "other-network", "internal_port": 8000}),
|
|
("artifact", {"release_version": "1.5.0", "image_repository": "repopulse-managed", "image_tag": "latest", "health_path": "/health"}),
|
|
("exposure", {"hostname": "other.example.com", "upstream_service": "repopulse"}),
|
|
("secrets", {"additional_secret_refs": ["OTHER_TOKEN"]}),
|
|
):
|
|
raw = _repopulse_plan().model_dump(mode="json")
|
|
raw[field] = value
|
|
with pytest.raises(ValueError):
|
|
EnrollmentPlanV1.model_validate(raw)
|
|
|
|
|
|
def test_repopulse_preflight_requires_existing_secret_and_trusted_npm_access_list(tmp_path):
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
npm = _RepoPulseNpm(access_list_ok=False)
|
|
plane = _plane(npm=npm)
|
|
|
|
missing_secret = plane.preflight(session, _repopulse_plan())
|
|
assert missing_secret["status"] == "blocked"
|
|
assert "repopulse_gitea_token_unavailable" in missing_secret["blockers"]
|
|
assert npm.validated_ids == [42]
|
|
|
|
set_secret(session, "REPOPULSE_GITEA_TOKEN", "existing-encrypted-gitea-token")
|
|
bad_access_list = plane.preflight(session, _repopulse_plan())
|
|
assert bad_access_list["status"] == "blocked"
|
|
assert "repopulse_access_list_unavailable" in bad_access_list["blockers"]
|
|
assert session.exec(select(Enrollment)).all() == []
|
|
assert session.exec(select(EnrollmentJob)).all() == []
|
|
|
|
|
|
def test_repopulse_legacy_profile_remains_usable_when_new_target_root_is_managed(tmp_path):
|
|
class RecordingBroker(_RepoPulseBroker):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.preflight_payloads = []
|
|
|
|
def enrollment_preflight(self, enrollment, *, secret):
|
|
self.preflight_payloads.append(dict(enrollment))
|
|
return super().enrollment_preflight(enrollment, secret=secret)
|
|
|
|
broker = RecordingBroker()
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
set_secret(session, "REPOPULSE_GITEA_TOKEN", "opaque-gitea-token")
|
|
EnrollmentPlane.set_trust_config(
|
|
session,
|
|
_trust().model_copy(update={
|
|
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
"managed_appdata_backup_acknowledged": True,
|
|
}),
|
|
)
|
|
result = _plane(broker=broker, npm=_RepoPulseNpm()).preflight(session, _repopulse_plan())
|
|
|
|
assert result["status"] == "ready"
|
|
assert "trusted_appdata_root_mismatch" not in result["blockers"]
|
|
assert broker.preflight_payloads[0]["trusted_appdata_root"] == "/srv/example/appdata"
|
|
|
|
|
|
def test_repopulse_enrollment_seals_exact_runtime_values_without_exposing_secrets(tmp_path):
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
gitea_token = "existing-encrypted-gitea-token"
|
|
set_secret(session, "REPOPULSE_GITEA_TOKEN", gitea_token)
|
|
npm = _RepoPulseNpm()
|
|
broker = _RepoPulseBroker()
|
|
result = _plane(broker=broker, npm=npm).enroll(
|
|
session, _repopulse_plan(), idempotency_key="enroll-repopulse-0001",
|
|
)
|
|
|
|
assert result["enrollment"]["status"] == "ready"
|
|
assert npm.validated_ids == [42]
|
|
assert npm.created_host["access_list_id"] == 42
|
|
assert broker.runtime_values["REPOPULSE_GITEA_URL"] == "https://gitea.example.com"
|
|
assert broker.runtime_values["REPOPULSE_GITEA_TOKEN"] == gitea_token
|
|
maintenance_token = get_secret(session, "ENROLL_REPOPULSE_MAINTENANCE_TOKEN")
|
|
assert maintenance_token and broker.runtime_values["REPOPULSE_MAINTENANCE_TOKEN"] == maintenance_token
|
|
assert gitea_token not in str(result)
|
|
assert maintenance_token not in str(result)
|
|
assert gitea_token not in str(result["job"]["evidence"])
|
|
assert maintenance_token not in str(result["job"]["evidence"])
|
|
|
|
|
|
def test_preflight_is_read_only_and_rejects_existing_container(tmp_path):
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
result = _plane(_CollisionPortainer()).preflight(session, _plan())
|
|
assert result["status"] == "blocked"
|
|
assert "unmanaged_resource_collision" in result["blockers"]
|
|
assert session.exec(select(Enrollment)).all() == []
|
|
assert session.exec(select(EnrollmentJob)).all() == []
|
|
|
|
|
|
def test_preflight_rejects_the_development_secret_fallback(tmp_path, monkeypatch):
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
monkeypatch.setattr(
|
|
enrollment_plane_module,
|
|
"secret_storage_status",
|
|
lambda: {"status": "ready", "productionReady": False},
|
|
)
|
|
result = _plane().preflight(session, _plan())
|
|
assert result["status"] == "blocked"
|
|
assert "secret_store" in result["blockers"]
|
|
assert session.exec(select(Enrollment)).all() == []
|
|
|
|
|
|
def test_capabilities_require_a_live_bootstrap_controller_contract(tmp_path):
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
result = _plane().capabilities(session)
|
|
assert result["targetEnrollment"]["status"] == "ready"
|
|
assert result["controller"]["status"] == "ready"
|
|
|
|
|
|
def test_capabilities_block_when_controller_root_differs_from_operator_trust(tmp_path):
|
|
class AlternateRootBroker(_Broker):
|
|
appdata_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
result = _plane(broker=AlternateRootBroker()).capabilities(session)
|
|
|
|
assert result["targetEnrollment"]["status"] == "unavailable"
|
|
assert result["controller"]["status"] == "ready"
|
|
assert result["controller"]["reason"] == ""
|
|
assert result["enrollmentReadiness"]["reason"] == "trusted_appdata_root_unavailable"
|
|
|
|
|
|
def test_capabilities_treat_roots_as_independent_availability_not_global_selection(tmp_path):
|
|
legacy_root = "/srv/example/appdata"
|
|
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
|
|
class MultiRootBroker(_Broker):
|
|
def enrollment_capabilities(self, *, secret):
|
|
assert secret == "b" * 64
|
|
return {
|
|
"status": "ready",
|
|
"trustedAppdataRoot": managed_root,
|
|
"availableTrustedAppdataRoots": [
|
|
{"root": legacy_root, "status": "ready", "reason": ""},
|
|
{"root": managed_root, "status": "ready", "reason": ""},
|
|
],
|
|
"managedStorage": {"status": "ready", "initializationAvailable": False},
|
|
"features": {
|
|
"deploymentApply": True,
|
|
"deploymentVerify": True,
|
|
"rollback": True,
|
|
"exactTargetPolicy": True,
|
|
"hmacV2": True,
|
|
"replayProtection": True,
|
|
"enrollment": True,
|
|
"targetRegistration": True,
|
|
"runtimeSecretSealing": True,
|
|
},
|
|
}
|
|
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
plane = _plane(broker=MultiRootBroker())
|
|
legacy = plane.capabilities(session)
|
|
assert legacy["targetEnrollment"]["status"] == "ready"
|
|
assert legacy["managedStorage"]["status"] == "not_selected"
|
|
assert legacy["availableTrustedAppdataRoots"] == [
|
|
{"root": legacy_root, "status": "ready"},
|
|
{"root": managed_root, "status": "ready"},
|
|
]
|
|
|
|
EnrollmentPlane.set_trust_config(
|
|
session,
|
|
_trust().model_copy(update={
|
|
"trusted_appdata_root": managed_root,
|
|
"managed_appdata_backup_acknowledged": True,
|
|
}),
|
|
)
|
|
managed = plane.capabilities(session)
|
|
|
|
assert managed["targetEnrollment"]["status"] == "ready"
|
|
assert managed["managedStorage"]["status"] == "ready"
|
|
assert managed["availableTrustedAppdataRoots"] == [
|
|
{"root": legacy_root, "status": "ready"},
|
|
{"root": managed_root, "status": "ready"},
|
|
]
|
|
|
|
|
|
def test_runtime_capabilities_remain_available_when_selected_enrollment_root_is_unavailable(tmp_path):
|
|
legacy_root = "/srv/example/appdata"
|
|
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
|
|
class CoreReadyBroker(_Broker):
|
|
def enrollment_capabilities(self, *, secret):
|
|
assert secret == "b" * 64
|
|
return {
|
|
"status": "ready",
|
|
"reason": "",
|
|
"availableTrustedAppdataRoots": [
|
|
{"root": legacy_root, "status": "unavailable", "reason": "legacy_appdata_root_unavailable"},
|
|
{"root": managed_root, "status": "unavailable", "reason": "managed_appdata_volume_missing"},
|
|
],
|
|
"managedStorage": {
|
|
"status": "unavailable",
|
|
"reason": "managed_appdata_volume_missing",
|
|
"initializationAvailable": False,
|
|
},
|
|
"features": {
|
|
"deploymentApply": True,
|
|
"deploymentVerify": True,
|
|
"rollback": True,
|
|
"exactTargetPolicy": True,
|
|
"hmacV2": True,
|
|
"replayProtection": True,
|
|
"enrollment": False,
|
|
"targetRegistration": False,
|
|
"runtimeSecretSealing": False,
|
|
},
|
|
"enrollmentReadiness": {
|
|
"status": "unavailable",
|
|
"reason": "legacy_appdata_root_unavailable",
|
|
},
|
|
"registry": {"status": "ready", "registeredProjectCount": 48},
|
|
}
|
|
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
result = _plane(broker=CoreReadyBroker()).capabilities(session)
|
|
|
|
assert result["controller"] == {"status": "ready", "reason": ""}
|
|
assert result["targetEnrollment"]["status"] == "unavailable"
|
|
assert result["runtimeDeployment"]["status"] == "ready"
|
|
assert result["rollback"]["status"] == "ready"
|
|
assert result["verification"]["status"] == "ready"
|
|
assert result["enrollmentReadiness"] == {
|
|
"status": "unavailable",
|
|
"reason": "legacy_appdata_root_unavailable",
|
|
}
|
|
assert result["availableTrustedAppdataRoots"] == [
|
|
{"root": legacy_root, "status": "unavailable", "reason": "legacy_appdata_root_unavailable"},
|
|
{"root": managed_root, "status": "unavailable", "reason": "managed_appdata_volume_missing"},
|
|
]
|
|
|
|
|
|
def test_runtime_capabilities_fail_closed_when_controller_feature_attestation_is_missing(tmp_path):
|
|
class MissingFeaturesBroker(_Broker):
|
|
def enrollment_capabilities(self, *, secret):
|
|
return {"status": "ready", "trustedAppdataRoot": self.appdata_root}
|
|
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
result = _plane(broker=MissingFeaturesBroker()).capabilities(session)
|
|
|
|
assert result["controller"]["status"] == "ready"
|
|
assert result["runtimeDeployment"]["status"] == "unavailable"
|
|
assert result["rollback"]["status"] == "unavailable"
|
|
assert result["verification"]["status"] == "unavailable"
|
|
assert result["targetEnrollment"]["status"] == "unavailable"
|
|
|
|
|
|
def test_managed_storage_capability_preserves_only_bounded_controller_reason(tmp_path):
|
|
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
|
|
class ManagedStorageUnavailableBroker(_Broker):
|
|
def enrollment_capabilities(self, *, secret):
|
|
return {
|
|
"status": "ready",
|
|
"availableTrustedAppdataRoots": [
|
|
{"root": "/srv/example/appdata", "status": "ready"},
|
|
{"root": managed_root, "status": "unavailable", "reason": "managed_appdata_volume_missing"},
|
|
],
|
|
"managedStorage": {
|
|
"status": "unavailable",
|
|
"reason": "managed_appdata_volume_missing",
|
|
"initializationAvailable": False,
|
|
},
|
|
"features": {
|
|
"deploymentApply": True,
|
|
"deploymentVerify": True,
|
|
"rollback": True,
|
|
"exactTargetPolicy": True,
|
|
"hmacV2": True,
|
|
"replayProtection": True,
|
|
"enrollment": False,
|
|
"targetRegistration": False,
|
|
"runtimeSecretSealing": False,
|
|
},
|
|
}
|
|
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
EnrollmentPlane.set_trust_config(
|
|
session,
|
|
_trust().model_copy(update={
|
|
"trusted_appdata_root": managed_root,
|
|
"managed_appdata_backup_acknowledged": True,
|
|
}),
|
|
)
|
|
result = _plane(broker=ManagedStorageUnavailableBroker()).capabilities(session)
|
|
|
|
assert result["managedStorage"]["status"] == "unavailable"
|
|
assert result["managedStorage"]["reason"] == "managed_appdata_volume_missing"
|
|
assert result["enrollmentReadiness"]["reason"] == "managed_appdata_volume_missing"
|
|
|
|
|
|
def test_capabilities_fail_closed_and_hide_unrecognized_controller_roots(tmp_path):
|
|
class MalformedRootsBroker(_Broker):
|
|
def enrollment_capabilities(self, *, secret):
|
|
return {
|
|
"status": "ready",
|
|
"trustedAppdataRoot": "/srv/example/appdata",
|
|
"availableTrustedAppdataRoots": [
|
|
{"root": "/srv/example/appdata", "status": "ready"},
|
|
{"root": "/etc/private", "status": "ready", "reason": "sensitive controller path"},
|
|
],
|
|
}
|
|
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
result = _plane(broker=MalformedRootsBroker()).capabilities(session)
|
|
|
|
assert result["targetEnrollment"]["status"] == "unavailable"
|
|
assert result["availableTrustedAppdataRoots"] == [
|
|
{"root": "/srv/example/appdata", "status": "unavailable"},
|
|
{"root": "/var/lib/docker/volumes/itworx-managed-appdata/_data", "status": "unavailable"},
|
|
]
|
|
assert "/etc/private" not in repr(result)
|
|
assert "sensitive controller path" not in repr(result)
|
|
|
|
|
|
def test_enrollment_preflight_binds_root_to_trust_and_controller_payload(tmp_path):
|
|
class RecordingBroker(_Broker):
|
|
def __init__(self):
|
|
self.preflight_payloads = []
|
|
|
|
def enrollment_preflight(self, enrollment, *, secret):
|
|
self.preflight_payloads.append(dict(enrollment))
|
|
return super().enrollment_preflight(enrollment, secret=secret)
|
|
|
|
broker = RecordingBroker()
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
plane = _plane(broker=broker)
|
|
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
alternate = _plan(trusted_appdata_root=managed_root)
|
|
blocked = plane.preflight(session, alternate)
|
|
assert blocked["status"] == "blocked"
|
|
assert "trusted_appdata_root_mismatch" in blocked["blockers"]
|
|
assert broker.preflight_payloads == []
|
|
|
|
EnrollmentPlane.set_trust_config(
|
|
session,
|
|
_trust().model_copy(update={
|
|
"trusted_appdata_root": managed_root,
|
|
"managed_appdata_backup_acknowledged": True,
|
|
}),
|
|
)
|
|
payload = plane._controller_payload(
|
|
alternate,
|
|
enrollment_id="enr-00000000000000000000",
|
|
)
|
|
|
|
assert payload["trusted_appdata_root"] == "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
assert payload["appdata_path"] == "/var/lib/docker/volumes/itworx-managed-appdata/_data/wsbc-portal"
|
|
assert payload["data_path"] == "/var/lib/docker/volumes/itworx-managed-appdata/_data/wsbc-portal/data"
|
|
|
|
|
|
def test_enrollment_creates_write_only_credentials_exact_policies_and_is_idempotent(tmp_path):
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
plane = _plane()
|
|
result = plane.enroll(session, _plan(), idempotency_key="enroll-wsbc-portal-0001")
|
|
assert result["enrollment"]["status"] == "ready"
|
|
assert result["job"]["status"] == "ready"
|
|
assert result["enrollment"]["targetSecretRef"] == "ENROLL_WSBC_PORTAL_TARGET_HMAC"
|
|
assert result["enrollment"]["oidcSecretRef"] == "ENROLL_WSBC_PORTAL_OIDC_CLIENT_SECRET"
|
|
target_secret = get_secret(session, "ENROLL_WSBC_PORTAL_TARGET_HMAC")
|
|
oidc_secret = get_secret(session, "ENROLL_WSBC_PORTAL_OIDC_CLIENT_SECRET")
|
|
assert target_secret and oidc_secret
|
|
assert target_secret not in str(result)
|
|
assert oidc_secret not in str(result)
|
|
policies = session.exec(select(Policy).where(Policy.target == "wsbc-portal")).all()
|
|
assert {(policy.action, policy.target, policy.manual, policy.voice) for policy in policies} == {
|
|
("deployment.apply", "wsbc-portal", "allowed", "blocked"),
|
|
("deployment.rollback", "wsbc-portal", "allowed", "blocked"),
|
|
}
|
|
assert all(policy.provenance_json["enrollmentId"] == result["enrollment"]["id"] for policy in policies)
|
|
replay = plane.enroll(session, _plan(), idempotency_key="enroll-wsbc-portal-0001")
|
|
assert replay["idempotentReplay"] is True
|
|
assert replay["enrollment"]["id"] == result["enrollment"]["id"]
|
|
assert len(session.exec(select(Enrollment)).all()) == 1
|
|
assert len(session.exec(select(EnrollmentJob)).all()) == 1
|
|
|
|
|
|
def test_enrollment_policy_id_collision_blocks_before_mutation_and_leaves_no_partial_policy(tmp_path):
|
|
class RecordingBroker(_CompensatingBroker):
|
|
enroll_calls = 0
|
|
|
|
def enroll_target(self, *args, **kwargs):
|
|
self.enroll_calls += 1
|
|
return super().enroll_target(*args, **kwargs)
|
|
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
collision = Policy(
|
|
id="enrollment-wsbc-portal-deployment-rollback",
|
|
action="deployment.rollback",
|
|
target="wsbc-portal",
|
|
voice="blocked",
|
|
manual="allowed",
|
|
risk="high",
|
|
reason="pre-existing exact-target policy",
|
|
provenance_json={"owner": "pre-existing"},
|
|
)
|
|
session.add(collision)
|
|
session.commit()
|
|
|
|
broker = RecordingBroker()
|
|
result = _compensating_plane(broker).enroll(
|
|
session, _plan(), idempotency_key="enroll-policy-collision-0001"
|
|
)
|
|
|
|
# A pre-existing rollback policy must never leave behind the apply half
|
|
# of the enrollment-generated pair, even after failure compensation.
|
|
assert session.get(Policy, "enrollment-wsbc-portal-deployment-apply") is None
|
|
assert session.get(Policy, collision.id).provenance_json == {"owner": "pre-existing"}
|
|
assert result["enrollment"]["status"] == "preflight_failed"
|
|
assert result["job"]["status"] == "preflight_failed"
|
|
assert "exact_policy_id_collision" in result["job"]["preflight"]["blockers"]
|
|
assert broker.enroll_calls == 0
|
|
assert session.exec(select(Policy).where(Policy.target == "wsbc-portal")).all() == [collision]
|
|
|
|
|
|
def test_late_policy_id_collision_rolls_back_the_whole_pair(tmp_path, monkeypatch):
|
|
class FailedControllerCompensationBroker(_CompensatingBroker):
|
|
def unenroll_target(self, _target, *, secret):
|
|
assert secret == "b" * 64
|
|
return {"status": "rollback_failed"}
|
|
|
|
collision = Policy(
|
|
id="enrollment-wsbc-portal-deployment-rollback",
|
|
action="deployment.rollback",
|
|
target="wsbc-portal",
|
|
voice="blocked",
|
|
manual="allowed",
|
|
risk="high",
|
|
reason="concurrent exact-target policy",
|
|
provenance_json={"owner": "concurrent"},
|
|
)
|
|
checkpoint = EnrollmentPlane._checkpoint
|
|
|
|
def add_collision_after_preflight(session, enrollment, job, *, stage, evidence):
|
|
checkpoint(session, enrollment, job, stage=stage, evidence=evidence)
|
|
if stage == "installing_policy":
|
|
session.add(collision)
|
|
session.commit()
|
|
|
|
monkeypatch.setattr(
|
|
EnrollmentPlane, "_checkpoint", staticmethod(add_collision_after_preflight)
|
|
)
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
result = _compensating_plane(FailedControllerCompensationBroker()).enroll(
|
|
session, _plan(), idempotency_key="fixture-" + "x" * 32
|
|
)
|
|
|
|
assert result["job"]["preflight"]["status"] == "ready"
|
|
assert result["enrollment"]["status"] == "compensation_required"
|
|
assert result["job"]["status"] == "compensation_required"
|
|
assert result["job"]["evidence"]["compensation"] == {
|
|
"completed": False,
|
|
"outcomes": {
|
|
"oidc": "removed",
|
|
"proxy": "removed",
|
|
"controller": "rollback_failed",
|
|
},
|
|
}
|
|
assert session.get(Policy, "enrollment-wsbc-portal-deployment-apply") is None
|
|
assert session.get(Policy, collision.id).provenance_json == {"owner": "concurrent"}
|
|
assert session.exec(select(Policy).where(Policy.target == "wsbc-portal")).all() == [collision]
|
|
|
|
|
|
def test_final_ready_commit_failure_rolls_back_the_policy_pair(tmp_path):
|
|
engine = create_engine(f"sqlite:///{tmp_path / 'enrollment.db'}")
|
|
SQLModel.metadata.create_all(engine)
|
|
with Session(engine) as session:
|
|
_configure(session)
|
|
plane = _plane()
|
|
commit = session.commit
|
|
|
|
def fail_after_policy_pair_is_staged():
|
|
ready = session.exec(
|
|
select(Enrollment).where(Enrollment.status == "ready")
|
|
).first()
|
|
policies = session.exec(
|
|
select(Policy).where(Policy.target == "wsbc-portal")
|
|
).all()
|
|
if ready is not None and {policy.action for policy in policies} == {
|
|
"deployment.apply", "deployment.rollback",
|
|
}:
|
|
raise RuntimeError("simulated final enrollment commit failure")
|
|
commit()
|
|
|
|
session.commit = fail_after_policy_pair_is_staged
|
|
with pytest.raises(RuntimeError, match="final enrollment commit failure"):
|
|
plane.enroll(session, _plan(), idempotency_key="enroll-final-commit-failure-0001")
|
|
|
|
# A request-scoped MCP session is closed when the operation raises; prove
|
|
# the pending pair was not committed by reading through a fresh session.
|
|
with Session(engine) as verification:
|
|
enrollment = verification.exec(select(Enrollment)).one()
|
|
job = verification.exec(select(EnrollmentJob)).one()
|
|
assert enrollment.status == "provisioning"
|
|
assert job.stage == "installing_policy"
|
|
assert verification.exec(
|
|
select(Policy).where(Policy.target == "wsbc-portal")
|
|
).all() == []
|
|
|
|
|
|
def test_concurrent_policy_id_collision_during_flush_rolls_back_pending_pair(tmp_path):
|
|
collision = Policy(
|
|
id="enrollment-wsbc-portal-deployment-rollback",
|
|
action="deployment.rollback",
|
|
target="wsbc-portal",
|
|
voice="blocked",
|
|
manual="allowed",
|
|
risk="high",
|
|
reason="concurrent exact-target policy",
|
|
provenance_json={"owner": "concurrent"},
|
|
)
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
flush = session.flush
|
|
inserted = False
|
|
|
|
def flush_with_concurrent_collision(*args, **kwargs):
|
|
nonlocal inserted
|
|
staged_ids = {
|
|
policy.id for policy in session.new if isinstance(policy, Policy)
|
|
}
|
|
if not inserted and staged_ids == {
|
|
"enrollment-wsbc-portal-deployment-apply",
|
|
"enrollment-wsbc-portal-deployment-rollback",
|
|
}:
|
|
inserted = True
|
|
with Session(session.get_bind()) as concurrent_session:
|
|
concurrent_session.add(collision)
|
|
concurrent_session.commit()
|
|
return flush(*args, **kwargs)
|
|
|
|
session.flush = flush_with_concurrent_collision
|
|
result = _compensating_plane().enroll(
|
|
session, _plan(), idempotency_key="enroll-policy-concurrent-0001"
|
|
)
|
|
|
|
assert inserted is True
|
|
assert result["job"]["preflight"]["status"] == "ready"
|
|
assert result["enrollment"]["status"] == "failed"
|
|
assert session.get(Policy, "enrollment-wsbc-portal-deployment-apply") is None
|
|
concurrent_policy = session.get(
|
|
Policy, "enrollment-wsbc-portal-deployment-rollback"
|
|
)
|
|
assert concurrent_policy is not None
|
|
assert concurrent_policy.provenance_json == {"owner": "concurrent"}
|
|
policies = session.exec(select(Policy).where(Policy.target == "wsbc-portal")).all()
|
|
assert [(policy.id, policy.action, policy.provenance_json) for policy in policies] == [
|
|
(
|
|
"enrollment-wsbc-portal-deployment-rollback",
|
|
"deployment.rollback",
|
|
{"owner": "concurrent"},
|
|
)
|
|
]
|
|
|
|
|
|
def test_interrupted_external_enrollment_requires_explicit_recovery(tmp_path):
|
|
class InterruptedBroker(_Broker):
|
|
def enroll_target(self, _enrollment, *, secret, idempotency_key):
|
|
assert secret == "b" * 64
|
|
assert idempotency_key == "enroll-wsbc-interrupted-0001"
|
|
raise RuntimeError("controller connection interrupted after request dispatch")
|
|
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
plane = _plane(broker=InterruptedBroker())
|
|
with pytest.raises(RuntimeError, match="interrupted"):
|
|
plane.enroll(session, _plan(), idempotency_key="enroll-wsbc-interrupted-0001")
|
|
|
|
job = session.exec(select(EnrollmentJob)).one()
|
|
assert job.status == "running"
|
|
assert job.stage == "registering_target"
|
|
assert job.evidence_json["intent"]["controller"]["applicationId"] == "wsbc-portal"
|
|
assert "target_credential" not in str(job.evidence_json)
|
|
|
|
recovered = plane.enroll(session, _plan(), idempotency_key="enroll-wsbc-interrupted-0001")
|
|
assert recovered["recoveryRequired"] is True
|
|
assert recovered["job"]["status"] == "recovery_required"
|
|
assert recovered["enrollment"]["status"] == "recovery_required"
|
|
|
|
|
|
def test_preflight_blocks_hostname_outside_root_trust_boundary(tmp_path):
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
raw = _plan().model_dump()
|
|
raw["exposure"]["hostname"] = "wsbc.example.net"
|
|
result = _plane().preflight(session, EnrollmentPlanV1.model_validate(raw))
|
|
assert result["status"] == "blocked"
|
|
assert "hostname_not_allowed" in result["blockers"]
|
|
|
|
|
|
def test_preflight_rejects_an_oidc_role_model_outside_root_trust(tmp_path):
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
raw = _plan().model_dump()
|
|
raw["authentication"]["roles"]["administrator"] = "TenantAdmin"
|
|
result = _plane().preflight(session, EnrollmentPlanV1.model_validate(raw))
|
|
assert result["status"] == "blocked"
|
|
assert "oidc_role_model" in result["blockers"]
|
|
|
|
|
|
def test_failed_identity_provisioning_compensates_only_created_resources(tmp_path):
|
|
class CompensatingBroker(_Broker):
|
|
def unenroll_target(self, target, *, secret):
|
|
assert target["application_id"] == "wsbc-portal"
|
|
assert secret == "b" * 64
|
|
return {"status": "rolled_back"}
|
|
|
|
class CompensatingNpm(_Npm):
|
|
def remove_managed_proxy_host(self, *, host_id, ownership_id):
|
|
assert host_id == 10
|
|
assert ownership_id.startswith("enr-")
|
|
return {"success": True, "status": "removed"}
|
|
|
|
class FailingAuthentik(_Authentik):
|
|
def ensure_deployment_oidc_application(self, **_kwargs):
|
|
return {"success": False, "status": "failed"}
|
|
|
|
with _session(tmp_path) as session:
|
|
_configure(session)
|
|
result = _plane(
|
|
broker=CompensatingBroker(),
|
|
npm=CompensatingNpm(),
|
|
authentik=FailingAuthentik(),
|
|
).enroll(session, _plan(), idempotency_key="enroll-wsbc-compensate-0001")
|
|
assert result["enrollment"]["status"] == "failed"
|
|
assert result["job"]["evidence"]["compensation"] == {
|
|
"completed": True,
|
|
"outcomes": {"proxy": "removed", "controller": "rolled_back"},
|
|
}
|
|
assert get_secret(session, "ENROLL_WSBC_PORTAL_TARGET_HMAC") is None
|
|
assert get_secret(session, "ENROLL_WSBC_PORTAL_OIDC_CLIENT_SECRET") is None
|
|
assert session.exec(select(Policy).where(Policy.target == "wsbc-portal")).all() == []
|