Add desktop store and entry creation workflows
This commit is contained in:
@@ -6,15 +6,20 @@ use crate::{
|
||||
authentication::{
|
||||
AuthenticationTimeout, NativeAuthenticationHandle, NativeAuthenticationSession,
|
||||
},
|
||||
command::InitRequest,
|
||||
config::{Config, ConfigSettings, EditorCommand},
|
||||
crypto::{KeyInfo, KeyStore, SecretProvider},
|
||||
document::{DocumentError, EntryDocument, EntryDocumentService},
|
||||
git::{AutomaticEntryCommitter, GitIdentity},
|
||||
git::{AutomaticEntryCommitter, AutomaticPolicyCommitter, GitIdentity},
|
||||
presentation::ClipboardTimeout,
|
||||
read::{TreeModel, VaultReader},
|
||||
recipient::{
|
||||
PolicyCommit, PolicyCommitError, PolicyCommitter, RecipientPolicyManager,
|
||||
RecipientPolicyOutcome,
|
||||
},
|
||||
repository::{DirectoryPath, Repository},
|
||||
secret_store::SecretProtectionPolicy,
|
||||
write::{WriteError, WriteOutcome},
|
||||
write::{VaultWriter, WriteError, WriteOutcome},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -29,6 +34,7 @@ pub enum DesktopErrorKind {
|
||||
Conflict,
|
||||
Unchanged,
|
||||
MissingDefaultKey,
|
||||
EntryExists,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -193,6 +199,109 @@ impl DesktopStorage {
|
||||
.map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
|
||||
}
|
||||
|
||||
/// Enumerate storage-validated encryption keys for recipient selection.
|
||||
pub fn key_infos(&self) -> Result<Vec<KeyInfo>, DesktopError> {
|
||||
Ok(self.keys()?.infos().filter(KeyInfo::can_encrypt).collect())
|
||||
}
|
||||
|
||||
/// Apply an upstream-compatible root or nested `.gpg-id` policy while the
|
||||
/// shared authentication lease is active.
|
||||
pub fn apply_active_recipient_policy(
|
||||
&self,
|
||||
handle: &NativeAuthenticationHandle,
|
||||
request: &InitRequest,
|
||||
default_key: &str,
|
||||
) -> Result<(Self, RecipientPolicyOutcome), DesktopError> {
|
||||
handle
|
||||
.ensure_active()
|
||||
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
|
||||
let mut provider = handle.clone();
|
||||
self.apply_recipient_policy(request, default_key, &mut provider)
|
||||
}
|
||||
|
||||
pub fn apply_recipient_policy(
|
||||
&self,
|
||||
request: &InitRequest,
|
||||
default_key: &str,
|
||||
provider: &mut impl SecretProvider,
|
||||
) -> Result<(Self, RecipientPolicyOutcome), DesktopError> {
|
||||
let repository = self.repository()?;
|
||||
let keys = self.keys()?;
|
||||
let default = keys
|
||||
.infos()
|
||||
.find(|key| key.fingerprint().as_str() == default_key)
|
||||
.ok_or_else(|| {
|
||||
DesktopError::new(
|
||||
DesktopErrorKind::MissingDefaultKey,
|
||||
"the selected default GPG key is unavailable",
|
||||
)
|
||||
})?;
|
||||
if !default.has_secret() {
|
||||
return Err(DesktopError::new(
|
||||
DesktopErrorKind::MissingDefaultKey,
|
||||
"the selected default GPG key has no secret key material",
|
||||
));
|
||||
}
|
||||
let mut settings = self.settings();
|
||||
settings.set_default_key(default_key.to_owned());
|
||||
let replacement = self.validate_settings(settings)?;
|
||||
let directory = request.path.as_deref().unwrap_or_default();
|
||||
let committer = AutomaticPolicyCommitter::for_directory(
|
||||
&repository,
|
||||
directory,
|
||||
GitIdentity::ironstorage(),
|
||||
)
|
||||
.map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?;
|
||||
let mut committer = ConfigPolicyCommitter {
|
||||
git: committer,
|
||||
previous: &self.config,
|
||||
replacement: &replacement.config,
|
||||
persisted: false,
|
||||
};
|
||||
let outcome = RecipientPolicyManager::new(&repository, &keys)
|
||||
.apply_init(request, None, provider, &mut committer)
|
||||
.map_err(|error| DesktopError::new(DesktopErrorKind::Repository, error))?;
|
||||
if !committer.persisted {
|
||||
replacement.persist()?;
|
||||
}
|
||||
Ok((replacement, outcome))
|
||||
}
|
||||
|
||||
/// Open a collision-free in-memory draft. No repository bytes are written
|
||||
/// until the structured document is explicitly saved.
|
||||
pub fn create_active_document(
|
||||
&self,
|
||||
handle: &NativeAuthenticationHandle,
|
||||
entry: &str,
|
||||
) -> Result<EntryDocument, DesktopError> {
|
||||
handle
|
||||
.ensure_active()
|
||||
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
|
||||
let mut provider = handle.clone();
|
||||
self.create_document(entry, &mut provider)
|
||||
}
|
||||
|
||||
pub fn create_document(
|
||||
&self,
|
||||
entry: &str,
|
||||
provider: &mut impl SecretProvider,
|
||||
) -> Result<EntryDocument, DesktopError> {
|
||||
let repository = self.repository()?;
|
||||
let keys = self.keys()?;
|
||||
if VaultWriter::new(&repository, &keys)
|
||||
.entry_exists(entry)
|
||||
.map_err(|error| DesktopError::new(DesktopErrorKind::Document, error))?
|
||||
{
|
||||
return Err(DesktopError::new(
|
||||
DesktopErrorKind::EntryExists,
|
||||
format!("password-store entry already exists: {entry}"),
|
||||
));
|
||||
}
|
||||
EntryDocumentService::new(&repository, &keys)
|
||||
.open(entry, provider)
|
||||
.map_err(DesktopError::document)
|
||||
}
|
||||
|
||||
pub fn open_document(
|
||||
&self,
|
||||
entry: &str,
|
||||
@@ -239,6 +348,33 @@ impl DesktopStorage {
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigPolicyCommitter<'a> {
|
||||
git: AutomaticPolicyCommitter,
|
||||
previous: &'a Config,
|
||||
replacement: &'a Config,
|
||||
persisted: bool,
|
||||
}
|
||||
|
||||
impl PolicyCommitter for ConfigPolicyCommitter<'_> {
|
||||
fn commit(&mut self, change: &PolicyCommit) -> Result<(), PolicyCommitError> {
|
||||
self.replacement
|
||||
.persist()
|
||||
.map_err(|error| PolicyCommitError::new(error.to_string()))?;
|
||||
match self.git.commit(change) {
|
||||
Ok(()) => {
|
||||
self.persisted = true;
|
||||
Ok(())
|
||||
}
|
||||
Err(operation) => match self.previous.persist() {
|
||||
Ok(()) => Err(operation),
|
||||
Err(rollback) => Err(PolicyCommitError::new(format!(
|
||||
"recipient-policy commit failed ({operation}) and configuration rollback failed ({rollback})"
|
||||
))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DesktopBootstrap {
|
||||
storage: DesktopStorage,
|
||||
authentication: NativeAuthenticationSession,
|
||||
|
||||
@@ -938,8 +938,11 @@ impl GitRepository {
|
||||
path: &Path,
|
||||
identity: GitIdentity,
|
||||
) -> Result<Self, GitError> {
|
||||
let relative = validate_relative(path)?;
|
||||
let mut candidate = store.root_path().join(relative);
|
||||
let mut candidate = if path.as_os_str().is_empty() {
|
||||
store.root_path().to_owned()
|
||||
} else {
|
||||
store.root_path().join(validate_relative(path)?)
|
||||
};
|
||||
if !candidate.is_dir() {
|
||||
candidate.pop();
|
||||
}
|
||||
|
||||
@@ -103,6 +103,8 @@ fn nested_repository_selection_is_innermost() -> TestResult {
|
||||
let temporary = tempfile::tempdir()?;
|
||||
let outer_store = Repository::open(temporary.path())?;
|
||||
GitRepository::init(&outer_store, identity())?;
|
||||
let root = GitRepository::open_innermost(&outer_store, Path::new(""), identity())?;
|
||||
assert_eq!(root.root(), fs::canonicalize(temporary.path())?);
|
||||
fs::create_dir(temporary.path().join("nested"))?;
|
||||
let inner_store = Repository::open(temporary.path().join("nested"))?;
|
||||
GitRepository::init(&inner_store, identity())?;
|
||||
|
||||
Reference in New Issue
Block a user