//! Retained v2 item, grant, and transaction regressions over native SQLite. use sausage::cred::{ edits::{Delete, Edit}, grants::Grant, items::*, requests::{GrantRequest, Overrides}, script::{Limits, Script}, *, }; struct Fake; impl Script for Fake { fn evaluate( &self, _: &str, _: &str, _: Option<&str>, _: Limits, ) -> std::result::Result { Ok("true".into()) } } fn fixture() -> (tempfile::TempDir, Cred, Caller, Item) { let dir = tempfile::tempdir().unwrap(); let cred = Cred::open(dir.path()).unwrap(); let caller = Caller { pid: 3, program: "/bin/tool.wasm".into(), conversations: vec!["root".into()], }; let item = cred .create_item( Item { id: String::new(), name: "item".into(), version: 0, free_use: false, rules: vec![Rule { port: None, scheme: "https".into(), hosts: vec!["api.test".into()], path_prefixes: vec!["/".into()], methods: None, }], validate_js: None, transform_js: String::new(), fields: vec![ Field { field_id: String::new(), name: "first".into(), public: false, value: None, needs_value: true, field_type: types::FieldType::Text, key_type: None, }, Field { field_id: String::new(), name: "second".into(), public: false, value: None, needs_value: true, field_type: types::FieldType::Text, key_type: None, }, ], }, &caller, cred.unlocked().unwrap(), ) .unwrap(); (dir, cred, caller, item) } fn parts() -> http::request::Parts { http::Request::get("https://api.test/") .body(()) .unwrap() .into_parts() .0 } fn grant(item: &Item) -> Grant { 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, } } #[test] fn missing_values_and_wrong_master_spend_nothing() { let (dir, cred, caller, item) = fixture(); let grant = cred .grant_create(grant(&item), &caller, cred.unlocked().unwrap()) .unwrap(); assert_eq!( cred.inject(parts(), &caller, &Fake).err().unwrap().0, "needs_value" ); assert_eq!(cred.grant_status(&grant.id).unwrap().uses, 0); for field in &item.fields { cred.set_field( &item.id, &field.field_id, "opaque", &caller, cred.unlocked().unwrap(), ) .unwrap(); } drop(cred); std::fs::write(dir.path().join("cred.key"), [7u8; 32]).unwrap(); assert!(Cred::open(dir.path()).is_err()); let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); let uses: i64 = db .query_row("SELECT uses FROM grants WHERE id=?", [&grant.id], |r| { r.get(0) }) .unwrap(); assert_eq!(uses, 0); } #[test] fn field_name_swaps_preserve_ciphertexts_and_failed_edits_roll_back() { let (dir, cred, caller, item) = fixture(); for field in &item.fields { cred.set_field( &item.id, &field.field_id, "opaque", &caller, cred.unlocked().unwrap(), ) .unwrap(); } let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); let cipher = || { let mut q = db .prepare("SELECT ciphertext FROM fields ORDER BY id") .unwrap(); q.query_map([], |r| r.get::<_, Vec>(0)) .unwrap() .collect::>>() .unwrap() }; let before = cipher(); let old = cred.item_get(&item.id).unwrap(); let mut config = old.clone(); config.fields[0].name = "second".into(); config.fields[1].name = "first".into(); let id = cred .file_edit_request( &caller, "swap", Edit { item: item.id.clone(), base_version: old.version, config, }, ) .unwrap(); cred.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), cred.unlocked().unwrap(), ) .unwrap(); assert!(cipher() == before); let current = cred.item_get(&item.id).unwrap(); let mut config = current.clone(); config.fields.clear(); let id = cred .file_edit_request( &caller, "remove", Edit { item: item.id.clone(), base_version: current.version, config, }, ) .unwrap(); let history = cred.item_versions(&item.id).unwrap().len(); let audit = cred.audit_list().unwrap().len(); db.execute_batch("CREATE TRIGGER fail_decision BEFORE UPDATE ON requests BEGIN SELECT RAISE(ABORT,'forced'); END;").unwrap(); assert!( cred.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), cred.unlocked().unwrap() ) .is_err() ); assert!(cipher() == before); assert_eq!(cred.item_get(&item.id).unwrap().version, current.version); assert_eq!(cred.item_versions(&item.id).unwrap().len(), history); assert_eq!(cred.audit_list().unwrap().len(), audit); } #[test] fn deleted_item_revert_preserves_identity_but_never_restores_secrets() { let (_dir, cred, caller, item) = fixture(); for field in &item.fields { cred.set_field( &item.id, &field.field_id, "opaque", &caller, cred.unlocked().unwrap(), ) .unwrap(); } let current = cred.item_get(&item.id).unwrap(); let id = cred .file_delete_request( &caller, "delete", Delete { item: item.id.clone(), base_version: current.version, }, ) .unwrap(); cred.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), cred.unlocked().unwrap(), ) .unwrap(); let id = cred .file_revert_request( &caller, "restore", &item.id, current.version + 1, current.version, ) .unwrap(); cred.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), cred.unlocked().unwrap(), ) .unwrap(); let restored = cred.item_get(&item.id).unwrap(); assert_eq!(restored.id, item.id); assert_eq!(restored.version, current.version + 2); assert!( restored .fields .iter() .all(|f| f.needs_value && f.value.is_none()) ); assert_eq!(restored.fields[0].field_id, item.fields[0].field_id); } #[test] fn secret_entry_keeps_oauth_cardinality_and_rolls_back_ciphertext() { let (dir, cred, caller, item) = fixture(); let input = FieldInput { value: sausage::cred::items::FieldValue::Plain("opaque"), field_type: Some(types::FieldType::Oauth2RefreshToken), key_type: None, }; cred.set_field_input( &item.id, &item.fields[0].field_id, &input, &caller, cred.unlocked().unwrap(), ) .unwrap(); cred.set_field( &item.id, &item.fields[1].field_id, "opaque", &caller, cred.unlocked().unwrap(), ) .unwrap(); let current = cred.item_get(&item.id).unwrap(); let history = cred.item_versions(&item.id).unwrap().len(); let audit = cred.audit_list().unwrap().len(); let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); let cipher = || { db.query_row( "SELECT ciphertext FROM fields WHERE item=?1 AND id=?2", rusqlite::params![item.id, item.fields[1].field_id], |r| r.get::<_, Vec>(0), ) .unwrap() }; let before = cipher(); assert_eq!( cred.set_field_input( &item.id, &item.fields[1].field_id, &input, &caller, cred.unlocked().unwrap() ) .err() .unwrap() .0, "invalid_params" ); assert!(cipher() == before); assert_eq!(cred.item_get(&item.id).unwrap().version, current.version); assert_eq!(cred.item_versions(&item.id).unwrap().len(), history); assert_eq!(cred.audit_list().unwrap().len(), audit); } #[test] fn typed_secret_entry_and_bundle_normalization() { let (_dir, cred, caller, item) = fixture(); let mut metadata = item.clone(); metadata.fields[0].field_type = types::FieldType::SshPrivateKey; metadata.fields[0].key_type = Some("es256".into()); let id = cred .file_edit_request( &caller, "metadata", Edit { item: item.id.clone(), base_version: item.version, config: metadata, }, ) .unwrap(); cred.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), cred.unlocked().unwrap(), ) .unwrap(); let input = FieldInput { value: sausage::cred::items::FieldValue::Plain("two\nlines"), field_type: Some(types::FieldType::Note), key_type: None, }; cred.set_field_input( &item.id, &item.fields[0].field_id, &input, &caller, cred.unlocked().unwrap(), ) .unwrap(); let current = cred.item_get(&item.id).unwrap(); assert_eq!(current.fields[0].field_type, types::FieldType::Note); assert!(current.fields[0].key_type.is_none()); assert!(current.fields[0].value.is_none()); assert_eq!( cred.item_versions(&item.id).unwrap()[0].config.fields[0].field_type, types::FieldType::Text ); let p = GrantRequest { items: vec![item.name.clone(), item.id.clone()], duration_secs: None, max_uses: Some(1), validator: None, }; let id = cred.file_request(&caller, "bundle", p).unwrap(); let p = GrantRequest { items: vec![item.id], duration_secs: None, max_uses: Some(1), validator: None, }; assert_eq!(cred.file_request(&caller, "bundle", p).unwrap(), id); cred.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides { unlimited: true, ..Overrides::default() }, cred.unlocked().unwrap(), ) .unwrap(); let g = cred.grant_list().unwrap().remove(0); assert_eq!(g.items.len(), 1); assert!(g.max_uses.is_none()); assert!(g.exp.is_some()); }