//! Explicit edit operations and atomic approval regression tests. use sausage::cred::{ edits::{Operation, OperationRequest}, items::*, requests::Overrides, *, }; fn config(name: &str) -> Item { Item { id: String::new(), name: name.into(), version: 0, free_use: false, rules: vec![], validate_js: None, transform_js: String::new(), fields: vec![Field { field_id: String::new(), name: "private".into(), public: false, value: None, needs_value: true, field_type: types::FieldType::SshPrivateKey, key_type: Some("es256".into()), }], } } #[test] fn creation_key_generation_and_deletion_keep_secrets_out_of_history() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller { program: "/bin/tool.wasm".into(), pid: 3, conversations: vec!["root".into()], }; let id = c .file_operation_request( &caller, "generate", OperationRequest { item: None, base_version: None, change: vec![Operation::GenerateEs256Item { config: config("generated"), }], }, ) .unwrap(); assert!(c.item_list().unwrap().is_empty()); c.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), c.unlocked().unwrap(), ) .unwrap(); let item = c.item_list().unwrap().remove(0); assert!(!item.fields[0].needs_value); assert!(item.fields[0].value.is_none()); assert!( !serde_json::to_string(&c.request_diff(&id).unwrap()) .unwrap() .contains("PRIVATE KEY") ); let id = c .file_operation_request( &caller, "delete", OperationRequest { item: Some(item.id.clone()), base_version: Some(item.version), change: vec![Operation::DeleteItem], }, ) .unwrap(); c.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), c.unlocked().unwrap(), ) .unwrap(); assert!(c.item_list().unwrap().is_empty()); assert_eq!(c.item_versions(&item.id).unwrap().len(), 1); let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); assert_eq!( db.query_row("SELECT COUNT(*) FROM fields", [], |r| r.get::<_, i64>(0)) .unwrap(), 0 ); } #[test] fn operation_order_name_conflicts_and_stale_resolution() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller { program: "/bin/tool.wasm".into(), pid: 3, conversations: vec!["root".into()], }; let item = c .create_item(config("first"), &caller, c.unlocked().unwrap()) .unwrap(); c.create_item(config("taken"), &caller, c.unlocked().unwrap()) .unwrap(); let request = |change| OperationRequest { item: Some(item.id.clone()), base_version: Some(item.version), change, }; assert_eq!( c.file_operation_request( &caller, "conflict", request(vec![Operation::SetName { name: "taken".into() }]) ) .unwrap_err() .0, "name_conflict" ); let id = c .file_operation_request( &caller, "rename", request(vec![ Operation::SetName { name: "intermediate".into(), }, Operation::SetName { name: "last".into(), }, ]), ) .unwrap(); let other = c .file_operation_request( &caller, "other", request(vec![Operation::SetFreeUse { enabled: true }]), ) .unwrap(); c.approve( &other, sausage::cred::webauthn::Decision::Approve, Overrides::default(), c.unlocked().unwrap(), ) .unwrap(); assert!(c.edit_diff(&id).unwrap().stale); assert_eq!( c.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), c.unlocked().unwrap() ) .unwrap_err() .0, "stale" ); c.resolve_stale(&id, 2, &caller, c.unlocked().unwrap()) .unwrap(); assert!(!c.edit_diff(&id).unwrap().stale); c.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), c.unlocked().unwrap(), ) .unwrap(); let current = c.item_get(&item.id).unwrap(); assert_eq!(current.name, "last"); assert!(current.free_use); } #[test] fn field_removal_revert_and_public_visibility() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller { program: "/bin/tool.wasm".into(), pid: 3, conversations: vec!["root".into()], }; let mut initial = config("item"); initial.fields[0].field_type = types::FieldType::Text; initial.fields[0].key_type = None; initial.fields.push(Field { field_id: String::new(), name: "public".into(), public: true, value: Some("first".into()), needs_value: false, field_type: types::FieldType::Text, key_type: None, }); let item = c .create_item(initial, &caller, c.unlocked().unwrap()) .unwrap(); c.set_field( &item.id, &item.fields[0].field_id, "SECRET_REMOVED", &caller, c.unlocked().unwrap(), ) .unwrap(); let current = c.item_get(&item.id).unwrap(); let request = |change| OperationRequest { item: Some(item.id.clone()), base_version: Some(current.version), change, }; let id = c .file_operation_request( &caller, "remove", request(vec![ Operation::RemoveField { field: item.fields[0].field_id.clone(), }, Operation::SetFieldValue { field: item.fields[1].field_id.clone(), value: "second".into(), }, ]), ) .unwrap(); assert!(c.edit_diff(&id).unwrap().discards_values); c.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), c.unlocked().unwrap(), ) .unwrap(); let id = c .file_revert_request(&caller, "revert", &item.id, 3, 2) .unwrap(); c.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), c.unlocked().unwrap(), ) .unwrap(); let restored = c.item_get(&item.id).unwrap(); assert!(restored.fields[0].needs_value); assert_eq!(restored.fields[0].field_id, item.fields[0].field_id); assert_eq!(restored.fields[1].value.as_deref(), Some("second")); let mut changed = restored.clone(); changed.fields[1].public = false; changed.fields[1].value = None; assert_eq!( c.file_edit_request( &caller, "visibility", sausage::cred::edits::Edit { item: item.id, base_version: restored.version, config: changed } ) .unwrap_err() .0, "immutable_visibility" ); assert!(!format!("{:?}", c.item_list().unwrap()).contains("SECRET_REMOVED")); } #[test] fn decision_failure_rolls_back_every_write() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller { program: "/bin/tool.wasm".into(), pid: 3, conversations: vec!["root".into()], }; let id = c .file_operation_request( &caller, "create", OperationRequest { item: None, base_version: None, change: vec![Operation::CreateItem { config: config("first"), }], }, ) .unwrap(); let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); db.execute_batch("CREATE TRIGGER fail_decision BEFORE UPDATE ON requests BEGIN SELECT RAISE(ABORT, 'forced'); END;").unwrap(); assert!( c.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), c.unlocked().unwrap() ) .is_err() ); assert!(c.item_list().unwrap().is_empty()); assert!(c.audit_list().unwrap().is_empty()); assert_eq!(c.request_list().unwrap()[0].state, "pending"); db.execute_batch("DROP TRIGGER fail_decision;").unwrap(); c.approve( &id, sausage::cred::webauthn::Decision::Approve, Overrides::default(), c.unlocked().unwrap(), ) .unwrap(); assert_eq!(c.item_list().unwrap().len(), 1); }