Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docker-socket-proxy/tests/test_policy.py

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions docker-socket-proxy/tests/test_proxy_connection_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
TestConnectionReuseDoesNotCauseIntermittentFailures for that live,
real-daemon reproduction; these tests pin down the pure header-framing
fix in isolation, no daemon needed.

Developer:
Manish Kumar <manish@omnibioai.org>
"""
from __future__ import annotations

Expand All @@ -36,11 +39,15 @@


def _raw(status_line: str, *header_lines: str) -> bytes:
"""Builds raw response header bytes (status line plus header lines, blank-line
terminated, ISO-8859-1) as input for _force_connection_close."""
text = "\r\n".join([status_line, *header_lines, "", ""])
return text.encode("iso-8859-1")


class TestForceConnectionClose:
"""_force_connection_close() rewrites an upstream response header block so it always
carries exactly one Connection: close header and leaves everything else intact."""
def test_adds_close_when_no_connection_header_present(self):
"""The exact real-world case that caused the bug: the real
daemon's /_ping response carries no Connection header at all."""
Expand All @@ -52,13 +59,17 @@ def test_adds_close_when_no_connection_header_present(self):
assert text.count("Connection:") == 1

def test_overrides_existing_keep_alive_value(self):
"""An upstream Connection: keep-alive header is replaced by a single Connection:
close."""
raw = _raw("HTTP/1.1 200 OK", "Content-Length: 0", "Connection: keep-alive")
out = _force_connection_close(raw).decode("iso-8859-1")
assert "Connection: close" in out
assert "keep-alive" not in out.lower()
assert out.count("Connection:") == 1

def test_case_insensitive_header_name_match(self):
"""The Connection header name is matched case-insensitively, so 'CONNECTION:
Keep-Alive' is replaced and exactly one Connection header remains."""
raw = _raw("HTTP/1.1 200 OK", "content-length: 0", "CONNECTION: Keep-Alive")
out = _force_connection_close(raw).decode("iso-8859-1")
assert "Connection: close" in out
Expand All @@ -67,6 +78,8 @@ def test_case_insensitive_header_name_match(self):
assert sum(1 for line in out.split("\r\n") if line.lower().startswith("connection:")) == 1

def test_preserves_status_line_and_other_headers(self):
"""The status line and unrelated headers are kept unchanged when Connection:
close is added."""
raw = _raw(
"HTTP/1.1 404 Not Found",
"Content-Type: application/json",
Expand All @@ -82,13 +95,17 @@ def test_preserves_status_line_and_other_headers(self):
assert "Connection: close" in lines

def test_no_headers_besides_status_line(self):
"""A response consisting only of a status line still gets a Connection: close
header."""
raw = _raw("HTTP/1.1 204 No Content")
out = _force_connection_close(raw).decode("iso-8859-1")
lines = out.split("\r\n")
assert lines[0] == "HTTP/1.1 204 No Content"
assert "Connection: close" in lines

def test_output_ends_with_blank_line_terminator(self):
"""The rewritten header block still ends with a single CRLF CRLF terminator and
nothing follows it."""
raw = _raw("HTTP/1.1 200 OK", "Content-Length: 0")
out = _force_connection_close(raw)
assert out.endswith(b"\r\n\r\n")
Expand Down
23 changes: 23 additions & 0 deletions docker-socket-proxy/tests/test_proxy_live_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
environment -- this is an opt-in live test, matching the pattern
established elsewhere in this codebase for tests that need a real
Docker daemon (e.g. pdf_report_builder's opt-in live E2E).

Developer:
Manish Kumar <manish@omnibioai.org>
"""
from __future__ import annotations

Expand Down Expand Up @@ -87,6 +90,8 @@ def proxy_env(tmp_path_factory):


def _docker(env, *args, timeout=30):
"""Runs the docker CLI with the given environment (whose DOCKER_HOST points at the
proxy socket) and captures its output."""
return subprocess.run(
[DOCKER_BIN, *args],
env=env,
Expand All @@ -107,11 +112,14 @@ class TestLegitimateTrafficWorksEndToEnd:
without breaking it."""

def test_docker_ps(self, proxy_env):
"""docker ps through the proxy exits 0."""
env, _ = proxy_env
result = _docker(env, "ps")
assert result.returncode == 0, result.stderr

def test_run_with_allowed_bind_mount_full_round_trip(self, proxy_env):
"""A docker run bind-mounting the allowed workdir succeeds, and the file the
container wrote is visible in the host workdir."""
env, workdir = proxy_env
name = _unique_container_name()
result = _docker(
Expand All @@ -132,6 +140,8 @@ def test_pull_already_present_image(self, proxy_env):
assert result.returncode == 0, result.stderr

def test_logs_on_a_running_container(self, proxy_env):
"""docker logs on a running proxied container returns its output; the container
is force-removed afterwards."""
env, workdir = proxy_env
name = _unique_container_name()
try:
Expand All @@ -155,6 +165,8 @@ class TestAdversarialRequestsAreBlocked:
(403-style denial) -- never merely fail for some other reason."""

def test_privileged_blocked(self, proxy_env):
"""docker run --privileged is rejected by the proxy with a denial message that
names privileged."""
env, workdir = proxy_env
result = _docker(
env, "run", "--rm", "--privileged",
Expand All @@ -165,12 +177,15 @@ def test_privileged_blocked(self, proxy_env):
assert "privileged" in result.stderr.lower()

def test_root_bind_mount_blocked(self, proxy_env):
"""docker run -v /:/hostroot is rejected by the proxy with a denial message."""
env, _ = proxy_env
result = _docker(env, "run", "--rm", "-v", "/:/hostroot", "alpine:latest", "true")
assert result.returncode != 0
assert "denied" in result.stderr.lower()

def test_bind_mount_outside_allowed_prefix_blocked(self, proxy_env):
"""A bind mount of /tmp, outside the allowed workdir prefix, is rejected by the
proxy with a denial message."""
env, _ = proxy_env
result = _docker(env, "run", "--rm", "-v", "/tmp:/hosttmp", "alpine:latest", "true")
assert result.returncode != 0
Expand All @@ -190,6 +205,8 @@ def test_docker_sock_remount_blocked(self, proxy_env):
assert "denied" in result.stderr.lower()

def test_cap_add_blocked(self, proxy_env):
"""docker run --cap-add SYS_ADMIN is rejected by the proxy with a denial message
that names capadd."""
env, workdir = proxy_env
result = _docker(
env, "run", "--rm", "--cap-add", "SYS_ADMIN",
Expand All @@ -200,6 +217,8 @@ def test_cap_add_blocked(self, proxy_env):
assert "capadd" in result.stderr.lower()

def test_network_host_blocked(self, proxy_env):
"""docker run --network host is rejected by the proxy with a denial message that
names networkmode."""
env, workdir = proxy_env
result = _docker(
env, "run", "--rm", "--network", "host",
Expand All @@ -210,6 +229,8 @@ def test_network_host_blocked(self, proxy_env):
assert "networkmode" in result.stderr.lower()

def test_exec_into_running_container_blocked(self, proxy_env):
"""docker exec into a running proxied container is rejected with a denial
message citing the allowlist; the container is force-removed afterwards."""
env, workdir = proxy_env
name = _unique_container_name()
try:
Expand Down Expand Up @@ -251,6 +272,8 @@ class TestConnectionReuseDoesNotCauseIntermittentFailures:
against the real published (pre-fix) image."""

def test_many_sequential_legitimate_runs_all_succeed(self, proxy_env):
"""Fifteen back-to-back allowed docker runs all succeed with no reset,
broken-pipe or closed-idle-connection errors, and each writes its output file."""
env, workdir = proxy_env
for i in range(15):
result = _docker(
Expand Down
11 changes: 11 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

Services are accessed through the nginx router at http://localhost (port 80)
and also directly via their native ports where noted.

Developer:
Manish Kumar <manish@omnibioai.org>
"""

import os
Expand Down Expand Up @@ -99,6 +102,9 @@ def _login_or_skip() -> dict:

@pytest.fixture(scope="session")
def auth_tokens():
"""Session-scoped login to the auth service with the
OMNIBIOAI_AUTH_EMAIL/OMNIBIOAI_AUTH_PASSWORD credentials; skips when credentials are
not configured or the service is unreachable."""
return _login_or_skip()


Expand Down Expand Up @@ -144,11 +150,16 @@ def _lims_login_or_skip() -> requests.Session:

@pytest.fixture(scope="session")
def lims_session():
"""Session-scoped requests.Session authenticated against LIMS with the
OMNIBIOAI_LIMS_USER/OMNIBIOAI_LIMS_PASSWORD credentials; skips when LIMS is
unreachable."""
return _lims_login_or_skip()


# ── Fixtures: RAG headers ─────────────────────────────────────────────────────

@pytest.fixture(scope="session")
def rag_headers():
"""Authorization header built from RAGBIO_API_KEY; the key is an empty string when
the environment variable is not set."""
return {"Authorization": f"Bearer {RAGBIO_API_KEY}"}
Loading
Loading