Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
test(wash-runtime): integration tests for AllowedHost
- `integration_http_allowed_hosts`: cover the four AllowedHost shapes
  end-to-end. Add `test_star_any_permits_all` (literal `*` allows
  everything), `test_url_policy_pins_scheme` (URL form rejects wrong
  scheme), and flip `test_empty_allowed_hosts_permits_all` →
  `test_empty_allowed_hosts_denies_all`.
- `integration_http_counter` / `integration_http_tls` / `tests/common`:
  populate `LocalResources.allowed_hosts` with `["example.com"]` since
  the http-counter fixture makes outgoing calls to example.com and the
  empty-list default now denies. Encoding the actual target host
  documents the test's intent precisely.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
  • Loading branch information
ricochet committed May 21, 2026
commit 584be7e11e3fb616f859ea6ab62985f7f39ea538
5 changes: 4 additions & 1 deletion crates/wash-runtime/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,16 @@ pub fn component_workload_request(
}

pub fn default_counter_resources() -> LocalResources {
// http-counter calls example.com — encode that exact host in the
// policy so the deny-all default doesn't block it AND the test
// documents which upstream the fixture talks to.
LocalResources {
memory_limit_mb: 256,
cpu_limit: 1,
config: HashMap::new(),
environment: HashMap::new(),
volume_mounts: vec![],
allowed_hosts: Default::default(),
allowed_hosts: vec!["example.com".parse().unwrap()].into(),
}
}

Expand Down
91 changes: 82 additions & 9 deletions crates/wash-runtime/tests/integration_http_allowed_hosts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ async fn start_host(addr: &str) -> Result<(std::net::SocketAddr, impl HostApi)>
}

fn allowed_hosts_workload(allowed_hosts: Vec<String>) -> WorkloadStartRequest {
let parsed: Vec<wash_runtime::host::allowed_hosts::AllowedHost> = allowed_hosts
.iter()
.map(|s| s.parse().expect("test gave invalid allowed_hosts entry"))
.collect();
WorkloadStartRequest {
workload_id: uuid::Uuid::new_v4().to_string(),
workload: Workload {
Expand All @@ -56,7 +60,7 @@ fn allowed_hosts_workload(allowed_hosts: Vec<String>) -> WorkloadStartRequest {
config: HashMap::new(),
environment: HashMap::new(),
volume_mounts: vec![],
allowed_hosts: allowed_hosts.into(),
allowed_hosts: parsed.into(),
},
pool_size: 1,
max_invocations: 100,
Expand Down Expand Up @@ -191,20 +195,90 @@ async fn test_allowed_hosts_wildcard() -> Result<()> {
Ok(())
}

/// When allowed_hosts is empty, all outgoing requests should be permitted.
/// Literal `*` (AllowedHost::Any) lets every host through, same as
/// an empty list but exercises the explicit `Any` variant rather than
/// the empty-list shortcut. Important because the wash config layer
/// resolves missing `allowed_hosts` to `[Any]` rather than an empty list.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_empty_allowed_hosts_permits_all() -> Result<()> {
async fn test_star_any_permits_all() -> Result<()> {
let (addr, host) = start_host("127.0.0.1:0").await?;

// Empty allowed_hosts = no restrictions
let req = allowed_hosts_workload(vec!["*".to_string()]);
host.workload_start(req)
.await
.context("Failed to start workload")?;

let client = reqwest::Client::new();
for path in ["/wiki", "/example"] {
let response = timeout(
Duration::from_secs(10),
client
.get(format!("http://{addr}{path}"))
.header("HOST", "test")
.send(),
)
.await
.context(format!("{path} request timed out"))?
.context(format!("Failed to make {path} request"))?;

assert_ne!(
response.status().as_u16(),
500,
"With allowed_hosts=['*'], {path} should not be blocked by policy"
);
}
Ok(())
}

/// URL-form policy pins scheme. `/example` hits `http://example.com`; the
/// policy below allows `https://example.com` only. The request should be
/// blocked because the schemes differ — the host alone isn't enough.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_url_policy_pins_scheme() -> Result<()> {
let (addr, host) = start_host("127.0.0.1:0").await?;

let req = allowed_hosts_workload(vec!["https://example.com".to_string()]);
host.workload_start(req)
.await
.context("Failed to start workload")?;

let client = reqwest::Client::new();
let response = timeout(
Duration::from_secs(10),
client
.get(format!("http://{addr}/example"))
.header("HOST", "test")
.send(),
)
.await
.context("Example request timed out")?
.context("Failed to make example request")?;

assert_eq!(
response.status().as_u16(),
500,
"http://example.com should be blocked when policy is https://example.com"
);
Ok(())
}

/// An empty `allowed_hosts` list denies all outgoing requests.
/// Callers that want unrestricted egress must use the explicit `["*"]` form,
/// which the wash config layer applies automatically when `allowedHosts` is
/// omitted from YAML (see [`test_star_any_permits_all`]).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_empty_allowed_hosts_denies_all() -> Result<()> {
let (addr, host) = start_host("127.0.0.1:0").await?;

// Empty allowed_hosts = deny all egress.
let req = allowed_hosts_workload(vec![]);
host.workload_start(req)
.await
.context("Failed to start workload")?;

let client = reqwest::Client::new();

// Both routes should be permitted (not blocked by policy)
// Both routes should be BLOCKED by the empty-list deny-all policy.
for path in ["/wiki", "/example"] {
let response = timeout(
Duration::from_secs(10),
Expand All @@ -217,11 +291,10 @@ async fn test_empty_allowed_hosts_permits_all() -> Result<()> {
.context(format!("{path} request timed out"))?
.context(format!("Failed to make {path} request"))?;

let status = response.status();
assert_ne!(
status.as_u16(),
assert_eq!(
response.status().as_u16(),
500,
"With empty allowed_hosts, {path} should not be blocked by policy"
"Empty allowed_hosts should deny all egress; {path} unexpectedly succeeded"
);
}

Expand Down
3 changes: 2 additions & 1 deletion crates/wash-runtime/tests/integration_http_counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ async fn test_http_counter_integration() -> Result<()> {
]),
environment: HashMap::new(),
volume_mounts: vec![],
allowed_hosts: Default::default(),
// http-counter calls example.com
allowed_hosts: vec!["example.com".parse().unwrap()].into(),
},
);

Expand Down
3 changes: 2 additions & 1 deletion crates/wash-runtime/tests/integration_http_tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ fn http_counter_request(host_header: &str) -> WorkloadStartRequest {
]),
environment: HashMap::new(),
volume_mounts: vec![],
allowed_hosts: Default::default(),
// http-counter calls example.com
allowed_hosts: vec!["example.com".parse().unwrap()].into(),
},
http_counter_host_interfaces(host_header),
)
Expand Down
3 changes: 2 additions & 1 deletion crates/wash-runtime/tests/integration_p2_p3_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,8 @@ async fn test_p2_regression_with_p3_enabled() -> Result<()> {
]),
environment: HashMap::new(),
volume_mounts: vec![],
allowed_hosts: Default::default(),
// http-counter calls example.com
allowed_hosts: vec!["example.com".parse().unwrap()].into(),
},
pool_size: 1,
max_invocations: 100,
Expand Down
9 changes: 7 additions & 2 deletions crates/wash-runtime/tests/integration_wasip3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ async fn test_p2_http_component_works_with_p3_enabled() -> Result<()> {
]),
environment: HashMap::new(),
volume_mounts: vec![],
allowed_hosts: Default::default(),
// http-counter calls example.com — empty-list default would deny.
allowed_hosts: vec!["example.com".parse().unwrap()].into(),
},
pool_size: 1,
max_invocations: 100,
Expand Down Expand Up @@ -217,7 +218,11 @@ async fn test_p2_concurrent_requests_with_p3_enabled() -> Result<()> {
name: "http-counter.wasm".to_string(),
digest: None,
bytes: bytes::Bytes::from_static(HTTP_COUNTER_WASM),
local_resources: LocalResources::default(),
local_resources: LocalResources {
// http-counter calls example.com.
allowed_hosts: vec!["example.com".parse().unwrap()].into(),
..Default::default()
},
pool_size: 1,
max_invocations: 100,
}],
Expand Down