249 lines
7.3 KiB
Rust
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());
|
|
}
|