Add desktop store and entry creation workflows
This commit is contained in:
@@ -6,6 +6,8 @@ use iced::keyboard::{self, key::Named};
|
|||||||
pub enum UiAction {
|
pub enum UiAction {
|
||||||
About,
|
About,
|
||||||
Settings,
|
Settings,
|
||||||
|
InitializeStore,
|
||||||
|
NewFolder,
|
||||||
NewEntry,
|
NewEntry,
|
||||||
OpenFolder,
|
OpenFolder,
|
||||||
OpenEntry,
|
OpenEntry,
|
||||||
@@ -24,6 +26,7 @@ pub enum UiAction {
|
|||||||
Refresh,
|
Refresh,
|
||||||
ReloadEntry,
|
ReloadEntry,
|
||||||
EditEntry,
|
EditEntry,
|
||||||
|
GeneratePassword,
|
||||||
ToggleReveal,
|
ToggleReveal,
|
||||||
Lock,
|
Lock,
|
||||||
Minimize,
|
Minimize,
|
||||||
@@ -35,6 +38,8 @@ impl UiAction {
|
|||||||
match self {
|
match self {
|
||||||
Self::About => "about",
|
Self::About => "about",
|
||||||
Self::Settings => "settings",
|
Self::Settings => "settings",
|
||||||
|
Self::InitializeStore => "initialize-store",
|
||||||
|
Self::NewFolder => "new-folder",
|
||||||
Self::NewEntry => "new-entry",
|
Self::NewEntry => "new-entry",
|
||||||
Self::OpenFolder => "open-folder",
|
Self::OpenFolder => "open-folder",
|
||||||
Self::OpenEntry => "open-entry",
|
Self::OpenEntry => "open-entry",
|
||||||
@@ -53,6 +58,7 @@ impl UiAction {
|
|||||||
Self::Refresh => "refresh",
|
Self::Refresh => "refresh",
|
||||||
Self::ReloadEntry => "reload-entry",
|
Self::ReloadEntry => "reload-entry",
|
||||||
Self::EditEntry => "edit-entry",
|
Self::EditEntry => "edit-entry",
|
||||||
|
Self::GeneratePassword => "generate-password",
|
||||||
Self::ToggleReveal => "toggle-reveal",
|
Self::ToggleReveal => "toggle-reveal",
|
||||||
Self::Lock => "lock",
|
Self::Lock => "lock",
|
||||||
Self::Minimize => "minimize",
|
Self::Minimize => "minimize",
|
||||||
@@ -112,6 +118,7 @@ pub struct ActionContext {
|
|||||||
pub modal_open: bool,
|
pub modal_open: bool,
|
||||||
pub focused_field: bool,
|
pub focused_field: bool,
|
||||||
pub focused_sensitive: bool,
|
pub focused_sensitive: bool,
|
||||||
|
pub focused_generatable: bool,
|
||||||
pub entry_path: bool,
|
pub entry_path: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +133,13 @@ pub struct ActionSpec {
|
|||||||
pub const ACTIONS: &[ActionSpec] = &[
|
pub const ACTIONS: &[ActionSpec] = &[
|
||||||
spec(UiAction::About, MenuGroup::App, "About IronStorage", None),
|
spec(UiAction::About, MenuGroup::App, "About IronStorage", None),
|
||||||
spec(UiAction::Settings, MenuGroup::App, "Settings…", Some("⌘,")),
|
spec(UiAction::Settings, MenuGroup::App, "Settings…", Some("⌘,")),
|
||||||
|
spec(
|
||||||
|
UiAction::InitializeStore,
|
||||||
|
MenuGroup::File,
|
||||||
|
"Initialize Store…",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
spec(UiAction::NewFolder, MenuGroup::File, "New Folder…", None),
|
||||||
spec(
|
spec(
|
||||||
UiAction::Quit,
|
UiAction::Quit,
|
||||||
MenuGroup::App,
|
MenuGroup::App,
|
||||||
@@ -179,6 +193,12 @@ pub const ACTIONS: &[ActionSpec] = &[
|
|||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
spec(UiAction::EditEntry, MenuGroup::Entry, "Edit Entry", None),
|
spec(UiAction::EditEntry, MenuGroup::Entry, "Edit Entry", None),
|
||||||
|
spec(
|
||||||
|
UiAction::GeneratePassword,
|
||||||
|
MenuGroup::Entry,
|
||||||
|
"Generate Password…",
|
||||||
|
None,
|
||||||
|
),
|
||||||
spec(
|
spec(
|
||||||
UiAction::ToggleReveal,
|
UiAction::ToggleReveal,
|
||||||
MenuGroup::Entry,
|
MenuGroup::Entry,
|
||||||
@@ -244,8 +264,13 @@ pub fn enabled(action: UiAction, context: ActionContext) -> bool {
|
|||||||
&& !context.modal_open
|
&& !context.modal_open
|
||||||
}
|
}
|
||||||
UiAction::CommandPalette => !context.modal_open,
|
UiAction::CommandPalette => !context.modal_open,
|
||||||
// Entry creation is not valid until the dedicated workflow exists.
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry => {
|
||||||
UiAction::NewEntry => false,
|
context.storage_ready
|
||||||
|
&& !context.dirty
|
||||||
|
&& !context.saving
|
||||||
|
&& !context.switching_vault
|
||||||
|
&& !context.modal_open
|
||||||
|
}
|
||||||
UiAction::OpenFolder => {
|
UiAction::OpenFolder => {
|
||||||
context.storage_ready && !context.saving && !context.switching_vault
|
context.storage_ready && !context.saving && !context.switching_vault
|
||||||
}
|
}
|
||||||
@@ -288,6 +313,13 @@ pub fn enabled(action: UiAction, context: ActionContext) -> bool {
|
|||||||
&& !context.editing
|
&& !context.editing
|
||||||
&& !context.switching_vault
|
&& !context.switching_vault
|
||||||
}
|
}
|
||||||
|
UiAction::GeneratePassword => {
|
||||||
|
context.unlocked
|
||||||
|
&& context.editing
|
||||||
|
&& context.focused_generatable
|
||||||
|
&& !context.switching_vault
|
||||||
|
&& !context.modal_open
|
||||||
|
}
|
||||||
UiAction::ToggleReveal => {
|
UiAction::ToggleReveal => {
|
||||||
context.unlocked
|
context.unlocked
|
||||||
&& context.document_open
|
&& context.document_open
|
||||||
@@ -303,7 +335,25 @@ pub fn disabled_reason(action: UiAction, context: ActionContext) -> Option<&'sta
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(match action {
|
Some(match action {
|
||||||
UiAction::NewEntry => "Entry creation is not available yet",
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry
|
||||||
|
if !context.storage_ready =>
|
||||||
|
{
|
||||||
|
"Shared configuration is unavailable"
|
||||||
|
}
|
||||||
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry if context.dirty => {
|
||||||
|
"Save or discard the current draft first"
|
||||||
|
}
|
||||||
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry if context.saving => {
|
||||||
|
"Wait for the active save"
|
||||||
|
}
|
||||||
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry
|
||||||
|
if context.switching_vault =>
|
||||||
|
{
|
||||||
|
"Wait for vault validation"
|
||||||
|
}
|
||||||
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry => {
|
||||||
|
"Close the current screen first"
|
||||||
|
}
|
||||||
UiAction::OpenFolder if !context.storage_ready => "Shared configuration is unavailable",
|
UiAction::OpenFolder if !context.storage_ready => "Shared configuration is unavailable",
|
||||||
UiAction::OpenFolder if context.saving => "Wait for the active save",
|
UiAction::OpenFolder if context.saving => "Wait for the active save",
|
||||||
UiAction::OpenFolder => "Wait for vault validation",
|
UiAction::OpenFolder => "Wait for vault validation",
|
||||||
@@ -345,6 +395,13 @@ pub fn disabled_reason(action: UiAction, context: ActionContext) -> Option<&'sta
|
|||||||
UiAction::EditEntry if !context.document_open => "Open an entry first",
|
UiAction::EditEntry if !context.document_open => "Open an entry first",
|
||||||
UiAction::EditEntry if context.editing => "The entry editor is already open",
|
UiAction::EditEntry if context.editing => "The entry editor is already open",
|
||||||
UiAction::EditEntry => "Wait for vault validation",
|
UiAction::EditEntry => "Wait for vault validation",
|
||||||
|
UiAction::GeneratePassword if !context.unlocked => "Unlock an entry first",
|
||||||
|
UiAction::GeneratePassword if !context.editing => "Open the entry editor first",
|
||||||
|
UiAction::GeneratePassword if !context.focused_generatable => {
|
||||||
|
"Select a password or other generatable secret field first"
|
||||||
|
}
|
||||||
|
UiAction::GeneratePassword if context.switching_vault => "Wait for vault validation",
|
||||||
|
UiAction::GeneratePassword => "Close the current screen first",
|
||||||
UiAction::ToggleReveal if !context.unlocked => "Unlock an entry first",
|
UiAction::ToggleReveal if !context.unlocked => "Unlock an entry first",
|
||||||
UiAction::ToggleReveal if !context.document_open => "Open an entry first",
|
UiAction::ToggleReveal if !context.document_open => "Open an entry first",
|
||||||
UiAction::ToggleReveal if !context.focused_sensitive => "Select a sensitive field first",
|
UiAction::ToggleReveal if !context.focused_sensitive => "Select a sensitive field first",
|
||||||
@@ -365,6 +422,8 @@ pub const fn aliases(action: UiAction) -> &'static [&'static str] {
|
|||||||
match action {
|
match action {
|
||||||
UiAction::About => &["version", "license", "credits"],
|
UiAction::About => &["version", "license", "credits"],
|
||||||
UiAction::Settings => &["preferences", "configuration", "config"],
|
UiAction::Settings => &["preferences", "configuration", "config"],
|
||||||
|
UiAction::InitializeStore => &["pass init", "recipients", "gpg id"],
|
||||||
|
UiAction::NewFolder => &["create folder", "directory", "nested recipients"],
|
||||||
UiAction::NewEntry => &["insert", "add password", "create entry"],
|
UiAction::NewEntry => &["insert", "add password", "create entry"],
|
||||||
UiAction::OpenFolder => &["open vault", "open store", "choose folder"],
|
UiAction::OpenFolder => &["open vault", "open store", "choose folder"],
|
||||||
UiAction::OpenEntry => &["show entry", "view password"],
|
UiAction::OpenEntry => &["show entry", "view password"],
|
||||||
@@ -382,6 +441,7 @@ pub const fn aliases(action: UiAction) -> &'static [&'static str] {
|
|||||||
UiAction::Refresh => &["reload vault", "refresh tree"],
|
UiAction::Refresh => &["reload vault", "refresh tree"],
|
||||||
UiAction::ReloadEntry => &["revert entry", "refresh entry"],
|
UiAction::ReloadEntry => &["revert entry", "refresh entry"],
|
||||||
UiAction::EditEntry => &["modify entry"],
|
UiAction::EditEntry => &["modify entry"],
|
||||||
|
UiAction::GeneratePassword => &["random password", "replace password", "generate"],
|
||||||
UiAction::ToggleReveal => &["show password", "hide password", "reveal field"],
|
UiAction::ToggleReveal => &["show password", "hide password", "reveal field"],
|
||||||
UiAction::Lock => &["secure", "log out", "relock"],
|
UiAction::Lock => &["secure", "log out", "relock"],
|
||||||
UiAction::Minimize => &["hide window"],
|
UiAction::Minimize => &["hide window"],
|
||||||
@@ -439,6 +499,7 @@ mod tests {
|
|||||||
modal_open: false,
|
modal_open: false,
|
||||||
focused_field: true,
|
focused_field: true,
|
||||||
focused_sensitive: true,
|
focused_sensitive: true,
|
||||||
|
focused_generatable: true,
|
||||||
entry_path: true,
|
entry_path: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,6 +525,7 @@ mod tests {
|
|||||||
assert!(!enabled(UiAction::NewEntry, ready));
|
assert!(!enabled(UiAction::NewEntry, ready));
|
||||||
assert!(enabled(UiAction::Save, ready));
|
assert!(enabled(UiAction::Save, ready));
|
||||||
assert!(enabled(UiAction::CopyEditedField, ready));
|
assert!(enabled(UiAction::CopyEditedField, ready));
|
||||||
|
assert!(enabled(UiAction::GeneratePassword, ready));
|
||||||
assert!(!enabled(UiAction::CopyField, ready));
|
assert!(!enabled(UiAction::CopyField, ready));
|
||||||
assert!(enabled(UiAction::ToggleReveal, ready));
|
assert!(enabled(UiAction::ToggleReveal, ready));
|
||||||
|
|
||||||
@@ -474,6 +536,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(enabled(UiAction::CopyField, viewing));
|
assert!(enabled(UiAction::CopyField, viewing));
|
||||||
assert!(!enabled(UiAction::CopyEditedField, viewing));
|
assert!(!enabled(UiAction::CopyEditedField, viewing));
|
||||||
|
assert!(enabled(UiAction::InitializeStore, viewing));
|
||||||
|
assert!(enabled(UiAction::NewFolder, viewing));
|
||||||
|
assert!(enabled(UiAction::NewEntry, viewing));
|
||||||
|
|
||||||
let locked = ActionContext {
|
let locked = ActionContext {
|
||||||
unlocked: false,
|
unlocked: false,
|
||||||
@@ -484,6 +549,7 @@ mod tests {
|
|||||||
UiAction::CopyField,
|
UiAction::CopyField,
|
||||||
UiAction::CopyEditedField,
|
UiAction::CopyEditedField,
|
||||||
UiAction::ToggleReveal,
|
UiAction::ToggleReveal,
|
||||||
|
UiAction::GeneratePassword,
|
||||||
UiAction::Lock,
|
UiAction::Lock,
|
||||||
] {
|
] {
|
||||||
assert!(!enabled(action, locked));
|
assert!(!enabled(action, locked));
|
||||||
@@ -523,6 +589,7 @@ mod tests {
|
|||||||
UiAction::Refresh,
|
UiAction::Refresh,
|
||||||
UiAction::ReloadEntry,
|
UiAction::ReloadEntry,
|
||||||
UiAction::EditEntry,
|
UiAction::EditEntry,
|
||||||
|
UiAction::GeneratePassword,
|
||||||
UiAction::ToggleReveal,
|
UiAction::ToggleReveal,
|
||||||
] {
|
] {
|
||||||
assert!(!enabled(action, switching), "{action:?}");
|
assert!(!enabled(action, switching), "{action:?}");
|
||||||
|
|||||||
@@ -35,6 +35,21 @@ impl EntryEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_entry(
|
||||||
|
document: EntryDocument,
|
||||||
|
password: SecretBytes,
|
||||||
|
) -> Result<Self, DocumentError> {
|
||||||
|
let mut editor = Self::new(document);
|
||||||
|
editor.add_after(None)?;
|
||||||
|
let id = editor
|
||||||
|
.focused
|
||||||
|
.expect("new entry receives its password field");
|
||||||
|
if !password.expose().is_empty() {
|
||||||
|
editor.replace_value(id, password)?;
|
||||||
|
}
|
||||||
|
Ok(editor)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn entry(&self) -> String {
|
pub fn entry(&self) -> String {
|
||||||
self.document.path().to_string()
|
self.document.path().to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -130,12 +130,15 @@ fn accelerator(action: UiAction) -> Option<Accelerator> {
|
|||||||
UiAction::Lock => (command, Code::KeyL),
|
UiAction::Lock => (command, Code::KeyL),
|
||||||
UiAction::Help => (Modifiers::empty(), Code::F1),
|
UiAction::Help => (Modifiers::empty(), Code::F1),
|
||||||
UiAction::About
|
UiAction::About
|
||||||
|
| UiAction::InitializeStore
|
||||||
|
| UiAction::NewFolder
|
||||||
| UiAction::CopyField
|
| UiAction::CopyField
|
||||||
| UiAction::CopyEditedField
|
| UiAction::CopyEditedField
|
||||||
| UiAction::TogglePaneFocus
|
| UiAction::TogglePaneFocus
|
||||||
| UiAction::OpenEntry
|
| UiAction::OpenEntry
|
||||||
| UiAction::ReloadEntry
|
| UiAction::ReloadEntry
|
||||||
| UiAction::EditEntry
|
| UiAction::EditEntry
|
||||||
|
| UiAction::GeneratePassword
|
||||||
| UiAction::ToggleReveal
|
| UiAction::ToggleReveal
|
||||||
| UiAction::Minimize => return None,
|
| UiAction::Minimize => return None,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -156,6 +156,16 @@ mod tests {
|
|||||||
assert_eq!(matches("setings").first(), Some(&UiAction::Settings));
|
assert_eq!(matches("setings").first(), Some(&UiAction::Settings));
|
||||||
assert_eq!(matches("rfsh").first(), Some(&UiAction::Refresh));
|
assert_eq!(matches("rfsh").first(), Some(&UiAction::Refresh));
|
||||||
assert_eq!(matches("open vault").first(), Some(&UiAction::OpenFolder));
|
assert_eq!(matches("open vault").first(), Some(&UiAction::OpenFolder));
|
||||||
|
assert_eq!(
|
||||||
|
matches("pass init").first(),
|
||||||
|
Some(&UiAction::InitializeStore)
|
||||||
|
);
|
||||||
|
assert_eq!(matches("create folder").first(), Some(&UiAction::NewFolder));
|
||||||
|
assert_eq!(matches("insert").first(), Some(&UiAction::NewEntry));
|
||||||
|
assert_eq!(
|
||||||
|
matches("generate").first(),
|
||||||
|
Some(&UiAction::GeneratePassword)
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matches(""),
|
matches(""),
|
||||||
action::ACTIONS
|
action::ACTIONS
|
||||||
|
|||||||
@@ -6,15 +6,20 @@ use crate::{
|
|||||||
authentication::{
|
authentication::{
|
||||||
AuthenticationTimeout, NativeAuthenticationHandle, NativeAuthenticationSession,
|
AuthenticationTimeout, NativeAuthenticationHandle, NativeAuthenticationSession,
|
||||||
},
|
},
|
||||||
|
command::InitRequest,
|
||||||
config::{Config, ConfigSettings, EditorCommand},
|
config::{Config, ConfigSettings, EditorCommand},
|
||||||
crypto::{KeyInfo, KeyStore, SecretProvider},
|
crypto::{KeyInfo, KeyStore, SecretProvider},
|
||||||
document::{DocumentError, EntryDocument, EntryDocumentService},
|
document::{DocumentError, EntryDocument, EntryDocumentService},
|
||||||
git::{AutomaticEntryCommitter, GitIdentity},
|
git::{AutomaticEntryCommitter, AutomaticPolicyCommitter, GitIdentity},
|
||||||
presentation::ClipboardTimeout,
|
presentation::ClipboardTimeout,
|
||||||
read::{TreeModel, VaultReader},
|
read::{TreeModel, VaultReader},
|
||||||
|
recipient::{
|
||||||
|
PolicyCommit, PolicyCommitError, PolicyCommitter, RecipientPolicyManager,
|
||||||
|
RecipientPolicyOutcome,
|
||||||
|
},
|
||||||
repository::{DirectoryPath, Repository},
|
repository::{DirectoryPath, Repository},
|
||||||
secret_store::SecretProtectionPolicy,
|
secret_store::SecretProtectionPolicy,
|
||||||
write::{WriteError, WriteOutcome},
|
write::{VaultWriter, WriteError, WriteOutcome},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -29,6 +34,7 @@ pub enum DesktopErrorKind {
|
|||||||
Conflict,
|
Conflict,
|
||||||
Unchanged,
|
Unchanged,
|
||||||
MissingDefaultKey,
|
MissingDefaultKey,
|
||||||
|
EntryExists,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -193,6 +199,109 @@ impl DesktopStorage {
|
|||||||
.map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
|
.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(
|
pub fn open_document(
|
||||||
&self,
|
&self,
|
||||||
entry: &str,
|
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 {
|
pub struct DesktopBootstrap {
|
||||||
storage: DesktopStorage,
|
storage: DesktopStorage,
|
||||||
authentication: NativeAuthenticationSession,
|
authentication: NativeAuthenticationSession,
|
||||||
|
|||||||
@@ -938,8 +938,11 @@ impl GitRepository {
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
identity: GitIdentity,
|
identity: GitIdentity,
|
||||||
) -> Result<Self, GitError> {
|
) -> Result<Self, GitError> {
|
||||||
let relative = validate_relative(path)?;
|
let mut candidate = if path.as_os_str().is_empty() {
|
||||||
let mut candidate = store.root_path().join(relative);
|
store.root_path().to_owned()
|
||||||
|
} else {
|
||||||
|
store.root_path().join(validate_relative(path)?)
|
||||||
|
};
|
||||||
if !candidate.is_dir() {
|
if !candidate.is_dir() {
|
||||||
candidate.pop();
|
candidate.pop();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ fn nested_repository_selection_is_innermost() -> TestResult {
|
|||||||
let temporary = tempfile::tempdir()?;
|
let temporary = tempfile::tempdir()?;
|
||||||
let outer_store = Repository::open(temporary.path())?;
|
let outer_store = Repository::open(temporary.path())?;
|
||||||
GitRepository::init(&outer_store, identity())?;
|
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"))?;
|
fs::create_dir(temporary.path().join("nested"))?;
|
||||||
let inner_store = Repository::open(temporary.path().join("nested"))?;
|
let inner_store = Repository::open(temporary.path().join("nested"))?;
|
||||||
GitRepository::init(&inner_store, identity())?;
|
GitRepository::init(&inner_store, identity())?;
|
||||||
|
|||||||
Reference in New Issue
Block a user