//! 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 { 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::(), 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"); }