112 lines
3.6 KiB
Rust
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(_)
|
|
));
|
|
}
|