Files
OpenShell/e2e/rust/tests/forward_proxy_graphql_l7.rs
Drew Newberry c1f2e7189f feat(isolation): implement the RFC 0012 sandbox architecture (#2942)
* feat(isolation): add RFC 0012 backend contract

Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com>

* refactor(isolation): name the interface crate explicitly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): expose trusted host gateway

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(agents): inventory the MXC driver

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add mediated DNS transport

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): tighten interface error and digest contracts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(isolation): remove unrelated driver inventory

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): define capability-free launch contract

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): seal confirmed boundary state

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): validate confirmation for external backend implementations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): clarify mediated DNS identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): generalize loopback connector

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): unify typed network mediation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): bind launches to sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(mxc): initialize extended sandbox status

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add boundary protocol and Linux primitives

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): harden signals and separate process status from transport

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): validate remote confirmation through public contract

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): validate wire state and propagate snapshot failures

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(isolation): import owned agent specification explicitly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(isolation): describe mediated DNS channel

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): bound mediation attach without nested retries

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): generalize loopback protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add transport-neutral session authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): separate sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): harden runtime boundary controls

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add terminal boundary operation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): split supervisor and sandbox runtimes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): harden boundary isolation and lifecycle ownership

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): reject private root redirects and adopt typed errors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): preserve accept thread ownership on musl

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(sandbox): isolate credential probes from filtered threads

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): return retained exec exit status to independent waiters

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): bound network mediation and preserve socket authorization

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): bound control admission and retire stale mediation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* ci(e2e): select migrated drivers per stack layer

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(sandbox): implement loopback connector

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): authenticate the Sandbox Protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(supervisor): rotate launch-scoped authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(sandbox): consume dedicated backend crate

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(sandbox): align topology session fixture

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): align projected bootstrap bundle

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): validate refreshed credentials before rotation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): fail closed across supervisor disconnects

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): repair rebased sandbox CI

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* build(runtime): publish separate sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(config): configure the sandbox runtime image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(ci): validate sandbox binary linkage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): use backend and runtime terminology

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(sandbox): use a scratch runtime image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(ci): refresh schema and dependency policy

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): bind reconnects to supervisor process

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs: align runtime split operational guidance

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* chore(security): document Kubernetes runtime RBAC

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): enforce runtime lifecycle invariants

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(compute): identify sandbox start generations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(server): restore sandbox launch sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): support authenticated runtime replacement

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): bind sandbox session successors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): retry pending sandbox successors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(vm): run the supervisor outside the guest workload

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): repair rebase integration

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): use unified build toolchain

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): use sandbox runtime terminology

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): own guest network bootstrap

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): expose guest init version

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): select native supervisor artifacts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): guard guest init Linux symbols

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): scope Linux test imports

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): avoid guest interface casts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): reconcile admitted sandbox identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): share resolved sandbox identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): surface host supervisor failures

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): include guest logs on supervisor exit

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): rotate and clean runtime generations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): make sandbox starts generation-aware

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): keep shared paths in the base layer

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(docker): isolate workloads behind the host supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(docker): rotate launch-scoped authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): use host networking for supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): preserve host gateway alias resolution

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(docker): use separate sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): restore startup validation after rebase

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): name the sandbox runtime directly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): narrow supervisor CA runtime storage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): close companion isolation gaps

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(docker): align mediated network expectations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(docker): exercise mediated network paths

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): attach supervisor to managed network

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): defer supervisor recovery until gateway is ready

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): make sandbox starts generation-aware

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): preserve workloads during session rotation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): remove unrelated configuration RFC changes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(kubernetes): add proxy-pod isolation topology

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(kubernetes): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): use stable sandbox service authority

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(kubernetes): split sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): adapt proxy pods to current runtime APIs

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(kubernetes): describe the single runtime placement

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(kubernetes): simplify sandbox orchestration

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): validate deployment prerequisites

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): update Trivy Helm profile inventory

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(kubernetes): update Trivy scan inventory count

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): reuse preloaded runtime images in e2e

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): type and clean runtime resources

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): make sandbox restarts recoverable

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): preserve supervisor egress

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(podman): adopt isolated sandbox and supervisor containers

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): stage bootstrap archives at named volume destinations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(podman): rotate launch-scoped authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(podman): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(podman): use host networking for supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(podman): split sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): repair rebase integration

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(podman): name the sandbox runtime directly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): provision supervisor CA runtime storage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): address isolation review findings

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): inspect Debian supervisor provenance

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): use libpod-compatible tmpfs options

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): bind verified sandbox runtime binary

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): provide external driver data directory

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): start sandbox before joining user namespace

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): separate supervisor user namespace

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): make sandbox starts generation-aware

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* perf(isolation): add TCP and DNS benchmark harnesses

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(perf): align benchmark timing and supported protocols

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(perf): report TCP benchmark metrics accurately

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(perf): cancel failed worker startup

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): build matching local supervisor image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): make local sandbox smoke test runnable

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): wire local sandbox runtime image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): narrow sandbox service RBAC

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(ci): validate split runtime artifacts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): harden runtime session handling

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(supervisor): add standalone network proxy role

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(rfc): remove implementation companion notes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): standardize runtime release name

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): pin renamed runtime artifacts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): persist sandbox runtime identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(runtime): restore branch validation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): reconcile main after rebase

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(network): close unframed HTTP 1.0 responses

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* chore(isolation): preserve upstream OCSF updates

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(security): close credential and TLS replay paths

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): make sandbox refresh retries idempotent

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

---------

Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com>
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-09-16 00:49:14 +00:00

429 lines
15 KiB
Rust

// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! E2E tests for GraphQL L7 inspection through transparent interception.
//!
//! The upstream server deliberately does not implement GraphQL. `OpenShell`
//! parses and enforces GraphQL before forwarding, so any HTTP server that
//! accepts POST /graphql is enough to prove allowed requests reach upstream
//! and denied requests are stopped by the sandbox proxy.
#![cfg(feature = "e2e")]
use std::io::Write;
use openshell_e2e::harness::container::ContainerHttpServer;
use openshell_e2e::harness::sandbox::SandboxGuard;
use tempfile::NamedTempFile;
const TEST_SERVER_ALIAS: &str = "graphql-l7.openshell.test";
async fn start_test_server() -> Result<ContainerHttpServer, String> {
let script = r#"from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def read_chunked(self):
while True:
size_line = self.rfile.readline()
if not size_line:
return
size = int(size_line.split(b";", 1)[0].strip(), 16)
if size == 0:
while self.rfile.readline().strip():
pass
return
self.rfile.read(size)
self.rfile.read(2)
def do_GET(self):
if self.path == "/" or self.path.startswith("/graphql?"):
self.send_response(200)
else:
self.send_response(418)
self.end_headers()
self.wfile.write(b'{"ok":true}')
def do_POST(self):
if self.headers.get("Transfer-Encoding", "").lower() == "chunked":
self.read_chunked()
else:
_ = self.rfile.read(int(self.headers.get("Content-Length", "0")))
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"ok":true}')
def log_message(self, format, *args):
pass
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
"#;
ContainerHttpServer::start_python(TEST_SERVER_ALIAS, script).await
}
fn write_graphql_policy(host: &str, port: u16) -> Result<NamedTempFile, String> {
let mut file = NamedTempFile::new().map_err(|e| format!("create temp policy file: {e}"))?;
let policy = format!(
r#"version: 1
filesystem_policy:
include_workdir: true
read_only:
- /usr
- /lib
- /proc
- /dev/urandom
- /app
- /etc
- /var/log
read_write:
- /sandbox
- /tmp
- /dev/null
landlock:
compatibility: best_effort
process:
run_as_user: sandbox
run_as_group: sandbox
network_policies:
test_graphql_l7:
name: test_graphql_l7
endpoints:
- host: {host}
port: {port}
protocol: graphql
enforcement: enforce
persisted_queries: allow_registered
graphql_persisted_queries:
abc123:
operation_type: query
operation_name: Viewer
fields: [viewer]
allowed_ips:
- "10.0.0.0/8"
- "172.0.0.0/8"
- "192.168.0.0/16"
- "fc00::/7"
rules:
- allow:
operation_type: query
fields: [viewer]
- allow:
operation_type: mutation
fields: [createIssue]
deny_rules:
- operation_type: mutation
fields: [deleteRepository]
binaries:
- path: /usr/bin/python*
- path: /usr/local/bin/python*
- path: /sandbox/.uv/python/*/bin/python*
"#
);
file.write_all(policy.as_bytes())
.map_err(|e| format!("write temp policy file: {e}"))?;
file.flush()
.map_err(|e| format!("flush temp policy file: {e}"))?;
Ok(file)
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn graphql_l7_enforces_high_level_and_raw_transparent_paths() {
let server = start_test_server().await.expect("start test server");
let policy = write_graphql_policy(&server.host, server.port).expect("write custom policy");
let policy_path = policy
.path()
.to_str()
.expect("temp policy path should be utf-8")
.to_string();
let script = format!(
r#"
import json
import socket
import time
import urllib.error
import urllib.parse
import urllib.request
HOST = {host:?}
PORT = {port}
DETAILS = {{}}
QUERY_VIEWER = "query Viewer {{ viewer {{ login }} }}"
QUERY_REPOSITORY = "query Repo {{ repository(owner:\"o\", name:\"r\") {{ id }} }}"
MUTATION_CREATE = "mutation Create {{ createIssue(input:{{repositoryId:\"r\", title:\"t\", body:\"b\"}}) {{ issue {{ id }} }} }}"
MUTATION_DELETE = "mutation Delete {{ deleteRepository(input:{{repositoryId:\"r\"}}) {{ clientMutationId }} }}"
def forward_status(query):
body = json.dumps({{"query": query}}).encode()
request = urllib.request.Request(
f"http://{{HOST}}:{{PORT}}/graphql",
data=body,
headers={{"Content-Type": "application/json"}},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
response.read()
return response.status
except urllib.error.HTTPError as error:
error.read()
return error.code
def forward_get_status(query):
encoded = urllib.parse.urlencode({{"query": query}})
request = urllib.request.Request(
f"http://{{HOST}}:{{PORT}}/graphql?{{encoded}}",
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
response.read()
return response.status
except urllib.error.HTTPError as error:
error.read()
return error.code
def forward_duplicate_get_status():
safe = urllib.parse.quote_plus(QUERY_VIEWER)
unsafe = urllib.parse.quote_plus(MUTATION_DELETE)
request = urllib.request.Request(
f"http://{{HOST}}:{{PORT}}/graphql?query={{safe}}&query={{unsafe}}",
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
response.read()
return response.status
except urllib.error.HTTPError as error:
error.read()
return error.code
def forward_persisted_get_status(hash_value):
extensions = json.dumps({{"persistedQuery": {{"version": 1, "sha256Hash": hash_value}}}})
encoded = urllib.parse.urlencode({{"operationName": "Viewer", "extensions": extensions}})
request = urllib.request.Request(
f"http://{{HOST}}:{{PORT}}/graphql?{{encoded}}",
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
response.read()
return response.status
except urllib.error.HTTPError as error:
error.read()
return error.code
# The sandbox may resolve policy binary symlinks shortly after the entrypoint
# starts, bumping the policy generation during the first forward request.
# Retry expected-allowed forward requests so the test still targets L7 behavior.
def retry_forward_allowed(label, request_fn):
last_status = None
for attempt in range(6):
last_status = request_fn()
if last_status != 403:
return last_status
DETAILS[f"{{label}}_transient_403_attempts"] = attempt + 1
time.sleep(0.3)
return last_status
def forward_chunked_status(query):
target = f"{{HOST}}:{{PORT}}"
body = json.dumps({{"query": query}}).encode()
chunk = f"{{len(body):x}}\r\n".encode() + body + b"\r\n0\r\n\r\n"
with socket.create_connection((HOST, PORT), timeout=15) as sock:
request = (
f"POST /graphql HTTP/1.1\r\n"
f"Host: {{target}}\r\n"
f"Content-Type: application/json\r\n"
f"Transfer-Encoding: chunked\r\n"
f"Connection: close\r\n"
f"\r\n"
).encode() + chunk
sock.sendall(request)
response, body = read_response(sock)
DETAILS["forward_chunked_query_allowed_detail"] = body.decode(errors="replace")
return status_code(response, "forward_chunked_response")
def read_until(sock, marker):
data = b""
while marker not in data:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
return data
def read_response(sock):
response = read_until(sock, b"\r\n\r\n")
headers, _, body = response.partition(b"\r\n\r\n")
content_length = 0
for line in headers.split(b"\r\n")[1:]:
if line.lower().startswith(b"content-length:"):
content_length = int(line.split(b":", 1)[1].strip())
break
while len(body) < content_length:
chunk = sock.recv(4096)
if not chunk:
break
body += chunk
return response, body
def status_code(response, label):
parts = response.split()
if len(parts) < 2:
DETAILS[f"{{label}}_raw"] = response.decode(errors="replace")
raise RuntimeError(f"{{label}}: malformed HTTP response: {{response!r}}")
try:
return int(parts[1])
except ValueError as error:
DETAILS[f"{{label}}_raw"] = response.decode(errors="replace")
raise RuntimeError(f"{{label}}: non-numeric HTTP status: {{response!r}}") from error
def raw_http_status(label, request):
last_error = None
for attempt in range(5):
try:
with socket.create_connection((HOST, PORT), timeout=15) as sock:
sock.sendall(request)
sock.shutdown(socket.SHUT_WR)
response = read_until(sock, b"\r\n\r\n")
return status_code(response, f"{{label}}_response")
except (OSError, RuntimeError) as error:
last_error = error
DETAILS[f"{{label}}_attempt_{{attempt + 1}}_error"] = str(error)
time.sleep(0.2)
raise RuntimeError(f"{{label}}: failed after 5 attempts: {{last_error}}")
def raw_status(query, label):
target = f"{{HOST}}:{{PORT}}"
body = json.dumps({{"query": query}}).encode()
request = (
f"POST /graphql HTTP/1.1\r\n"
f"Host: {{target}}\r\n"
f"Content-Type: application/json\r\n"
f"Content-Length: {{len(body)}}\r\n"
f"Connection: close\r\n"
f"\r\n"
).encode() + body
return raw_http_status(label, request)
def raw_get_status(query, label):
target = f"{{HOST}}:{{PORT}}"
encoded = urllib.parse.urlencode({{"query": query}})
request = (
f"GET /graphql?{{encoded}} HTTP/1.1\r\n"
f"Host: {{target}}\r\n"
f"Connection: close\r\n"
f"\r\n"
).encode()
return raw_http_status(label, request)
def raw_duplicate_get_status():
target = f"{{HOST}}:{{PORT}}"
safe = urllib.parse.quote_plus(QUERY_VIEWER)
unsafe = urllib.parse.quote_plus(MUTATION_DELETE)
request = (
f"GET /graphql?query={{safe}}&query={{unsafe}} HTTP/1.1\r\n"
f"Host: {{target}}\r\n"
f"Connection: close\r\n"
f"\r\n"
).encode()
return raw_http_status("raw_duplicate_get_denied", request)
def raw_persisted_get_status(hash_value, label):
target = f"{{HOST}}:{{PORT}}"
extensions = json.dumps({{"persistedQuery": {{"version": 1, "sha256Hash": hash_value}}}})
encoded = urllib.parse.urlencode({{"operationName": "Viewer", "extensions": extensions}})
request = (
f"GET /graphql?{{encoded}} HTTP/1.1\r\n"
f"Host: {{target}}\r\n"
f"Connection: close\r\n"
f"\r\n"
).encode()
return raw_http_status(label, request)
def raw_chunked_status(query):
target = f"{{HOST}}:{{PORT}}"
body = json.dumps({{"query": query}}).encode()
chunk = f"{{len(body):x}}\r\n".encode() + body + b"\r\n0\r\n\r\n"
request = (
f"POST /graphql HTTP/1.1\r\n"
f"Host: {{target}}\r\n"
f"Content-Type: application/json\r\n"
f"Transfer-Encoding: chunked\r\n"
f"Connection: close\r\n"
f"\r\n"
).encode() + chunk
return raw_http_status("raw_chunked_query_allowed", request)
results = {{
"forward_query_allowed": retry_forward_allowed("forward_query_allowed", lambda: forward_status(QUERY_VIEWER)),
"forward_get_query_allowed": retry_forward_allowed("forward_get_query_allowed", lambda: forward_get_status(QUERY_VIEWER)),
"forward_duplicate_get_denied": forward_duplicate_get_status(),
"forward_persisted_get_allowed": retry_forward_allowed("forward_persisted_get_allowed", lambda: forward_persisted_get_status("abc123")),
"forward_unregistered_persisted_get_denied": forward_persisted_get_status("missing"),
"forward_chunked_query_allowed": retry_forward_allowed("forward_chunked_query_allowed", lambda: forward_chunked_status(QUERY_VIEWER)),
"forward_unlisted_field_denied": forward_status(QUERY_REPOSITORY),
"forward_mutation_allowed": retry_forward_allowed("forward_mutation_allowed", lambda: forward_status(MUTATION_CREATE)),
"forward_deny_rule_denied": forward_status(MUTATION_DELETE),
"raw_query_allowed": raw_status(QUERY_VIEWER, "raw_query_allowed"),
"raw_get_query_allowed": raw_get_status(QUERY_VIEWER, "raw_get_query_allowed"),
"raw_duplicate_get_denied": raw_duplicate_get_status(),
"raw_persisted_get_allowed": raw_persisted_get_status("abc123", "raw_persisted_get_allowed"),
"raw_unregistered_persisted_get_denied": raw_persisted_get_status("missing", "raw_unregistered_persisted_get_denied"),
"raw_chunked_query_allowed": raw_chunked_status(QUERY_VIEWER),
"raw_unlisted_field_denied": raw_status(QUERY_REPOSITORY, "raw_unlisted_field_denied"),
"raw_mutation_allowed": raw_status(MUTATION_CREATE, "raw_mutation_allowed"),
"raw_deny_rule_denied": raw_status(MUTATION_DELETE, "raw_deny_rule_denied"),
}}
results.update(DETAILS)
print(json.dumps(results, sort_keys=True))
"#,
host = server.host,
port = server.port,
);
let guard = SandboxGuard::create(&["--policy", &policy_path, "--", "python3", "-c", &script])
.await
.expect("sandbox create");
for (key, expected) in [
("forward_query_allowed", 200),
("forward_get_query_allowed", 200),
("forward_duplicate_get_denied", 403),
("forward_persisted_get_allowed", 200),
("forward_unregistered_persisted_get_denied", 403),
("forward_chunked_query_allowed", 200),
("forward_unlisted_field_denied", 403),
("forward_mutation_allowed", 200),
("forward_deny_rule_denied", 403),
("raw_query_allowed", 200),
("raw_get_query_allowed", 200),
("raw_duplicate_get_denied", 403),
("raw_persisted_get_allowed", 200),
("raw_unregistered_persisted_get_denied", 403),
("raw_chunked_query_allowed", 200),
("raw_unlisted_field_denied", 403),
("raw_mutation_allowed", 200),
("raw_deny_rule_denied", 403),
] {
let expected_fragment = format!(r#""{key}": {expected}"#);
assert!(
guard.create_output.contains(&expected_fragment),
"expected {key}={expected}, got:\n{}",
guard.create_output
);
}
}