//! 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()); }