Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
commit
cb5be898ea
663 files changed
+82674
No files matched your search
@@ -0,0 +1,261 @@
|
||||
"""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("<redacted>", 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()
|
||||
Reference in new issue
Block a user