Files
sausage/host/tests/cred.rs
T

299 lines
8.7 KiB
Rust

//! Credential core acceptance tests with a script fake and host-only temporary storage.
use sausage::cred::{
grants::Grant,
inject::Outcome,
items::*,
requests::{GrantRequest, Overrides},
script::{self, Limits, Script},
*,
};
fn caller(id: &str) -> Caller {
Caller {
program: "/bin/tool.wasm".into(),
pid: 42,
conversations: vec![id.into()],
}
}
fn item(free_use: bool) -> Item {
Item {
id: String::new(),
name: "token".into(),
version: 0,
free_use,
rules: vec![Rule {
port: None,
scheme: "https".into(),
hosts: vec!["api.example".into()],
path_prefixes: vec!["/".into()],
methods: None,
}],
validate_js: None,
transform_js: String::new(),
fields: vec![Field {
field_id: String::new(),
name: "token".into(),
public: false,
value: None,
needs_value: true,
field_type: types::FieldType::Text,
key_type: None,
}],
}
}
fn parts(host: &str) -> http::request::Parts {
http::Request::get(format!("https://{host}/path"))
.body(())
.unwrap()
.into_parts()
.0
}
struct Fake;
impl Script for Fake {
fn evaluate(
&self,
source: &str,
input: &str,
_: Option<&str>,
_: Limits,
) -> std::result::Result<String, script::Error> {
let ctx: serde_json::Value = serde_json::from_str(input).unwrap();
match source {
"deny"=>Ok("false".into()),
"validate"=>{assert!(ctx["credential"].get("token").is_none());Ok("true".into())},
"throw"=>Err(script::Error{kind:script::ErrorKind::Throw,message:ctx["credential"]["token"].as_str().unwrap().into()}),
"escape"=>Ok(r#"{"url":"https://evil.example/"}"#.into()),
_=>Ok(serde_json::json!({"headers":[{"name":"authorization","value":ctx["credential"]["token"]}]}).to_string())
}
}
}
#[test]
fn inject_selection_idempotency_and_subtree() {
let dir = tempfile::tempdir().unwrap();
let c = Cred::open(dir.path()).unwrap();
let root = caller("root");
assert!(matches!(
c.inject(parts("api.example"), &root, &Fake).unwrap(),
Outcome::Forward(_)
));
let i = c
.create_item(item(false), &root, c.unlocked().unwrap())
.unwrap();
let first = match c.inject(parts("api.example"), &root, &Fake).unwrap() {
Outcome::Denied { item, request } => {
assert_eq!(item, "token");
request
}
_ => panic!("expected denial"),
};
let second = match c.inject(parts("api.example"), &root, &Fake).unwrap() {
Outcome::Denied { request, .. } => request,
_ => panic!("expected denial"),
};
assert_eq!(first, second);
assert_eq!(c.request_list().unwrap().len(), 1);
c.set_field(
&i.id,
&i.fields[0].field_id,
"opaque",
&root,
c.unlocked().unwrap(),
)
.unwrap();
c.approve(
&first,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
c.unlocked().unwrap(),
)
.unwrap();
let child = Caller {
program: "/bin/tool.wasm".into(),
pid: 43,
conversations: vec!["child".into(), "root".into()],
};
assert!(matches!(
c.inject(parts("api.example"), &child, &Fake).unwrap(),
Outcome::Forward(_)
));
assert!(matches!(
c.inject(parts("api.example"), &caller("sibling"), &Fake)
.unwrap(),
Outcome::Denied { .. }
));
let mut other = item(true);
other.name = "other".into();
c.create_item(other, &root, c.unlocked().unwrap()).unwrap();
assert_eq!(
c.inject(parts("api.example"), &root, &Fake)
.err()
.unwrap()
.0,
"ambiguous_item"
);
assert_eq!(c.grant_list().unwrap()[0].items, vec![i.id]);
}
#[test]
fn free_use_validators_transform_recheck_and_redaction() {
for (source, validator, expected) in [
("inject", Some("validate"), None),
("inject", Some("deny"), Some("validator_denied")),
("escape", None, Some("rule_recheck")),
("throw", None, Some("script")),
] {
let dir = tempfile::tempdir().unwrap();
let c = Cred::open(dir.path()).unwrap();
let caller = caller("root");
let mut config = item(true);
config.transform_js = source.into();
config.validate_js = validator.map(Into::into);
let i = c
.create_item(config, &caller, c.unlocked().unwrap())
.unwrap();
let secret = "SECRET_SENTINEL_19";
c.set_field(
&i.id,
&i.fields[0].field_id,
secret,
&caller,
c.unlocked().unwrap(),
)
.unwrap();
let result = c.inject(parts("api.example"), &caller, &Fake);
if let Some(expected) = expected {
let error = result.err().unwrap();
assert_eq!(error.0, expected);
assert!(!format!("{error:?}").contains(secret));
} else {
let Outcome::Forward(p) = result.unwrap() else {
panic!("denied")
};
assert!(p.headers["authorization"] == secret);
}
c.audit_sign(&i.id, &caller, secret.as_bytes()).unwrap();
assert!(!format!("{:?}", c.item_get(&i.id).unwrap()).contains(secret));
assert!(
!serde_json::to_string(&c.audit_list().unwrap())
.unwrap()
.contains(secret)
);
let id = c
.file_request(
&caller,
"review",
GrantRequest {
items: vec![i.id.clone()],
duration_secs: None,
max_uses: None,
validator: None,
},
)
.unwrap();
assert!(!format!("{:?}", c.request_diff(&id).unwrap()).contains(secret));
}
}
#[test]
fn atomic_budget_and_decision_notification() {
let dir = tempfile::tempdir().unwrap();
let c = std::sync::Arc::new(Cred::open(dir.path()).unwrap());
let caller = caller("root");
let i = c
.create_item(item(false), &caller, c.unlocked().unwrap())
.unwrap();
c.set_field(
&i.id,
&i.fields[0].field_id,
"opaque",
&caller,
c.unlocked().unwrap(),
)
.unwrap();
c.grant_create(
Grant {
id: String::new(),
items: vec![i.id],
subject: types::Subject::Conversation("root".into()),
exp: None,
max_uses: Some(1),
uses: 0,
revoked: false,
validator: None,
note: None,
},
&caller,
c.unlocked().unwrap(),
)
.unwrap();
let handles: Vec<_> = (0..2)
.map(|_| {
let c = c.clone();
let caller = caller.clone();
std::thread::spawn(move || {
matches!(
c.inject(parts("api.example"), &caller, &Fake).unwrap(),
Outcome::Forward(_)
)
})
})
.collect();
assert_eq!(
handles
.into_iter()
.map(|h| usize::from(h.join().unwrap()))
.sum::<usize>(),
1
);
let id = c.request_list().unwrap()[0].id.clone();
let mut receiver = c.subscribe();
c.deny(
&id,
sausage::cred::webauthn::Decision::Deny,
c.unlocked().unwrap(),
)
.unwrap();
assert_eq!(receiver.try_recv().unwrap(), id);
assert_eq!(
c.deny(
&id,
sausage::cred::webauthn::Decision::Deny,
c.unlocked().unwrap()
)
.unwrap_err()
.0,
"already_decided"
);
}
#[test]
fn key_and_secret_persist() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let c = Cred::open(dir.path()).unwrap();
let caller = caller("root");
let mut config = item(true);
config.transform_js = "inject".into();
let i = c
.create_item(config, &caller, c.unlocked().unwrap())
.unwrap();
c.set_field(
&i.id,
&i.fields[0].field_id,
"persistent",
&caller,
c.unlocked().unwrap(),
)
.unwrap();
drop(c);
assert_eq!(
std::fs::metadata(dir.path().join("cred.key"))
.unwrap()
.permissions()
.mode()
& 0o777,
0o600
);
let c = Cred::open(dir.path()).unwrap();
let Outcome::Forward(p) = c.inject(parts("api.example"), &caller, &Fake).unwrap() else {
panic!("denied")
};
assert!(p.headers["authorization"] == "persistent");
}