Skip to content

Commit 2175cd8

Browse files
committed
test(policy): cover forward graphql chunked proxy
1 parent 212fb77 commit 2175cd8

2 files changed

Lines changed: 123 additions & 13 deletions

File tree

crates/openshell-sandbox/src/l7/graphql.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,4 +696,113 @@ mod tests {
696696
assert!(!forwarded.to_ascii_lowercase().contains("trailer:"));
697697
assert!(req.raw_header.ends_with(body));
698698
}
699+
700+
#[tokio::test]
701+
async fn absolute_form_chunked_graphql_post_classifies_after_inspection() {
702+
let body = br#"{"query":"query Viewer { viewer { login } }"}"#;
703+
let mut raw_header =
704+
b"POST http://example.com/graphql HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n"
705+
.to_vec();
706+
raw_header.extend_from_slice(format!("{:x}\r\n", body.len()).as_bytes());
707+
raw_header.extend_from_slice(body);
708+
raw_header.extend_from_slice(b"\r\n0\r\n\r\n");
709+
710+
let mut req = L7Request {
711+
action: "POST".to_string(),
712+
target: "/graphql".to_string(),
713+
query_params: HashMap::new(),
714+
raw_header,
715+
body_length: BodyLength::Chunked,
716+
};
717+
let mut client = tokio::io::empty();
718+
719+
let info = inspect_graphql_request(&mut client, &mut req, DEFAULT_MAX_BODY_BYTES)
720+
.await
721+
.expect("absolute-form chunked body should inspect");
722+
723+
assert_eq!(info.error, None);
724+
assert_eq!(info.operations[0].operation_type, "query");
725+
assert_eq!(info.operations[0].fields, vec!["viewer"]);
726+
}
727+
728+
#[tokio::test]
729+
async fn absolute_form_chunked_graphql_post_is_allowed_by_field_policy() {
730+
let body = br#"{"query":"query Viewer { viewer { login } }"}"#;
731+
let mut raw_header =
732+
b"POST http://host.openshell.internal:8080/graphql HTTP/1.1\r\nHost: host.openshell.internal:8080\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n"
733+
.to_vec();
734+
raw_header.extend_from_slice(format!("{:x}\r\n", body.len()).as_bytes());
735+
raw_header.extend_from_slice(body);
736+
raw_header.extend_from_slice(b"\r\n0\r\n\r\n");
737+
738+
let mut req = L7Request {
739+
action: "POST".to_string(),
740+
target: "/graphql".to_string(),
741+
query_params: HashMap::new(),
742+
raw_header,
743+
body_length: BodyLength::Chunked,
744+
};
745+
let mut client = tokio::io::empty();
746+
let info = inspect_graphql_request(&mut client, &mut req, DEFAULT_MAX_BODY_BYTES)
747+
.await
748+
.expect("chunked body should inspect");
749+
750+
let data = r"
751+
network_policies:
752+
test_graphql_l7:
753+
name: test_graphql_l7
754+
endpoints:
755+
- host: host.openshell.internal
756+
port: 8080
757+
protocol: graphql
758+
enforcement: enforce
759+
persisted_queries: allow_registered
760+
graphql_persisted_queries:
761+
abc123:
762+
operation_type: query
763+
operation_name: Viewer
764+
fields: [viewer]
765+
rules:
766+
- allow:
767+
operation_type: query
768+
fields: [viewer]
769+
- allow:
770+
operation_type: mutation
771+
fields: [createIssue]
772+
deny_rules:
773+
- operation_type: mutation
774+
fields: [deleteRepository]
775+
binaries:
776+
- { path: /usr/bin/python3 }
777+
";
778+
let engine = crate::opa::OpaEngine::from_strings(
779+
include_str!("../../data/sandbox-policy.rego"),
780+
data,
781+
)
782+
.expect("policy should load");
783+
let ctx = crate::l7::relay::L7EvalContext {
784+
host: "host.openshell.internal".to_string(),
785+
port: 8080,
786+
policy_name: "test_graphql_l7".to_string(),
787+
binary_path: "/usr/bin/python3".to_string(),
788+
ancestors: Vec::new(),
789+
cmdline_paths: Vec::new(),
790+
secret_resolver: None,
791+
};
792+
let request_info = crate::l7::L7RequestInfo {
793+
action: req.action,
794+
target: req.target,
795+
query_params: req.query_params,
796+
graphql: Some(info),
797+
};
798+
799+
let (allowed, reason) = crate::l7::relay::evaluate_l7_request(
800+
&std::sync::Mutex::new(engine.clone_engine_for_tunnel().unwrap()),
801+
&ctx,
802+
&request_info,
803+
)
804+
.expect("evaluation should complete");
805+
806+
assert!(allowed, "expected query to be allowed, got {reason}");
807+
}
699808
}

e2e/rust/tests/forward_proxy_graphql_l7.rs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -289,18 +289,19 @@ def forward_persisted_get_status(hash_value):
289289
error.read()
290290
return error.code
291291
292-
def proxy_parts():
293-
proxy_url = (
294-
os.environ.get("HTTPS_PROXY")
295-
or os.environ.get("https_proxy")
296-
or os.environ.get("HTTP_PROXY")
297-
or os.environ.get("http_proxy")
298-
)
292+
def proxy_parts(*names):
293+
proxy_url = next((os.environ.get(name) for name in names if os.environ.get(name)), None)
299294
parsed = urllib.parse.urlparse(proxy_url)
300295
return parsed.hostname, parsed.port or 80
301296
297+
def forward_proxy_parts():
298+
return proxy_parts("HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy")
299+
300+
def connect_proxy_parts():
301+
return proxy_parts("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy")
302+
302303
def forward_chunked_status(query):
303-
proxy_host, proxy_port = proxy_parts()
304+
proxy_host, proxy_port = forward_proxy_parts()
304305
target = f"{{HOST}}:{{PORT}}"
305306
body = json.dumps({{"query": query}}).encode()
306307
chunk = f"{{len(body):x}}\r\n".encode() + body + b"\r\n0\r\n\r\n"
@@ -329,7 +330,7 @@ def read_until(sock, marker):
329330
return data
330331
331332
def connect_status(query):
332-
proxy_host, proxy_port = proxy_parts()
333+
proxy_host, proxy_port = connect_proxy_parts()
333334
target = f"{{HOST}}:{{PORT}}"
334335
body = json.dumps({{"query": query}}).encode()
335336
@@ -355,7 +356,7 @@ def connect_status(query):
355356
return int(response.split()[1])
356357
357358
def connect_get_status(query):
358-
proxy_host, proxy_port = proxy_parts()
359+
proxy_host, proxy_port = connect_proxy_parts()
359360
target = f"{{HOST}}:{{PORT}}"
360361
encoded = urllib.parse.urlencode({{"query": query}})
361362
@@ -379,7 +380,7 @@ def connect_get_status(query):
379380
return int(response.split()[1])
380381
381382
def connect_duplicate_get_status():
382-
proxy_host, proxy_port = proxy_parts()
383+
proxy_host, proxy_port = connect_proxy_parts()
383384
target = f"{{HOST}}:{{PORT}}"
384385
safe = urllib.parse.quote_plus(QUERY_VIEWER)
385386
unsafe = urllib.parse.quote_plus(MUTATION_DELETE)
@@ -404,7 +405,7 @@ def connect_duplicate_get_status():
404405
return int(response.split()[1])
405406
406407
def connect_persisted_get_status(hash_value):
407-
proxy_host, proxy_port = proxy_parts()
408+
proxy_host, proxy_port = connect_proxy_parts()
408409
target = f"{{HOST}}:{{PORT}}"
409410
extensions = json.dumps({{"persistedQuery": {{"version": 1, "sha256Hash": hash_value}}}})
410411
encoded = urllib.parse.urlencode({{"operationName": "Viewer", "extensions": extensions}})
@@ -429,7 +430,7 @@ def connect_persisted_get_status(hash_value):
429430
return int(response.split()[1])
430431
431432
def connect_chunked_status(query):
432-
proxy_host, proxy_port = proxy_parts()
433+
proxy_host, proxy_port = connect_proxy_parts()
433434
target = f"{{HOST}}:{{PORT}}"
434435
body = json.dumps({{"query": query}}).encode()
435436
chunk = f"{{len(body):x}}\r\n".encode() + body + b"\r\n0\r\n\r\n"

0 commit comments

Comments
 (0)