Files
sausage/host/tests/cred_maintenance.rs
T

217 lines
6.7 KiB
Rust

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