416 lines
11 KiB
Rust
416 lines
11 KiB
Rust
//! v2 keyed replay compares intent before current item validation or edit planning.
|
|
use sausage::cred::{
|
|
edits::*,
|
|
items::*,
|
|
requests::{GrantRequest, Overrides},
|
|
types::FieldType,
|
|
*,
|
|
};
|
|
fn fixture() -> (tempfile::TempDir, Cred, Caller, Item) {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let cred = Cred::open(dir.path()).unwrap();
|
|
let caller = Caller {
|
|
pid: 1,
|
|
program: "/bin/tool".into(),
|
|
conversations: vec!["root".into()],
|
|
};
|
|
let item = cred
|
|
.create_item(
|
|
Item {
|
|
id: String::new(),
|
|
name: "item".into(),
|
|
version: 0,
|
|
free_use: false,
|
|
rules: vec![],
|
|
validate_js: None,
|
|
transform_js: String::new(),
|
|
fields: vec![Field {
|
|
field_id: String::new(),
|
|
name: "token".into(),
|
|
public: false,
|
|
value: None,
|
|
needs_value: true,
|
|
field_type: FieldType::Text,
|
|
key_type: None,
|
|
}],
|
|
},
|
|
&caller,
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
(dir, cred, caller, item)
|
|
}
|
|
#[test]
|
|
fn approved_operations_and_deletes_replay() {
|
|
let (_dir, cred, caller, item) = fixture();
|
|
let request = OperationRequest {
|
|
item: Some(item.id.clone()),
|
|
base_version: Some(item.version),
|
|
change: vec![Operation::SetName {
|
|
name: "renamed".into(),
|
|
}],
|
|
};
|
|
let id = cred
|
|
.file_operation_request(&caller, "edit", request.clone())
|
|
.unwrap();
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
cred.file_operation_request(&caller, "edit", request)
|
|
.unwrap(),
|
|
id
|
|
);
|
|
let current = cred.item_get(&item.id).unwrap();
|
|
let delete = Delete {
|
|
item: item.id,
|
|
base_version: current.version,
|
|
};
|
|
let id = cred
|
|
.file_delete_request(&caller, "delete", delete.clone())
|
|
.unwrap();
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
cred.file_delete_request(&caller, "delete", delete).unwrap(),
|
|
id
|
|
);
|
|
}
|
|
#[test]
|
|
fn approved_replacements_and_creations_replay() {
|
|
let (_dir, cred, caller, item) = fixture();
|
|
let mut config = item.clone();
|
|
config.name = "renamed".into();
|
|
let edit = Edit {
|
|
item: item.id,
|
|
base_version: item.version,
|
|
config: config.clone(),
|
|
};
|
|
let id = cred
|
|
.file_edit_request(&caller, "replace", edit.clone())
|
|
.unwrap();
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
cred.file_edit_request(&caller, "replace", edit).unwrap(),
|
|
id
|
|
);
|
|
config.name = "new".into();
|
|
let request = OperationRequest {
|
|
item: None,
|
|
base_version: None,
|
|
change: vec![Operation::CreateItem { config }],
|
|
};
|
|
let id = cred
|
|
.file_operation_request(&caller, "create", request.clone())
|
|
.unwrap();
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
cred.file_operation_request(&caller, "create", request)
|
|
.unwrap(),
|
|
id
|
|
);
|
|
assert_eq!(cred.item_list().unwrap().len(), 2);
|
|
}
|
|
#[test]
|
|
fn grant_defaults_normalize_and_pending_requests_replay_after_deletion() {
|
|
let (_dir, cred, caller, item) = fixture();
|
|
let payload = GrantRequest {
|
|
items: vec![item.id.clone()],
|
|
duration_secs: None,
|
|
max_uses: None,
|
|
validator: Some(String::new()),
|
|
};
|
|
let id = cred.file_request(&caller, "grant", payload).unwrap();
|
|
let payload = GrantRequest {
|
|
items: vec![item.id.clone()],
|
|
duration_secs: Some(3600),
|
|
max_uses: None,
|
|
validator: None,
|
|
};
|
|
assert_eq!(
|
|
cred.file_request(&caller, "grant", payload.clone())
|
|
.unwrap(),
|
|
id
|
|
);
|
|
let delete = cred
|
|
.file_delete_request(
|
|
&caller,
|
|
"delete",
|
|
Delete {
|
|
item: item.id,
|
|
base_version: item.version,
|
|
},
|
|
)
|
|
.unwrap();
|
|
cred.approve(
|
|
&delete,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(cred.file_request(&caller, "grant", payload).unwrap(), id);
|
|
assert!(
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap()
|
|
)
|
|
.is_err()
|
|
);
|
|
assert_eq!(
|
|
cred.request_list()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|r| r.id == id)
|
|
.unwrap()
|
|
.state,
|
|
"pending"
|
|
);
|
|
}
|
|
#[test]
|
|
fn values_are_checked_at_each_public_operation() {
|
|
let (_dir, cred, caller, item) = fixture();
|
|
let mut config = item.clone();
|
|
config.fields.push(Field {
|
|
field_id: String::new(),
|
|
name: "public".into(),
|
|
public: true,
|
|
value: Some("old".into()),
|
|
needs_value: false,
|
|
field_type: FieldType::Text,
|
|
key_type: None,
|
|
});
|
|
let id = cred
|
|
.file_edit_request(
|
|
&caller,
|
|
"setup",
|
|
Edit {
|
|
item: item.id.clone(),
|
|
base_version: item.version,
|
|
config,
|
|
},
|
|
)
|
|
.unwrap();
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
let current = cred.item_get(&item.id).unwrap();
|
|
let field = current
|
|
.fields
|
|
.iter()
|
|
.find(|f| f.name == "public")
|
|
.unwrap()
|
|
.field_id
|
|
.clone();
|
|
let mut request = OperationRequest {
|
|
item: Some(item.id.clone()),
|
|
base_version: Some(current.version),
|
|
change: vec![
|
|
Operation::SetFieldValue {
|
|
field: field.clone(),
|
|
value: "bad\nvalue".into(),
|
|
},
|
|
Operation::SetFieldType {
|
|
field,
|
|
field_type: FieldType::Note,
|
|
key_type: None,
|
|
},
|
|
],
|
|
};
|
|
assert_eq!(
|
|
cred.file_operation_request(&caller, "invalid", request.clone())
|
|
.err()
|
|
.unwrap()
|
|
.0,
|
|
"invalid_value"
|
|
);
|
|
assert_eq!(cred.item_get(&item.id).unwrap().version, current.version);
|
|
request.change.reverse();
|
|
let id = cred
|
|
.file_operation_request(&caller, "ordered", request)
|
|
.unwrap();
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
cred.item_get(&item.id).unwrap().version,
|
|
current.version + 1
|
|
);
|
|
}
|
|
#[test]
|
|
fn secret_retyping_checks_each_operation_only_on_approval() {
|
|
let (_dir, cred, caller, item) = fixture();
|
|
let field = &item.fields[0].field_id;
|
|
cred.set_field_input(
|
|
&item.id,
|
|
field,
|
|
&FieldInput {
|
|
value: sausage::cred::items::FieldValue::Plain("VALUE_SENTINEL\nTAIL_SENTINEL"),
|
|
field_type: Some(FieldType::Note),
|
|
key_type: None,
|
|
},
|
|
&caller,
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
let current = cred.item_get(&item.id).unwrap();
|
|
let request = OperationRequest {
|
|
item: Some(item.id.clone()),
|
|
base_version: Some(current.version),
|
|
change: vec![
|
|
Operation::SetFieldType {
|
|
field: field.clone(),
|
|
field_type: FieldType::Text,
|
|
key_type: None,
|
|
},
|
|
Operation::SetFieldType {
|
|
field: field.clone(),
|
|
field_type: FieldType::Note,
|
|
key_type: None,
|
|
},
|
|
],
|
|
};
|
|
let id = cred
|
|
.file_operation_request(&caller, "types", request)
|
|
.unwrap();
|
|
assert!(
|
|
!cred.request_list().unwrap()[0]
|
|
.payload
|
|
.to_string()
|
|
.contains("VALUE_SENTINEL")
|
|
);
|
|
assert_eq!(
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap()
|
|
)
|
|
.err()
|
|
.unwrap()
|
|
.0,
|
|
"invalid_value"
|
|
);
|
|
assert_eq!(cred.item_get(&item.id).unwrap().version, current.version);
|
|
assert_eq!(cred.request_list().unwrap()[0].state, "pending");
|
|
let history = cred.item_versions(&item.id).unwrap().len();
|
|
cred.set_field(
|
|
&item.id,
|
|
field,
|
|
"single-line",
|
|
&caller,
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
let next = cred.item_get(&item.id).unwrap();
|
|
assert_eq!(next.version, current.version + 1);
|
|
assert_eq!(cred.item_versions(&item.id).unwrap().len(), history + 1);
|
|
cred.resolve_stale(&id, next.version, &caller, cred.unlocked().unwrap())
|
|
.unwrap();
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
#[test]
|
|
fn stale_rebases_keep_original_keyed_intent() {
|
|
let (_dir, cred, caller, item) = fixture();
|
|
let request = OperationRequest {
|
|
item: Some(item.id.clone()),
|
|
base_version: Some(item.version),
|
|
change: vec![Operation::SetName {
|
|
name: "renamed".into(),
|
|
}],
|
|
};
|
|
let id = cred
|
|
.file_operation_request(&caller, "rebase", request.clone())
|
|
.unwrap();
|
|
cred.set_field(
|
|
&item.id,
|
|
&item.fields[0].field_id,
|
|
"opaque",
|
|
&caller,
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
let current = cred.item_get(&item.id).unwrap();
|
|
cred.resolve_stale(&id, current.version, &caller, cred.unlocked().unwrap())
|
|
.unwrap();
|
|
assert_eq!(
|
|
cred.file_operation_request(&caller, "rebase", request.clone())
|
|
.unwrap(),
|
|
id
|
|
);
|
|
cred.approve(
|
|
&id,
|
|
sausage::cred::webauthn::Decision::Approve,
|
|
Overrides::default(),
|
|
cred.unlocked().unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
cred.file_operation_request(&caller, "rebase", request)
|
|
.unwrap(),
|
|
id
|
|
);
|
|
assert!(!cred.item_get(&item.id).unwrap().fields[0].needs_value);
|
|
}
|
|
#[test]
|
|
fn operation_order_is_part_of_key_identity() {
|
|
let (_dir, cred, caller, item) = fixture();
|
|
let mut request = OperationRequest {
|
|
item: Some(item.id),
|
|
base_version: Some(item.version),
|
|
change: vec![
|
|
Operation::SetName {
|
|
name: "renamed".into(),
|
|
},
|
|
Operation::SetFreeUse { enabled: true },
|
|
],
|
|
};
|
|
cred.file_operation_request(&caller, "ordered", request.clone())
|
|
.unwrap();
|
|
request.change.reverse();
|
|
assert_eq!(
|
|
cred.file_operation_request(&caller, "ordered", request)
|
|
.err()
|
|
.unwrap()
|
|
.0,
|
|
"key_conflict"
|
|
);
|
|
}
|