206 lines
7.7 KiB
Rust
206 lines
7.7 KiB
Rust
//! Transform one outgoing request. Never expose script exception text to the world.
|
|
use super::{
|
|
error::{Error, Result},
|
|
items::Item,
|
|
requests::GrantRequest,
|
|
script::{Limits, Script},
|
|
store::Cred,
|
|
types::Caller,
|
|
};
|
|
use http::request::Parts;
|
|
use serde_json::{Value, json};
|
|
pub enum Outcome {
|
|
Forward(Parts),
|
|
Denied { item: String, request: String },
|
|
}
|
|
fn matches(item: &Item, p: &Parts) -> bool {
|
|
matches_rules(&item.rules, p)
|
|
}
|
|
fn matches_rules(rules: &[super::items::Rule], p: &Parts) -> bool {
|
|
if !safe_uri(&p.uri) {
|
|
return false;
|
|
}
|
|
rules.iter().any(|r| {
|
|
super::items::rule_matches_port(
|
|
r,
|
|
p.uri.scheme_str().unwrap_or(""),
|
|
p.uri.host().unwrap_or(""),
|
|
p.uri.path(),
|
|
p.method.as_str(),
|
|
p.uri.port_u16(),
|
|
)
|
|
})
|
|
}
|
|
fn safe_uri(uri: &http::Uri) -> bool {
|
|
uri.authority().is_some_and(|a| !a.as_str().contains('@'))
|
|
&& super::items::safe_path(uri.path())
|
|
}
|
|
fn request(p: &Parts) -> Result<Value> {
|
|
let mut headers = Vec::new();
|
|
for (name, value) in &p.headers {
|
|
headers.push(json!({"name":name.as_str(),"value":value.to_str().map_err(|_|Error("invalid_headers"))?}));
|
|
}
|
|
Ok(json!({"method":p.method.as_str(),"url":p.uri.to_string(),"headers":headers}))
|
|
}
|
|
fn evaluate(script: &dyn Script, source: &str, context: &Value) -> Result<Value> {
|
|
let input = serde_json::to_string(context)?;
|
|
let result = script
|
|
.evaluate(source, &input, None, Limits::default())
|
|
.map_err(|_| Error("script"))?;
|
|
serde_json::from_str(&result).map_err(|_| Error("script"))
|
|
}
|
|
impl Cred {
|
|
pub fn inject(
|
|
&self,
|
|
mut parts: Parts,
|
|
caller: &Caller,
|
|
script: &dyn Script,
|
|
) -> Result<Outcome> {
|
|
if !safe_uri(&parts.uri) {
|
|
return Err(Error("invalid_url"));
|
|
}
|
|
let candidates: Vec<_> = {
|
|
let s = self.access()?;
|
|
s.rules
|
|
.iter()
|
|
.filter(|(_, (_, rules))| matches_rules(rules, &parts))
|
|
.map(|(id, (version, _))| (id.clone(), *version))
|
|
.collect()
|
|
};
|
|
if candidates.len() > 1 {
|
|
return Err(Error("ambiguous_item"));
|
|
}
|
|
let Some((id, version)) = candidates.into_iter().next() else {
|
|
return Ok(Outcome::Forward(parts));
|
|
};
|
|
let item = self.item_get(&id)?;
|
|
if item.version != version {
|
|
return Err(Error("stale"));
|
|
}
|
|
let original_request = request(&parts)?;
|
|
if item
|
|
.fields
|
|
.iter()
|
|
.any(|f| f.field_type == super::types::FieldType::SshPrivateKey)
|
|
{
|
|
return Err(Error("ssh_item"));
|
|
}
|
|
if item
|
|
.fields
|
|
.iter()
|
|
.any(|f| f.field_type == super::types::FieldType::Oauth2RefreshToken)
|
|
{
|
|
return Err(Error("oauth2_item"));
|
|
}
|
|
let prepared = self.reserve(&item, caller)?;
|
|
let Some((grant, secret_credential, audit_id)) = prepared else {
|
|
let request = self.file_request(
|
|
caller,
|
|
&format!("inject:{}", item.id),
|
|
GrantRequest {
|
|
items: vec![item.id.clone()],
|
|
duration_secs: None,
|
|
max_uses: None,
|
|
validator: None,
|
|
},
|
|
)?;
|
|
return Ok(Outcome::Denied {
|
|
item: item.name,
|
|
request,
|
|
});
|
|
};
|
|
let result = (|| {
|
|
let public = self.credential(&item, false)?;
|
|
let ctx = json!({"request":original_request,"credential":public,"item":{"id":item.id,"name":item.name},"grant":grant.as_ref().map(|g|json!({"conversation":match &g.subject {super::types::Subject::Conversation(id)=>Some(id),super::types::Subject::Program(_)=>None},"subject":g.subject,"uses_left":g.max_uses.map(|m|m-g.uses),"exp":g.exp}))});
|
|
for source in [
|
|
item.validate_js.as_deref(),
|
|
grant.as_ref().and_then(|g| g.validator.as_deref()),
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
{
|
|
let value = evaluate(script, source, &ctx)?;
|
|
let allow = value
|
|
.as_bool()
|
|
.or_else(|| value.get("allow").and_then(Value::as_bool))
|
|
.ok_or(Error("script"))?;
|
|
if !allow {
|
|
return Err(Error("validator_denied"));
|
|
}
|
|
}
|
|
if !item.transform_js.is_empty() {
|
|
let mut ctx = json!({"request":original_request,"credential":secret_credential});
|
|
let url =
|
|
url::Url::parse(&parts.uri.to_string()).map_err(|_| Error("invalid_url"))?;
|
|
let mut base = url.clone();
|
|
base.set_query(None);
|
|
base.set_fragment(None);
|
|
ctx["url"] = json!({"base":base.as_str(),"query":url.query_pairs().collect::<Vec<_>>(),"fragment":url.fragment().map(|f|format!("#{f}")).unwrap_or_default()});
|
|
let output = evaluate(script, &item.transform_js, &ctx)?;
|
|
let object = output.as_object().ok_or(Error("transform"))?;
|
|
if object
|
|
.keys()
|
|
.any(|k| k != "url" && k != "headers" && k != "method")
|
|
|| output
|
|
.get("method")
|
|
.is_some_and(|m| m.as_str() != Some(parts.method.as_str()))
|
|
{
|
|
return Err(Error("transform"));
|
|
}
|
|
if let Some(url) = output.get("url") {
|
|
parts.uri = url
|
|
.as_str()
|
|
.ok_or(Error("transform"))?
|
|
.parse()
|
|
.map_err(|_| Error("transform"))?;
|
|
}
|
|
if let Some(headers) = output.get("headers") {
|
|
let mut new = http::HeaderMap::new();
|
|
for header in headers.as_array().ok_or(Error("transform"))? {
|
|
let name = header
|
|
.get("name")
|
|
.and_then(Value::as_str)
|
|
.ok_or(Error("transform"))?;
|
|
let value = header
|
|
.get("value")
|
|
.and_then(Value::as_str)
|
|
.ok_or(Error("transform"))?;
|
|
let mut value =
|
|
http::HeaderValue::from_str(value).map_err(|_| Error("transform"))?;
|
|
value.set_sensitive(true);
|
|
new.append(
|
|
http::header::HeaderName::from_bytes(name.as_bytes())
|
|
.map_err(|_| Error("transform"))?,
|
|
value,
|
|
);
|
|
}
|
|
parts.headers = new;
|
|
}
|
|
}
|
|
if !matches(&item, &parts) {
|
|
return Err(Error("rule_recheck"));
|
|
}
|
|
let authority = parts.uri.authority().ok_or(Error("rule_recheck"))?;
|
|
parts.headers.insert(
|
|
http::header::HOST,
|
|
http::HeaderValue::from_str(authority.as_str())
|
|
.map_err(|_| Error("rule_recheck"))?,
|
|
);
|
|
Ok(())
|
|
})();
|
|
self.access()?.db.execute(
|
|
"UPDATE audit SET event=?1 WHERE id=?2",
|
|
rusqlite::params![
|
|
if result.is_ok() {
|
|
"fetch.use"
|
|
} else {
|
|
"fetch.denied"
|
|
},
|
|
audit_id
|
|
],
|
|
)?;
|
|
result?;
|
|
Ok(Outcome::Forward(parts))
|
|
}
|
|
}
|