Implement read-only vault domains (#6)
This commit is contained in:
298
crates/storage/tests/read_domains.rs
Normal file
298
crates/storage/tests/read_domains.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod support;
|
||||
|
||||
use std::{collections::BTreeMap, fs, num::NonZeroUsize, path::Path};
|
||||
|
||||
use ironstorage::{
|
||||
command::{EXIT_FAILURE, GrepRequest, Presentation, ShowRequest},
|
||||
crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
||||
read::{PresentationChannel, ReadError, ShowOutput, ShowResult, TreeNodeKind, VaultReader},
|
||||
repository::{DirectoryPath, EntryPath, Repository, RepositoryError, SecretBytes},
|
||||
};
|
||||
use support::compatibility::{FixtureSet, TestResult};
|
||||
|
||||
struct FixtureSecrets {
|
||||
values: BTreeMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl FixtureSecrets {
|
||||
fn all(fixture: &FixtureSet) -> Self {
|
||||
Self {
|
||||
values: fixture
|
||||
.generated
|
||||
.keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
(
|
||||
key.primary_fingerprint.clone(),
|
||||
key.passphrase.as_bytes().to_vec(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretProvider for FixtureSecrets {
|
||||
fn secret_for(&mut self, key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
|
||||
self.values
|
||||
.get(key.fingerprint().as_str())
|
||||
.cloned()
|
||||
.map(SecretBytes::new)
|
||||
.ok_or(SecretProviderError::Unavailable)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_root_and_subtree_models_render_without_ciphertext_suffixes() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let reader = VaultReader::new(&repository, &keys);
|
||||
|
||||
let root = reader.list(&DirectoryPath::root())?;
|
||||
assert_eq!(root.title(), "Password Store");
|
||||
assert_eq!(
|
||||
root.render_plain(),
|
||||
concat!(
|
||||
"Password Store\n",
|
||||
"├── email\n",
|
||||
"│ └── personal\n",
|
||||
"├── otp\n",
|
||||
"│ ├── hotp\n",
|
||||
"│ └── totp\n",
|
||||
"├── shared\n",
|
||||
"│ └── multiple\n",
|
||||
"├── team\n",
|
||||
"│ └── service\n",
|
||||
"└── unicode\n",
|
||||
" └── 咖啡\n",
|
||||
)
|
||||
);
|
||||
assert!(!root.render_plain().contains(".gpg"));
|
||||
assert!(!root.render_plain().contains(".gpg-id"));
|
||||
|
||||
let team = reader.list(&DirectoryPath::parse("team")?)?;
|
||||
assert_eq!(team.title(), "team");
|
||||
assert_eq!(team.render_plain(), "team\n└── service\n");
|
||||
assert!(matches!(
|
||||
reader.list(&DirectoryPath::parse("missing")?),
|
||||
Err(ReadError::Repository(RepositoryError::NotFound { .. }))
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_and_implicit_show_dispatch_to_entry_or_directory() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let reader = VaultReader::new(&repository, &keys);
|
||||
let mut provider = FixtureSecrets::all(&fixture);
|
||||
|
||||
let expected = fixture.read("expected/basic/email/personal.txt")?;
|
||||
match reader.show(Some("email/personal"), &mut provider)? {
|
||||
ShowResult::Entry(secret) => assert_eq!(secret.expose(), expected),
|
||||
ShowResult::Directory(_) => panic!("entry dispatched as directory"),
|
||||
}
|
||||
match reader.show(Some("team"), &mut provider)? {
|
||||
ShowResult::Directory(tree) => assert_eq!(tree.render_plain(), "team\n└── service\n"),
|
||||
ShowResult::Entry(_) => panic!("directory dispatched as entry"),
|
||||
}
|
||||
match reader.show(None, &mut provider)? {
|
||||
ShowResult::Directory(tree) => assert_eq!(tree.title(), "Password Store"),
|
||||
ShowResult::Entry(_) => panic!("default show did not list root"),
|
||||
}
|
||||
assert!(matches!(
|
||||
reader.show(Some("does-not-exist"), &mut provider),
|
||||
Err(ReadError::Repository(RepositoryError::NotFound { .. }))
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_clipboard_and_qr_requests_select_lines_without_rendered_state() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let reader = VaultReader::new(&repository, &keys);
|
||||
let mut provider = FixtureSecrets::all(&fixture);
|
||||
|
||||
let clipboard = ShowRequest {
|
||||
entry: Some("email/personal".to_owned()),
|
||||
presentation: Presentation::Clipboard {
|
||||
line: NonZeroUsize::new(1).expect("nonzero"),
|
||||
},
|
||||
};
|
||||
let ShowOutput::Present(clipboard) = reader.execute_show(&clipboard, &mut provider)? else {
|
||||
panic!("clipboard request was not selected")
|
||||
};
|
||||
assert_eq!(clipboard.channel(), PresentationChannel::Clipboard);
|
||||
assert_eq!(clipboard.line(), 1);
|
||||
assert_eq!(clipboard.contents().expose(), b"correct horse fixture");
|
||||
assert!(!format!("{clipboard:?}").contains("correct horse"));
|
||||
|
||||
let qr = ShowRequest {
|
||||
entry: Some("email/personal".to_owned()),
|
||||
presentation: Presentation::QrCode {
|
||||
line: NonZeroUsize::new(2).expect("nonzero"),
|
||||
},
|
||||
};
|
||||
let ShowOutput::Present(qr) = reader.execute_show(&qr, &mut provider)? else {
|
||||
panic!("QR request was not selected")
|
||||
};
|
||||
assert_eq!(qr.channel(), PresentationChannel::QrCode);
|
||||
assert_eq!(qr.contents().expose(), b"login: alice@example.test");
|
||||
|
||||
let missing = reader.select_line(&EntryPath::parse("email/personal")?, 99, &mut provider);
|
||||
assert!(matches!(
|
||||
&missing,
|
||||
Err(ReadError::LineNotFound { line: 99, .. })
|
||||
));
|
||||
assert_eq!(missing.unwrap_err().exit_code(), EXIT_FAILURE);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_matches_entry_and_directory_names_case_insensitively() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let reader = VaultReader::new(&repository, &keys);
|
||||
|
||||
let results = reader.find(&["service".to_owned(), "咖啡".to_owned()])?;
|
||||
assert_eq!(
|
||||
results
|
||||
.matches()
|
||||
.iter()
|
||||
.map(|matched| (matched.path(), matched.kind()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("team/service", TreeNodeKind::Entry),
|
||||
("unicode/咖啡", TreeNodeKind::Entry),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
results.tree().render_plain(),
|
||||
concat!(
|
||||
"Password Store\n",
|
||||
"├── team\n",
|
||||
"│ └── service\n",
|
||||
"└── unicode\n",
|
||||
" └── 咖啡\n",
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
results
|
||||
.render_plain()
|
||||
.starts_with("Search Terms: service,咖啡\nPassword Store\n")
|
||||
);
|
||||
let personal = reader.find(&["PERSONAL".to_owned()])?;
|
||||
assert_eq!(personal.matches()[0].path(), "email/personal");
|
||||
assert!(matches!(
|
||||
reader.find(&[]),
|
||||
Err(ReadError::MissingSearchTerms)
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypted_grep_supports_regex_case_inversion_fixed_strings_and_lines() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let reader = VaultReader::new(&repository, &keys);
|
||||
let mut provider = FixtureSecrets::all(&fixture);
|
||||
|
||||
let mut numbered = grep("alice@example\\.test");
|
||||
numbered.line_number = true;
|
||||
let basic = reader.grep(&numbered, &mut provider)?;
|
||||
assert_eq!(basic.entries().len(), 1);
|
||||
assert_eq!(basic.entries()[0].path().to_string(), "email/personal");
|
||||
assert_eq!(basic.entries()[0].lines()[0].number(), 2);
|
||||
assert_eq!(
|
||||
basic.entries()[0].lines()[0].contents().expose(),
|
||||
b"login: alice@example.test"
|
||||
);
|
||||
assert!(!format!("{basic:?}").contains("alice@example"));
|
||||
assert_eq!(
|
||||
basic.render_plain().expose(),
|
||||
b"email/personal:\n2:login: alice@example.test\n"
|
||||
);
|
||||
|
||||
let mut insensitive = grep("FIXTURE");
|
||||
insensitive.ignore_case = true;
|
||||
assert!(reader.grep(&insensitive, &mut provider)?.entries().len() >= 4);
|
||||
|
||||
let mut fixed = grep("alice@example.test");
|
||||
fixed.fixed_strings = true;
|
||||
assert_eq!(reader.grep(&fixed, &mut provider)?.entries().len(), 1);
|
||||
|
||||
let mut inverted = grep("fixture");
|
||||
inverted.invert_match = true;
|
||||
let inverted = reader.grep(&inverted, &mut provider)?;
|
||||
assert!(inverted.entries().iter().any(|entry| {
|
||||
entry.path().to_string() == "email/personal"
|
||||
&& entry
|
||||
.lines()
|
||||
.iter()
|
||||
.any(|line| line.contents().expose() == b"login: alice@example.test")
|
||||
}));
|
||||
|
||||
let invalid = grep("[");
|
||||
assert!(matches!(
|
||||
reader.grep(&invalid, &mut provider),
|
||||
Err(ReadError::InvalidRegex)
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_excludes_extensions_git_metadata_and_ciphertext_names() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
fs::create_dir_all(store.path().join(".extensions/nested"))?;
|
||||
fs::copy(
|
||||
store.path().join("email/personal.gpg"),
|
||||
store.path().join(".extensions/nested/leaked-secret.gpg"),
|
||||
)?;
|
||||
fs::create_dir(store.path().join(".git"))?;
|
||||
fs::write(store.path().join(".git/secret.gpg"), b"not a packet")?;
|
||||
fs::write(store.path().join("visible-metadata.txt"), b"fixture")?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let reader = VaultReader::new(&repository, &keys);
|
||||
let mut provider = FixtureSecrets::all(&fixture);
|
||||
|
||||
let tree = reader.list(&DirectoryPath::root())?.render_plain();
|
||||
assert!(!tree.contains("extensions"));
|
||||
assert!(!tree.contains(".git"));
|
||||
assert!(!tree.contains("metadata"));
|
||||
assert!(!tree.contains(".gpg"));
|
||||
assert!(reader.find(&["leaked".to_owned()])?.matches().is_empty());
|
||||
assert!(reader.find(&["metadata".to_owned()])?.matches().is_empty());
|
||||
assert!(
|
||||
reader
|
||||
.grep(&grep("correct horse"), &mut provider)?
|
||||
.entries()
|
||||
.iter()
|
||||
.all(|entry| !entry.path().as_path().starts_with(Path::new(".extensions")))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn grep(pattern: &str) -> GrepRequest {
|
||||
GrepRequest {
|
||||
pattern: pattern.to_owned(),
|
||||
ignore_case: false,
|
||||
invert_match: false,
|
||||
line_number: false,
|
||||
fixed_strings: false,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user