use crate::http_request_policy::{
AllowedHostConfig, AllowedHostPolicy, GlobalHttpConfig, HttpRequestPolicy, PlaceholderSecret,
SecretResolver, generate_placeholder,
};
use secrecy::SecretString;
use wasmtime_wasi::WasiCtxBuilder;
pub(crate) fn build_http_policy(
allowed_hosts: &[AllowedHostConfig],
global_http_config: &GlobalHttpConfig,
resolver: &dyn SecretResolver,
wasi_ctx: &mut WasiCtxBuilder,
) -> HttpRequestPolicy {
let (mut policy, placeholders) = build_http_policy_inner(allowed_hosts, resolver);
for (env_key, placeholder) in &placeholders {
wasi_ctx.env(env_key, placeholder);
}
policy.global_allowlist = Some(build_authorization_hosts(global_http_config.entries()));
policy
}
fn build_authorization_hosts(allowed_hosts: &[AllowedHostConfig]) -> Vec<AllowedHostPolicy> {
allowed_hosts
.iter()
.map(|host_config| AllowedHostPolicy {
pattern: host_config.pattern.clone(),
request_url_regex: host_config.request_url_regex.clone(),
secrets: host_config
.secret_names
.iter()
.map(|name| PlaceholderSecret {
name: name.clone(),
placeholder: String::new(),
real_value: SecretString::from(String::new()),
replace_in: host_config.replace_in.clone(),
})
.collect(),
})
.collect()
}
fn build_http_policy_inner(
allowed_hosts: &[AllowedHostConfig],
resolver: &dyn SecretResolver,
) -> (
HttpRequestPolicy,
hashbrown::HashMap<
String, String, >,
) {
let mut by_name: hashbrown::HashMap<String, (String, SecretString)> = hashbrown::HashMap::new();
let mut placeholders: hashbrown::HashMap<String, String> = hashbrown::HashMap::new();
let mut hosts = Vec::with_capacity(allowed_hosts.len());
for host_config in allowed_hosts {
let mut secrets = Vec::with_capacity(host_config.secret_names.len());
for name in &host_config.secret_names {
if !by_name.contains_key(name) {
let Some(value) = resolver.secret_lookup(name) else {
continue;
};
by_name.insert(name.clone(), (generate_placeholder(), value));
}
let (placeholder, value) = &by_name[name];
placeholders.insert(name.clone(), placeholder.clone());
secrets.push(PlaceholderSecret {
name: name.clone(),
placeholder: placeholder.clone(),
real_value: value.clone(),
replace_in: host_config.replace_in.clone(),
});
}
hosts.push(AllowedHostPolicy {
pattern: host_config.pattern.clone(),
request_url_regex: host_config.request_url_regex.clone(),
secrets,
});
}
(
HttpRequestPolicy {
hosts,
global_allowlist: None,
},
placeholders,
)
}
#[cfg(test)]
mod tests {
use super::build_http_policy_inner;
use crate::http_request_policy::{
AllowedHostConfig, HostPattern, MethodsPattern, ReplacementLocation, SecretResolver,
};
use hyper::http::Method;
use secrecy::SecretString;
use wasmtime_wasi_http::p2::body::HyperOutgoingBody;
#[derive(Debug)]
struct MapResolver(hashbrown::HashMap<String, SecretString>);
impl SecretResolver for MapResolver {
fn secret_lookup(&self, name: &str) -> Option<SecretString> {
self.0.get(name).cloned()
}
}
fn empty_body() -> HyperOutgoingBody {
http_body_util::combinators::UnsyncBoxBody::new(http_body_util::BodyExt::map_err(
http_body_util::Empty::<hyper::body::Bytes>::new(),
|_| unreachable!(),
))
}
fn host_config(name: &str, methods: Vec<Method>) -> AllowedHostConfig {
AllowedHostConfig {
pattern: HostPattern::parse_with_methods(
"api.example.com",
MethodsPattern::Specific(methods),
)
.unwrap(),
request_url_regex: None,
secret_names: vec![name.to_string()],
replace_in: hashbrown::HashSet::from([ReplacementLocation::Headers]),
}
}
#[test]
fn shared_secret_across_two_allowed_hosts_substitutes_for_either_entry() {
const ENV_KEY: &str = "OBELISK__API__TOKEN";
const REAL_VALUE: &str = "real-token-value";
let allowed_hosts = vec![
host_config(ENV_KEY, vec![Method::GET]),
host_config(ENV_KEY, vec![Method::PUT]),
];
let resolver = MapResolver(hashbrown::HashMap::from([(
ENV_KEY.to_string(),
SecretString::from(REAL_VALUE),
)]));
let (policy, placeholders) = build_http_policy_inner(&allowed_hosts, &resolver);
assert_eq!(
placeholders.len(),
1,
"env var must be bound exactly once, got: {placeholders:?}"
);
let placeholder = &policy.hosts[0].secrets[0].placeholder;
assert_eq!(&policy.hosts[1].secrets[0].placeholder, placeholder);
assert_eq!(placeholders.get(ENV_KEY), Some(placeholder));
for method in [Method::GET, Method::PUT] {
let mut request = hyper::Request::builder()
.method(method.clone())
.uri("https://api.example.com/v1/items")
.header("authorization", format!("Bearer {placeholder}"))
.body(empty_body())
.unwrap();
policy.apply(&mut request).unwrap();
assert_eq!(
request.headers().get("authorization").unwrap(),
&format!("Bearer {REAL_VALUE}"),
"placeholder not substituted for {method} request"
);
}
}
}