Files
sausage/host/tests/cred_core.rs
T

249 lines
7.3 KiB
Rust

//! Ported rule boundaries and request/edit ledger regressions.
use sausage::cred::{
edits::Edit,
items::*,
requests::{GrantRequest, Overrides},
*,
};
fn fixture() -> (tempfile::TempDir, Cred, Caller, Item) {
let dir = tempfile::tempdir().unwrap();
let c = Cred::open(dir.path()).unwrap();
let caller = Caller {
program: "/bin/tool.wasm".into(),
pid: 7,
conversations: vec!["root".into()],
};
let i = c
.create_item(
Item {
id: String::new(),
name: "item".into(),
version: 0,
free_use: false,
rules: vec![],
validate_js: None,
transform_js: String::new(),
fields: vec![Field {
field_id: String::new(),
name: "secret".into(),
public: false,
value: None,
needs_value: true,
field_type: types::FieldType::Text,
key_type: None,
}],
},
&caller,
c.unlocked().unwrap(),
)
.unwrap();
(dir, c, caller, i)
}
#[test]
fn rule_matching_table() {
let mut rule:Rule=serde_json::from_value(serde_json::json!({"scheme":"https","hosts":["api.example.test","*.service.test"],"path_prefixes":["/v1/","/health"],"methods":["GET","POST"]})).unwrap();
for (scheme, host, path, method, allowed) in [
("https", "api.example.test", "/v1/a", "GET", true),
("HTTPS", "API.EXAMPLE.TEST", "/v1/a", "get", true),
("https", "a.service.test", "/health", "POST", true),
("https", "a.b.service.test", "/healthz", "POST", true),
("https", "service.test", "/health", "GET", false),
("https", "evilservice.test", "/health", "GET", false),
("https", "a.service.test.evil", "/health", "GET", false),
("https", "api.example.test.evil", "/v1/a", "GET", false),
("http", "api.example.test", "/v1/a", "GET", false),
("https", "api.example.test", "/V1/a", "GET", false),
("https", "api.example.test", "/v1", "GET", false),
("https", "api.example.test", "/v1/a", "DELETE", false),
] {
assert_eq!(rule_matches(&rule, scheme, host, path, method), allowed);
}
rule.methods = None;
assert!(rule_matches(
&rule,
"https",
"api.example.test",
"/v1/a",
"DELETE"
));
rule.methods = Some(vec![]);
assert!(!rule_matches(
&rule,
"https",
"api.example.test",
"/v1/a",
"GET"
));
rule.methods = None;
rule.path_prefixes.clear();
assert!(!rule_matches(
&rule,
"https",
"api.example.test",
"/v1/a",
"GET"
));
rule.path_prefixes.push("/".into());
rule.hosts.clear();
assert!(!rule_matches(
&rule,
"https",
"api.example.test",
"/v1/a",
"GET"
));
}
#[test]
fn revisions_needs_value_and_rename_are_metadata_only() {
let (_dir, c, caller, i) = fixture();
c.set_field(
&i.id,
&i.fields[0].field_id,
"opaque",
&caller,
c.unlocked().unwrap(),
)
.unwrap();
let old = c.item_get(&i.id).unwrap();
assert_eq!(old.version, 2);
assert!(!old.fields[0].needs_value);
assert!(old.fields[0].value.is_none());
let mut new = old.clone();
new.name = "renamed".into();
new.fields[0].name = "renamed_field".into();
let id = c
.file_edit_request(
&caller,
"rename",
Edit {
item: i.id.clone(),
base_version: 2,
config: new,
},
)
.unwrap();
assert!(!c.edit_diff(&id).unwrap().discards_values);
c.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
c.unlocked().unwrap(),
)
.unwrap();
let current = c.item_get(&i.id).unwrap();
assert_eq!(current.version, 3);
assert_eq!(current.fields[0].field_id, old.fields[0].field_id);
assert_eq!(current.name, "renamed");
assert_eq!(c.item_versions(&i.id).unwrap().len(), 2);
}
#[test]
fn stale_edits_leave_history_and_values_untouched() {
let (_dir, c, caller, i) = fixture();
let id = c
.file_edit_request(
&caller,
"edit",
Edit {
item: i.id.clone(),
base_version: 1,
config: i.clone(),
},
)
.unwrap();
c.set_field(
&i.id,
&i.fields[0].field_id,
"opaque",
&caller,
c.unlocked().unwrap(),
)
.unwrap();
assert_eq!(
c.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
c.unlocked().unwrap()
)
.unwrap_err()
.0,
"stale"
);
assert_eq!(c.request_list().unwrap()[0].state, "pending");
assert_eq!(c.item_versions(&i.id).unwrap().len(), 1);
}
#[test]
fn key_conflict_renewal_and_expiry() {
let (dir, c, caller, i) = fixture();
let p = GrantRequest {
items: vec![i.id.clone()],
duration_secs: None,
max_uses: None,
validator: None,
};
let id = c.file_request(&caller, "key", p.clone()).unwrap();
let mut other = p.clone();
other.max_uses = Some(1);
assert_eq!(
c.file_request(&caller, "key", other).unwrap_err().0,
"key_conflict"
);
c.deny(
&id,
sausage::cred::webauthn::Decision::Deny,
c.unlocked().unwrap(),
)
.unwrap();
assert_eq!(c.file_request(&caller, "key", p.clone()).unwrap(), id);
let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap();
db.execute("UPDATE requests SET expires=0 WHERE id=?", [&id])
.unwrap();
let next = c.file_request(&caller, "key", p).unwrap();
assert_ne!(id, next);
assert_eq!(c.request_decisions().unwrap().len(), 1);
db.execute("UPDATE requests SET expires=0 WHERE id=?", [&next])
.unwrap();
let mut notification = c.subscribe();
assert_eq!(c.expire_requests().unwrap(), vec![next.clone()]);
assert_eq!(notification.try_recv().unwrap(), next);
assert_eq!(
c.approve(
&next,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
c.unlocked().unwrap()
)
.unwrap_err()
.0,
"request_expired"
);
assert!(c.grant_list().unwrap().is_empty());
}
#[test]
fn typed_values_and_secret_configuration_fail_without_writes() {
let (_dir, c, caller, i) = fixture();
assert_eq!(
c.set_field(
&i.id,
&i.fields[0].field_id,
"line\nbreak",
&caller,
c.unlocked().unwrap()
)
.unwrap_err()
.0,
"invalid_value"
);
assert_eq!(c.item_get(&i.id).unwrap().version, 1);
let mut config = i.clone();
config.fields[0].value = Some("forbidden".into());
assert_eq!(
c.create_item(config, &caller, c.unlocked().unwrap())
.unwrap_err()
.0,
"secret_field"
);
assert_eq!(c.item_list().unwrap().len(), 1);
assert!(c.item_versions(&i.id).unwrap().is_empty());
}