318 lines
9.1 KiB
Rust
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);
|
|
}
|