Files
sausage/host/tests/it/cred_accounting.rs
T

112 lines
3.6 KiB
Rust

//! 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<String, script::Error> {
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(_)
));
}