Files
sausage/host/tests/cred_edits.rs
T

318 lines
9.1 KiB
Rust

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