//! Reservations commit their audit row atomically. Unrelated rows do not enter the hot path. use sausage::cred::{ grants::Grant, inject::Outcome, items::Item, script::{self, Limits, Script}, *, }; struct Noop; impl Script for Noop { fn evaluate( &self, _: &str, _: &str, _: Option<&str>, _: Limits, ) -> std::result::Result { Ok("{}".into()) } } fn config(name: &str, host: &str) -> Item { serde_json::from_value(serde_json::json!({"name":name,"rules":[{"scheme":"https","hosts":[host],"path_prefixes":["/"],"methods":null}],"validate_js":null,"transform_js":"","fields":[]})).unwrap() } fn fixture() -> (tempfile::TempDir, Cred, Caller, Item, Grant) { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller { program: "/bin/test.wasm".into(), pid: 1, conversations: vec!["root".into()], }; let item = c .create_item( config("test", "api.example"), &caller, c.unlocked().unwrap(), ) .unwrap(); let grant = c .grant_create( Grant { id: String::new(), items: vec![item.id.clone()], 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(); (dir, c, caller, item, grant) } fn parts() -> http::request::Parts { http::Request::get("https://api.example/") .body(()) .unwrap() .into_parts() .0 } #[test] fn audit_failure_rolls_back_reservation() { let (dir, c, caller, _item, grant) = fixture(); let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); db.execute_batch("CREATE TRIGGER fail_fetch_audit BEFORE INSERT ON audit WHEN NEW.event LIKE 'fetch.%' BEGIN SELECT RAISE(ABORT,'forced'); END;").unwrap(); assert!(c.inject(parts(), &caller, &Noop).is_err()); assert_eq!(c.grant_status(&grant.id).unwrap().uses, 0); db.execute_batch("DROP TRIGGER fail_fetch_audit").unwrap(); assert!(matches!( c.inject(parts(), &caller, &Noop).unwrap(), Outcome::Forward(_) )); let rows = c.audit_list().unwrap(); assert_eq!(rows.iter().filter(|r| r.event == "fetch.use").count(), 1); } #[test] fn revoked_rows_are_filtered_before_deserialization() { let (dir, c, caller, _item, _grant) = fixture(); let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); db.execute("INSERT INTO grants(id,items,subject,uses,revoked) VALUES('inactive','invalid-json','invalid-json',0,1)",[]).unwrap(); assert!(matches!( c.inject(parts(), &caller, &Noop).unwrap(), Outcome::Forward(_) )); } #[test] fn unmatched_configs_are_not_read_during_injection() { let (dir, c, caller, _item, _grant) = fixture(); let other = c .create_item( config("other", "other.example"), &caller, c.unlocked().unwrap(), ) .unwrap(); let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); // An invalid JSON marker detects an unwanted read of an unrelated config. db.execute( "UPDATE items SET config='invalid-json' WHERE id=?", [other.id], ) .unwrap(); assert!(matches!( c.inject(parts(), &caller, &Noop).unwrap(), Outcome::Forward(_) )); }