Implement iPhone password swipe actions
This commit is contained in:
1 parent
a748744425
commit
6edcb5fc86
7 files changed
+1816
-6
No files matched your search
@@ -9,12 +9,16 @@ use crate::{
|
||||
config::{Config, ConfigError},
|
||||
crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
||||
document::{DocumentError, EntryDocument, EntryDocumentService, EntryFieldId},
|
||||
git::{AutomaticEntryCommitter, GitIdentity},
|
||||
git::{AutomaticEntryCommitter, AutomaticTreeCommitter, GitError, GitIdentity},
|
||||
mobile_entry::{
|
||||
MobileEntryDraft, MobileEntryEditorError, MobileEntryEditorFieldKind,
|
||||
MobileEntryEditorInput, MobileEntryEditorPage, MobileEntryEditorSession, MobileEntryPage,
|
||||
MobileEntryValueError, field_value,
|
||||
},
|
||||
mobile_mutation::{
|
||||
MobileMutationAction, MobileMutationError, MobileMutationOutcome, MobileMutationPlan,
|
||||
MobileMutationRequest, MobileMutationService,
|
||||
},
|
||||
mobile_totp::{MobileTotpDetail, MobileTotpError, MobileTotpPage, MobileTotpService},
|
||||
recipient::RecipientPolicyManager,
|
||||
repository::{
|
||||
@@ -162,6 +166,7 @@ struct MobileAuthenticationStatus {
|
||||
active: Option<ActiveMobileLease>,
|
||||
next_editor_id: u64,
|
||||
editors: BTreeMap<u64, MobileEntryDraft>,
|
||||
mutation_active: bool,
|
||||
watch_shared_totp_entries: std::collections::BTreeSet<EntryPath>,
|
||||
}
|
||||
|
||||
@@ -196,6 +201,7 @@ impl MobileAuthentication {
|
||||
active: None,
|
||||
next_editor_id: 0,
|
||||
editors: BTreeMap::new(),
|
||||
mutation_active: false,
|
||||
watch_shared_totp_entries: config.watch_shared_totp_entries().clone(),
|
||||
}),
|
||||
config,
|
||||
@@ -399,6 +405,66 @@ impl MobileAuthentication {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prepare_entry_mutation(
|
||||
&self,
|
||||
path: &str,
|
||||
action: MobileMutationAction,
|
||||
) -> Result<MobileMutationPlan, MobileAuthenticationError> {
|
||||
let (has_open_editor, has_dirty_editor) = self.editor_state(path)?;
|
||||
MobileMutationService::new(&self.repository)
|
||||
.prepare(path, action, has_open_editor, has_dirty_editor)
|
||||
.map_err(mutation_error)
|
||||
}
|
||||
|
||||
pub fn perform_entry_mutation(
|
||||
&self,
|
||||
request: MobileMutationRequest,
|
||||
) -> Result<MobileMutationOutcome, MobileAuthenticationError> {
|
||||
self.ensure_active()?;
|
||||
self.reserve_entry_mutation()?;
|
||||
let result = self.perform_reserved_entry_mutation(request);
|
||||
self.release_entry_mutation();
|
||||
result
|
||||
}
|
||||
|
||||
fn perform_reserved_entry_mutation(
|
||||
&self,
|
||||
request: MobileMutationRequest,
|
||||
) -> Result<MobileMutationOutcome, MobileAuthenticationError> {
|
||||
let (handle, key) = {
|
||||
let status = self.status()?;
|
||||
let active = status.active.as_ref().ok_or_else(locked_error)?;
|
||||
(active.handle.clone(), active.key.clone())
|
||||
};
|
||||
let mut committer = AutomaticTreeCommitter::for_source(
|
||||
&self.repository,
|
||||
&request.source,
|
||||
GitIdentity::ironstorage(),
|
||||
)
|
||||
.map_err(git_mutation_error)?;
|
||||
let has_open_editor = self.editor_state(&request.source)?.0;
|
||||
let editors = if has_open_editor && request.discard_editor {
|
||||
self.take_entry_editors(&request.source)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut provider = KeyOnlyProvider::new(handle, &key);
|
||||
let result = MobileMutationService::new(&self.repository).perform(
|
||||
&request,
|
||||
has_open_editor && editors.is_empty(),
|
||||
&self.keys,
|
||||
&mut provider,
|
||||
&mut committer,
|
||||
);
|
||||
match result {
|
||||
Ok(outcome) => Ok(outcome),
|
||||
Err(error) => {
|
||||
self.restore_entry_editors(editors)?;
|
||||
Err(mutation_error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unlock_totp(
|
||||
&self,
|
||||
passphrase: Option<SecretBytes>,
|
||||
@@ -766,6 +832,12 @@ impl MobileAuthentication {
|
||||
draft: MobileEntryDraft,
|
||||
) -> Result<MobileEntryEditorSession, MobileAuthenticationError> {
|
||||
let mut status = self.status()?;
|
||||
if status.mutation_active {
|
||||
return Err(entry_detail(
|
||||
"Password Action In Progress",
|
||||
"wait for the current move, copy, or delete action to finish",
|
||||
));
|
||||
}
|
||||
let id = status.next_editor_id;
|
||||
status.next_editor_id = status.next_editor_id.checked_add(1).ok_or_else(|| {
|
||||
entry_detail(
|
||||
@@ -778,6 +850,66 @@ impl MobileAuthentication {
|
||||
Ok(MobileEntryEditorSession::new(id, page))
|
||||
}
|
||||
|
||||
fn reserve_entry_mutation(&self) -> Result<(), MobileAuthenticationError> {
|
||||
let mut status = self.status()?;
|
||||
// ponytail: serialize mobile mutations; use per-path reservations if concurrent UI needs it.
|
||||
if status.mutation_active {
|
||||
return Err(MobileAuthenticationError::new(
|
||||
MobileAuthenticationErrorKind::Conflict,
|
||||
"Password Action In Progress",
|
||||
"Wait for the current move, copy, or delete action to finish.",
|
||||
));
|
||||
}
|
||||
status.mutation_active = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release_entry_mutation(&self) {
|
||||
if let Ok(mut status) = self.status.lock() {
|
||||
status.mutation_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_state(&self, path: &str) -> Result<(bool, bool), MobileAuthenticationError> {
|
||||
let status = self.status()?;
|
||||
let mut matching = status
|
||||
.editors
|
||||
.values()
|
||||
.filter(|draft| draft.document().path().to_string() == path);
|
||||
let Some(first) = matching.next() else {
|
||||
return Ok((false, false));
|
||||
};
|
||||
Ok((
|
||||
true,
|
||||
first.document().is_modified() || matching.any(|draft| draft.document().is_modified()),
|
||||
))
|
||||
}
|
||||
|
||||
fn take_entry_editors(
|
||||
&self,
|
||||
path: &str,
|
||||
) -> Result<Vec<(u64, MobileEntryDraft)>, MobileAuthenticationError> {
|
||||
let mut status = self.status()?;
|
||||
let ids = status
|
||||
.editors
|
||||
.iter()
|
||||
.filter(|(_, draft)| draft.document().path().to_string() == path)
|
||||
.map(|(id, _)| *id)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(ids
|
||||
.into_iter()
|
||||
.filter_map(|id| status.editors.remove(&id).map(|draft| (id, draft)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn restore_entry_editors(
|
||||
&self,
|
||||
editors: Vec<(u64, MobileEntryDraft)>,
|
||||
) -> Result<(), MobileAuthenticationError> {
|
||||
self.status()?.editors.extend(editors);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_editor(
|
||||
&self,
|
||||
editor: u64,
|
||||
@@ -870,6 +1002,38 @@ fn document_error(error: DocumentError) -> MobileAuthenticationError {
|
||||
MobileAuthenticationError::new(kind, "Password Entry Could Not Be Saved", error.to_string())
|
||||
}
|
||||
|
||||
fn mutation_error(error: MobileMutationError) -> MobileAuthenticationError {
|
||||
MobileAuthenticationError::new(
|
||||
if error.is_conflict() {
|
||||
MobileAuthenticationErrorKind::Conflict
|
||||
} else {
|
||||
MobileAuthenticationErrorKind::Entry
|
||||
},
|
||||
error.title(),
|
||||
error.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn git_mutation_error(error: GitError) -> MobileAuthenticationError {
|
||||
let conflict = matches!(
|
||||
&error,
|
||||
GitError::DirtyWorktree | GitError::MergeConflicts { .. }
|
||||
);
|
||||
MobileAuthenticationError::new(
|
||||
if conflict {
|
||||
MobileAuthenticationErrorKind::Conflict
|
||||
} else {
|
||||
MobileAuthenticationErrorKind::Entry
|
||||
},
|
||||
if conflict {
|
||||
"Password Action Blocked by Git"
|
||||
} else {
|
||||
"Password Action Failed"
|
||||
},
|
||||
error.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn value_error(error: MobileEntryValueError) -> MobileAuthenticationError {
|
||||
entry_detail("Field Value Is Unavailable", error)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user