//! D57 retention preserves pending work and all versions. Revocation only removes access. use sausage::cred::{ edits::{Edit, Operation, OperationRequest}, grants::Grant, items::Item, requests::{GrantRequest, Overrides}, webauthn::Decision, *, }; fn fixture() -> (tempfile::TempDir, Cred, Caller, Item) { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller { program: "/bin/test.wasm".into(), pid: 1, conversations: vec!["root".into()], }; let item=c.create_item(serde_json::from_value(serde_json::json!({"name":"test","rules":[],"validate_js":null,"transform_js":"","fields":[]})).unwrap(),&caller,c.unlocked().unwrap()).unwrap(); (dir, c, caller, item) } fn grant(item: &Item, subject: types::Subject) -> Grant { Grant { id: String::new(), items: vec![item.id.clone()], subject, exp: None, max_uses: None, uses: 0, revoked: false, validator: None, note: None, } } fn request(item: &Item) -> GrantRequest { GrantRequest { items: vec![item.id.clone()], duration_secs: None, max_uses: None, validator: None, } } #[test] fn prune_obeys_cutoffs_and_preserves_pending_live_and_versions() { let (dir, c, caller, item) = fixture(); let now = 200 * 86400; let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); let old = c.file_request(&caller, "old", request(&item)).unwrap(); c.approve( &old, Decision::Approve, Overrides::default(), c.unlocked().unwrap(), ) .unwrap(); let expired = c.grant_list().unwrap()[0].id.clone(); db.execute( "UPDATE requests SET decided_at=? WHERE id=?", rusqlite::params![now - 31 * 86400, old], ) .unwrap(); db.execute( "UPDATE grants SET exp=? WHERE id=?", rusqlite::params![now - 31 * 86400, expired], ) .unwrap(); let pending = c.file_request(&caller, "pending", request(&item)).unwrap(); db.execute("UPDATE requests SET expires=0 WHERE id=?", [&pending]) .unwrap(); let edge = c.file_request(&caller, "edge", request(&item)).unwrap(); c.deny(&edge, Decision::Deny, c.unlocked().unwrap()) .unwrap(); db.execute( "UPDATE requests SET decided_at=? WHERE id=?", rusqlite::params![now - 30 * 86400, edge], ) .unwrap(); let live = c .grant_create( grant(&item, types::Subject::Conversation("root".into())), &caller, c.unlocked().unwrap(), ) .unwrap(); let revoked = c .grant_create( grant(&item, types::Subject::Conversation("root".into())), &caller, c.unlocked().unwrap(), ) .unwrap(); c.revoke_grant(&revoked.id, &caller).unwrap(); db.execute( "UPDATE grants SET revoked_at=? WHERE id=?", rusqlite::params![now - 31 * 86400, revoked.id], ) .unwrap(); let protected = c .grant_create( grant(&item, types::Subject::Conversation("root".into())), &caller, c.unlocked().unwrap(), ) .unwrap(); db.execute( "UPDATE grants SET exp=? WHERE id=?", rusqlite::params![now - 31 * 86400, protected.id], ) .unwrap(); c.file_operation_request( &caller, "protected", OperationRequest { item: None, base_version: None, change: vec![Operation::GrantRevoke { target: protected.id.clone(), }], }, ) .unwrap(); let mut config = item.clone(); config.name = "renamed".into(); let edit = c .file_edit_request( &caller, "edit", Edit { item: item.id.clone(), base_version: item.version, config, }, ) .unwrap(); c.approve( &edit, Decision::Approve, Overrides::default(), c.unlocked().unwrap(), ) .unwrap(); db.execute("UPDATE versions SET created=0", []).unwrap(); db.execute( "INSERT INTO audit(time,event,pid,program) VALUES(?,'old-marker',1,'test')", [now - 91 * 86400], ) .unwrap(); db.execute( "INSERT INTO audit(time,event,pid,program) VALUES(?,'edge-marker',1,'test')", [now - 90 * 86400], ) .unwrap(); c.prune(now).unwrap(); let requests = c.request_list().unwrap(); assert!(!requests.iter().any(|r| r.id == old)); assert!(requests.iter().any(|r| r.id == pending)); assert!(requests.iter().any(|r| r.id == edge)); let grants = c.grant_list().unwrap(); assert!(!grants.iter().any(|g| g.id == expired || g.id == revoked.id)); assert!(grants.iter().any(|g| g.id == live.id)); assert!(grants.iter().any(|g| g.id == protected.id)); let audit = c.audit_list().unwrap(); assert!(!audit.iter().any(|r| r.event == "old-marker")); assert!(audit.iter().any(|r| r.event == "edge-marker")); assert_eq!(c.item_versions(&item.id).unwrap().len(), 1); } #[test] fn revoke_ids_programs_and_full_subtrees_without_widening_access() { let (dir, c, caller, item) = fixture(); let root = c .grant_create( grant(&item, types::Subject::Conversation("root".into())), &caller, c.unlocked().unwrap(), ) .unwrap(); let child = c .grant_create( grant(&item, types::Subject::Conversation("child".into())), &caller, c.unlocked().unwrap(), ) .unwrap(); c.revoke_subjects( &["root".into(), "child".into()], &caller, c.unlocked().unwrap(), ) .unwrap(); assert!(c.grant_status(&root.id).unwrap().revoked); assert!(c.grant_status(&child.id).unwrap().revoked); let program = c .grant_create( grant(&item, types::Subject::Program("/bin/service.wasm".into())), &caller, c.unlocked().unwrap(), ) .unwrap(); let other = c .grant_create( grant(&item, types::Subject::Program("/bin/other.wasm".into())), &caller, c.unlocked().unwrap(), ) .unwrap(); let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); db.execute( "INSERT INTO passkey_credentials VALUES('synthetic','test','{}')", [], ) .unwrap(); c.revoke_program("/bin/service.wasm", &caller).unwrap(); assert!(c.grant_status(&program.id).unwrap().revoked); assert!(!c.grant_status(&other.id).unwrap().revoked); c.revoke_grant(&other.id, &caller).unwrap(); assert!(c.grant_status(&other.id).unwrap().revoked); }