Files
sausage/host/tests/cred_regressions.rs
T

390 lines
11 KiB
Rust

//! Retained v2 item, grant, and transaction regressions over native SQLite.
use sausage::cred::{
edits::{Delete, Edit},
grants::Grant,
items::*,
requests::{GrantRequest, Overrides},
script::{Limits, Script},
*,
};
struct Fake;
impl Script for Fake {
fn evaluate(
&self,
_: &str,
_: &str,
_: Option<&str>,
_: Limits,
) -> std::result::Result<String, script::Error> {
Ok("true".into())
}
}
fn fixture() -> (tempfile::TempDir, Cred, Caller, Item) {
let dir = tempfile::tempdir().unwrap();
let cred = Cred::open(dir.path()).unwrap();
let caller = Caller {
pid: 3,
program: "/bin/tool.wasm".into(),
conversations: vec!["root".into()],
};
let item = cred
.create_item(
Item {
id: String::new(),
name: "item".into(),
version: 0,
free_use: false,
rules: vec![Rule {
port: None,
scheme: "https".into(),
hosts: vec!["api.test".into()],
path_prefixes: vec!["/".into()],
methods: None,
}],
validate_js: None,
transform_js: String::new(),
fields: vec![
Field {
field_id: String::new(),
name: "first".into(),
public: false,
value: None,
needs_value: true,
field_type: types::FieldType::Text,
key_type: None,
},
Field {
field_id: String::new(),
name: "second".into(),
public: false,
value: None,
needs_value: true,
field_type: types::FieldType::Text,
key_type: None,
},
],
},
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
(dir, cred, caller, item)
}
fn parts() -> http::request::Parts {
http::Request::get("https://api.test/")
.body(())
.unwrap()
.into_parts()
.0
}
fn grant(item: &Item) -> Grant {
Grant {
id: String::new(),
items: vec![item.id.clone()],
subject: types::Subject::Conversation("root".into()),
exp: None,
max_uses: Some(1),
uses: 0,
revoked: false,
validator: None,
note: None,
}
}
#[test]
fn missing_values_and_wrong_master_spend_nothing() {
let (dir, cred, caller, item) = fixture();
let grant = cred
.grant_create(grant(&item), &caller, cred.unlocked().unwrap())
.unwrap();
assert_eq!(
cred.inject(parts(), &caller, &Fake).err().unwrap().0,
"needs_value"
);
assert_eq!(cred.grant_status(&grant.id).unwrap().uses, 0);
for field in &item.fields {
cred.set_field(
&item.id,
&field.field_id,
"opaque",
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
}
drop(cred);
std::fs::write(dir.path().join("cred.key"), [7u8; 32]).unwrap();
assert!(Cred::open(dir.path()).is_err());
let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap();
let uses: i64 = db
.query_row("SELECT uses FROM grants WHERE id=?", [&grant.id], |r| {
r.get(0)
})
.unwrap();
assert_eq!(uses, 0);
}
#[test]
fn field_name_swaps_preserve_ciphertexts_and_failed_edits_roll_back() {
let (dir, cred, caller, item) = fixture();
for field in &item.fields {
cred.set_field(
&item.id,
&field.field_id,
"opaque",
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
}
let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap();
let cipher = || {
let mut q = db
.prepare("SELECT ciphertext FROM fields ORDER BY id")
.unwrap();
q.query_map([], |r| r.get::<_, Vec<u8>>(0))
.unwrap()
.collect::<rusqlite::Result<Vec<_>>>()
.unwrap()
};
let before = cipher();
let old = cred.item_get(&item.id).unwrap();
let mut config = old.clone();
config.fields[0].name = "second".into();
config.fields[1].name = "first".into();
let id = cred
.file_edit_request(
&caller,
"swap",
Edit {
item: item.id.clone(),
base_version: old.version,
config,
},
)
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
assert!(cipher() == before);
let current = cred.item_get(&item.id).unwrap();
let mut config = current.clone();
config.fields.clear();
let id = cred
.file_edit_request(
&caller,
"remove",
Edit {
item: item.id.clone(),
base_version: current.version,
config,
},
)
.unwrap();
let history = cred.item_versions(&item.id).unwrap().len();
let audit = cred.audit_list().unwrap().len();
db.execute_batch("CREATE TRIGGER fail_decision BEFORE UPDATE ON requests BEGIN SELECT RAISE(ABORT,'forced'); END;").unwrap();
assert!(
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap()
)
.is_err()
);
assert!(cipher() == before);
assert_eq!(cred.item_get(&item.id).unwrap().version, current.version);
assert_eq!(cred.item_versions(&item.id).unwrap().len(), history);
assert_eq!(cred.audit_list().unwrap().len(), audit);
}
#[test]
fn deleted_item_revert_preserves_identity_but_never_restores_secrets() {
let (_dir, cred, caller, item) = fixture();
for field in &item.fields {
cred.set_field(
&item.id,
&field.field_id,
"opaque",
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
}
let current = cred.item_get(&item.id).unwrap();
let id = cred
.file_delete_request(
&caller,
"delete",
Delete {
item: item.id.clone(),
base_version: current.version,
},
)
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
let id = cred
.file_revert_request(
&caller,
"restore",
&item.id,
current.version + 1,
current.version,
)
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
let restored = cred.item_get(&item.id).unwrap();
assert_eq!(restored.id, item.id);
assert_eq!(restored.version, current.version + 2);
assert!(
restored
.fields
.iter()
.all(|f| f.needs_value && f.value.is_none())
);
assert_eq!(restored.fields[0].field_id, item.fields[0].field_id);
}
#[test]
fn secret_entry_keeps_oauth_cardinality_and_rolls_back_ciphertext() {
let (dir, cred, caller, item) = fixture();
let input = FieldInput {
value: sausage::cred::items::FieldValue::Plain("opaque"),
field_type: Some(types::FieldType::Oauth2RefreshToken),
key_type: None,
};
cred.set_field_input(
&item.id,
&item.fields[0].field_id,
&input,
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
cred.set_field(
&item.id,
&item.fields[1].field_id,
"opaque",
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
let current = cred.item_get(&item.id).unwrap();
let history = cred.item_versions(&item.id).unwrap().len();
let audit = cred.audit_list().unwrap().len();
let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap();
let cipher = || {
db.query_row(
"SELECT ciphertext FROM fields WHERE item=?1 AND id=?2",
rusqlite::params![item.id, item.fields[1].field_id],
|r| r.get::<_, Vec<u8>>(0),
)
.unwrap()
};
let before = cipher();
assert_eq!(
cred.set_field_input(
&item.id,
&item.fields[1].field_id,
&input,
&caller,
cred.unlocked().unwrap()
)
.err()
.unwrap()
.0,
"invalid_params"
);
assert!(cipher() == before);
assert_eq!(cred.item_get(&item.id).unwrap().version, current.version);
assert_eq!(cred.item_versions(&item.id).unwrap().len(), history);
assert_eq!(cred.audit_list().unwrap().len(), audit);
}
#[test]
fn typed_secret_entry_and_bundle_normalization() {
let (_dir, cred, caller, item) = fixture();
let mut metadata = item.clone();
metadata.fields[0].field_type = types::FieldType::SshPrivateKey;
metadata.fields[0].key_type = Some("es256".into());
let id = cred
.file_edit_request(
&caller,
"metadata",
Edit {
item: item.id.clone(),
base_version: item.version,
config: metadata,
},
)
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
let input = FieldInput {
value: sausage::cred::items::FieldValue::Plain("two\nlines"),
field_type: Some(types::FieldType::Note),
key_type: None,
};
cred.set_field_input(
&item.id,
&item.fields[0].field_id,
&input,
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
let current = cred.item_get(&item.id).unwrap();
assert_eq!(current.fields[0].field_type, types::FieldType::Note);
assert!(current.fields[0].key_type.is_none());
assert!(current.fields[0].value.is_none());
assert_eq!(
cred.item_versions(&item.id).unwrap()[0].config.fields[0].field_type,
types::FieldType::Text
);
let p = GrantRequest {
items: vec![item.name.clone(), item.id.clone()],
duration_secs: None,
max_uses: Some(1),
validator: None,
};
let id = cred.file_request(&caller, "bundle", p).unwrap();
let p = GrantRequest {
items: vec![item.id],
duration_secs: None,
max_uses: Some(1),
validator: None,
};
assert_eq!(cred.file_request(&caller, "bundle", p).unwrap(), id);
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides {
unlimited: true,
..Overrides::default()
},
cred.unlocked().unwrap(),
)
.unwrap();
let g = cred.grant_list().unwrap().remove(0);
assert_eq!(g.items.len(), 1);
assert!(g.max_uses.is_none());
assert!(g.exp.is_some());
}