"""WP-100 broker security and bounded-output tests.""" from __future__ import annotations import json import os import subprocess import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch from services.docker_read_broker import ( BrokerApp, FakeDockerBackend, RegistrationStore, ServiceTokenVerifier, issue_service_token, ) from services.docker_read_broker.broker import ( BrokerError, Registration, _decode_docker_log_stream, _decode_http_response, _json_query, _storage_rows, ) from services.docker_read_broker.server import _load_registrations class SocketUnavailableBackend(FakeDockerBackend): def host(self): raise BrokerError("BROKER_SOCKET_UNAVAILABLE", "Docker socket is unavailable", 503) class SpoofedLabelBackend(FakeDockerBackend): def inspect(self, container_id: str): value = dict(super().inspect(container_id)) config = dict(value["Config"]) config["Labels"] = dict(config["Labels"], **{"com.docker.compose.service": "spoofed"}) value["Config"] = config return value class AmbiguousLabelBackend(FakeDockerBackend): def containers(self, state=None): first = dict(super().containers(state)[0]) second = dict(first, Id="container-web-002") return [first, second] def make_app(backend=None) -> tuple[BrokerApp, str]: secret = "test-broker-secret" # noqa: S105 registration = Registration( project="ludarium", environment="prod", registered_id="ludarium-web", container_id="container-web-001", compose_project="ludarium", service="web", allowed_reads=frozenset({"inspect", "health", "logs"}), ) app = BrokerApp( backend or FakeDockerBackend(), RegistrationStore((registration,)), ServiceTokenVerifier(secret), ) token = issue_service_token(secret, "ludarium", "prod") return app, token class DockerReadBrokerTests(unittest.TestCase): def test_broker_runtime_import_does_not_require_the_mcp_sdk(self) -> None: script = """ import builtins real_import = builtins.__import__ def without_mcp(name, *args, **kwargs): if name == "mcp" or name.startswith("mcp."): raise ModuleNotFoundError("mcp is intentionally unavailable") return real_import(name, *args, **kwargs) builtins.__import__ = without_mcp from services.docker_read_broker import issue_service_token assert callable(issue_service_token) """ result = subprocess.run( # noqa: S603 - fixed interpreter and static script [sys.executable, "-c", script], cwd=Path(__file__).resolve().parents[1], check=False, capture_output=True, text=True, ) self.assertEqual(result.returncode, 0, result.stderr) def test_docker_chunked_http_response_is_decoded(self) -> None: body = b'[{"Id":"container-web-001"}]' response = ( b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" + f"{len(body):X}\r\n".encode() + body + b"\r\n0\r\n\r\n" ) self.assertEqual(_decode_http_response(response), body) def test_docker_http_parser_fails_closed_on_invalid_chunk(self) -> None: response = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nZZ\r\n{}\r\n0\r\n\r\n" with self.assertRaisesRegex(BrokerError, "invalid chunks"): _decode_http_response(response) def test_docker_http_parser_hides_engine_error_body(self) -> None: response = b"HTTP/1.1 500 Error\r\nContent-Length: 20\r\n\r\nsensitive engine msg" with self.assertRaisesRegex(BrokerError, "Docker read request failed") as raised: _decode_http_response(response) self.assertNotIn("sensitive", str(raised.exception)) def test_docker_multiplexed_logs_are_decoded(self) -> None: stdout = b"first line\n" stderr = b"second line\n" payload = ( b"\x01\x00\x00\x00" + len(stdout).to_bytes(4, "big") + stdout + b"\x02\x00\x00\x00" + len(stderr).to_bytes(4, "big") + stderr ) self.assertEqual(_decode_docker_log_stream(payload), ["first line", "second line"]) def test_docker_log_parser_rejects_invalid_frame_header(self) -> None: payload = b"\x01\x01\x00\x00\x00\x00\x00\x01x" with self.assertRaisesRegex(BrokerError, "invalid log frames"): _decode_docker_log_stream(payload) def test_docker_filters_are_url_encoded_json(self) -> None: self.assertEqual( _json_query({"status": ["running"]}), "%7B%22status%22%3A%5B%22running%22%5D%7D", ) def test_docker_storage_projection_is_bounded_and_structured(self) -> None: rows = _storage_rows({"Driver": "btrfs", "DockerRootDir": "/secret/host/path"}) self.assertEqual( rows, [{"name": "docker-root", "driver": "btrfs", "usageBytes": 0, "capacityBytes": 0}] ) self.assertNotIn("DockerRootDir", repr(rows)) def test_runtime_registration_resolves_stable_compose_labels(self) -> None: document = { "schemaVersion": "1.0", "defaultDecision": "deny", "containers": [ { "registeredId": "ludarium-web", "project": "ludarium", "environment": "prod", "match": {"composeProject": "ludarium", "service": "web"}, "allowedReads": ["inspect", "health", "logs"], } ], } with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "registrations.json" path.write_text(json.dumps(document), encoding="utf-8") with patch.dict(os.environ, {"BROKER_REGISTRATIONS_FILE": str(path)}): store = _load_registrations(FakeDockerBackend()) registration = store.resolve("ludarium", "prod", "ludarium-web", "inspect") self.assertEqual(registration.container_id, "container-web-001") def test_runtime_registration_fails_closed_on_ambiguous_labels(self) -> None: document = { "schemaVersion": "1.0", "defaultDecision": "deny", "containers": [ { "registeredId": "ludarium-web", "project": "ludarium", "environment": "prod", "match": {"composeProject": "ludarium", "service": "web"}, "allowedReads": ["inspect"], } ], } with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "registrations.json" path.write_text(json.dumps(document), encoding="utf-8") with patch.dict(os.environ, {"BROKER_REGISTRATIONS_FILE": str(path)}): with self.assertRaisesRegex(SystemExit, "registration validation failed"): _load_registrations(AmbiguousLabelBackend()) def test_only_registered_safe_inventory_is_returned(self) -> None: app, token = make_app() status, body = app.handle("GET", "/v1/containers", {"Authorization": f"Bearer {token}"}) self.assertEqual(status, 200) item = body["items"][0] self.assertEqual(item["containerRef"], "ludarium-web") self.assertNotIn("Env", item) self.assertNotIn("Mounts", item) self.assertNotIn("TOKEN", repr(body)) def test_inspect_health_and_logs_are_registration_bound(self) -> None: app, token = make_app() headers = {"Authorization": f"Bearer {token}"} for route in ( "/v1/containers/ludarium-web", "/v1/containers/ludarium-web/health", "/v1/containers/ludarium-web/logs?tail=10", ): status, body = app.handle("GET", route, headers) self.assertEqual(status, 200, route) if route.endswith("logs?tail=10"): self.assertIn("", repr(body)) self.assertNotIn("super-secret-token", repr(body)) status, _ = app.handle("GET", "/v1/containers/unknown/inspect", headers) self.assertEqual(status, 404) def test_mutation_exec_and_raw_routes_are_denied(self) -> None: app, token = make_app() headers = {"Authorization": f"Bearer {token}"} for method, path in ( ("POST", "/v1/containers/ludarium-web/start"), ("POST", "/v1/containers/ludarium-web/exec"), ("GET", "/v1/containers/json"), ("GET", "/v1/images/json"), ): status, body = app.handle(method, path, headers) self.assertIn(status, {403, 404, 405}) self.assertTrue(body["code"].startswith("BROKER_")) def test_auth_scope_and_token_expiry_are_enforced(self) -> None: app, _ = make_app() status, _ = app.handle("GET", "/v1/host") self.assertEqual(status, 401) foreign = issue_service_token("test-broker-secret", "other-project", "prod") status, _ = app.handle( "GET", "/v1/containers/ludarium-web", {"Authorization": f"Bearer {foreign}"} ) self.assertEqual(status, 403) expired = issue_service_token("test-broker-secret", "ludarium", "prod", ttl_seconds=-1) status, _ = app.handle("GET", "/v1/host", {"Authorization": f"Bearer {expired}"}) self.assertEqual(status, 401) def test_label_spoof_is_denied(self) -> None: app, token = make_app(SpoofedLabelBackend()) status, body = app.handle( "GET", "/v1/containers/ludarium-web", {"Authorization": f"Bearer {token}"} ) self.assertEqual(status, 403) self.assertEqual(body["code"], "BROKER_REGISTRATION_MISMATCH") def test_socket_unavailable_degrades_broker_capability(self) -> None: app, token = make_app(SocketUnavailableBackend()) status, body = app.handle("GET", "/v1/host", {"Authorization": f"Bearer {token}"}) self.assertEqual(status, 503) self.assertEqual(body["code"], "BROKER_SOCKET_UNAVAILABLE") if __name__ == "__main__": unittest.main()