Skip to content

Commit

Permalink
Merge pull request #159 from andrewwhitehead/import-api
Browse files Browse the repository at this point in the history
Support copying individual profiles or a whole Store
  • Loading branch information
andrewwhitehead authored Aug 10, 2023
2 parents 26cbd92 + 79ee509 commit 9172fe2
Show file tree
Hide file tree
Showing 22 changed files with 838 additions and 91 deletions.
104 changes: 98 additions & 6 deletions askar-storage/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ use super::postgres;
use super::sqlite;

/// A dynamic store backend instance
pub type AnyBackend = Arc<dyn Backend<Session = AnyBackendSession>>;
#[derive(Clone, Debug)]
pub struct AnyBackend(Arc<dyn Backend<Session = AnyBackendSession>>);

/// Wrap a backend instance into an AnyBackend
pub fn into_any_backend(inst: impl Backend + 'static) -> AnyBackend {
Arc::new(WrapBackend(inst))
AnyBackend(Arc::new(WrapBackend(inst)))
}

/// This structure turns a generic backend into a concrete type
#[derive(Debug)]
struct WrapBackend<B: Backend>(B);

Expand All @@ -37,8 +39,23 @@ impl<B: Backend> Backend for WrapBackend<B> {
}

#[inline]
fn get_profile_name(&self) -> &str {
self.0.get_profile_name()
fn get_active_profile(&self) -> String {
self.0.get_active_profile()
}

#[inline]
fn get_default_profile(&self) -> BoxFuture<'_, Result<String, Error>> {
self.0.get_default_profile()
}

#[inline]
fn set_default_profile(&self, profile: String) -> BoxFuture<'_, Result<(), Error>> {
self.0.set_default_profile(profile)
}

#[inline]
fn list_profiles(&self) -> BoxFuture<'_, Result<Vec<String>, Error>> {
self.0.list_profiles()
}

#[inline]
Expand Down Expand Up @@ -82,6 +99,81 @@ impl<B: Backend> Backend for WrapBackend<B> {
}
}

// Forward to the concrete inner backend instance
impl Backend for AnyBackend {
type Session = AnyBackendSession;

#[inline]
fn create_profile(&self, name: Option<String>) -> BoxFuture<'_, Result<String, Error>> {
self.0.create_profile(name)
}

#[inline]
fn get_active_profile(&self) -> String {
self.0.get_active_profile()
}

#[inline]
fn get_default_profile(&self) -> BoxFuture<'_, Result<String, Error>> {
self.0.get_default_profile()
}

#[inline]
fn set_default_profile(&self, profile: String) -> BoxFuture<'_, Result<(), Error>> {
self.0.set_default_profile(profile)
}

#[inline]
fn list_profiles(&self) -> BoxFuture<'_, Result<Vec<String>, Error>> {
self.0.list_profiles()
}

#[inline]
fn remove_profile(&self, name: String) -> BoxFuture<'_, Result<bool, Error>> {
self.0.remove_profile(name)
}

#[inline]
fn scan(
&self,
profile: Option<String>,
kind: Option<EntryKind>,
category: Option<String>,
tag_filter: Option<TagFilter>,
offset: Option<i64>,
limit: Option<i64>,
) -> BoxFuture<'_, Result<Scan<'static, Entry>, Error>> {
self.0
.scan(profile, kind, category, tag_filter, offset, limit)
}

#[inline]
fn session(&self, profile: Option<String>, transaction: bool) -> Result<Self::Session, Error> {
Ok(AnyBackendSession(Box::new(
self.0.session(profile, transaction)?,
)))
}

#[inline]
fn rekey(
&mut self,
method: StoreKeyMethod,
key: PassKey<'_>,
) -> BoxFuture<'_, Result<(), Error>> {
match Arc::get_mut(&mut self.0) {
Some(inner) => inner.rekey(method, key),
None => Box::pin(std::future::ready(Err(err_msg!(
"Cannot re-key a store with multiple references"
)))),
}
}

#[inline]
fn close(&self) -> BoxFuture<'_, Result<(), Error>> {
self.0.close()
}
}

/// A dynamic store session instance
#[derive(Debug)]
pub struct AnyBackendSession(Box<dyn BackendSession>);
Expand Down Expand Up @@ -160,7 +252,7 @@ impl<'a> ManageBackend<'a> for &'a str {
self,
method: Option<StoreKeyMethod>,
pass_key: PassKey<'a>,
profile: Option<&'a str>,
profile: Option<String>,
) -> BoxFuture<'a, Result<Self::Backend, Error>> {
Box::pin(async move {
let opts = self.into_options()?;
Expand Down Expand Up @@ -194,7 +286,7 @@ impl<'a> ManageBackend<'a> for &'a str {
self,
method: StoreKeyMethod,
pass_key: PassKey<'a>,
profile: Option<&'a str>,
profile: Option<String>,
recreate: bool,
) -> BoxFuture<'a, Result<Self::Backend, Error>> {
Box::pin(async move {
Expand Down
5 changes: 3 additions & 2 deletions askar-storage/src/backend/db_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use sqlx::{
};

use crate::{
entry::{EncEntryTag, Entry, EntryTag, TagFilter},
entry::{EncEntryTag, Entry, EntryKind, EntryTag, TagFilter},
error::Error,
future::BoxFuture,
protect::{EntryEncryptor, KeyCache, PassKey, ProfileId, ProfileKey, StoreKey, StoreKeyMethod},
Expand Down Expand Up @@ -346,6 +346,7 @@ where
}

pub struct EncScanEntry {
pub kind: EntryKind,
pub category: Vec<u8>,
pub name: Vec<u8>,
pub value: Vec<u8>,
Expand Down Expand Up @@ -535,7 +536,7 @@ pub fn decrypt_scan_entry(
let tags = key.decrypt_entry_tags(
decode_tags(enc_entry.tags).map_err(|_| err_msg!(Unexpected, "Error decoding tags"))?,
)?;
Ok(Entry::new(category, name, value, tags))
Ok(Entry::new(enc_entry.kind, category, name, value, tags))
}

pub fn expiry_timestamp(expire_ms: i64) -> Result<Expiry, Error> {
Expand Down
87 changes: 82 additions & 5 deletions askar-storage/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::fmt::Debug;

use crate::{
entry::{Entry, EntryKind, EntryOperation, EntryTag, Scan, TagFilter},
error::Error,
error::{Error, ErrorKind},
future::BoxFuture,
protect::{PassKey, StoreKeyMethod},
};
Expand All @@ -31,7 +31,16 @@ pub trait Backend: Debug + Send + Sync {
fn create_profile(&self, name: Option<String>) -> BoxFuture<'_, Result<String, Error>>;

/// Get the name of the active profile
fn get_profile_name(&self) -> &str;
fn get_active_profile(&self) -> String;

/// Get the name of the default profile
fn get_default_profile(&self) -> BoxFuture<'_, Result<String, Error>>;

/// Set the the default profile
fn set_default_profile(&self, profile: String) -> BoxFuture<'_, Result<(), Error>>;

/// Get the details of all store profiles
fn list_profiles(&self) -> BoxFuture<'_, Result<Vec<String>, Error>>;

/// Remove an existing profile
fn remove_profile(&self, name: String) -> BoxFuture<'_, Result<bool, Error>>;
Expand Down Expand Up @@ -64,22 +73,22 @@ pub trait Backend: Debug + Send + Sync {
/// Create, open, or remove a generic backend implementation
pub trait ManageBackend<'a> {
/// The type of backend being managed
type Backend;
type Backend: Backend;

/// Open an existing store
fn open_backend(
self,
method: Option<StoreKeyMethod>,
pass_key: PassKey<'a>,
profile: Option<&'a str>,
profile: Option<String>,
) -> BoxFuture<'a, Result<Self::Backend, Error>>;

/// Provision a new store
fn provision_backend(
self,
method: StoreKeyMethod,
pass_key: PassKey<'a>,
profile: Option<&'a str>,
profile: Option<String>,
recreate: bool,
) -> BoxFuture<'a, Result<Self::Backend, Error>>;

Expand Down Expand Up @@ -116,6 +125,30 @@ pub trait BackendSession: Debug + Send {
for_update: bool,
) -> BoxFuture<'q, Result<Vec<Entry>, Error>>;

/// Insert scan results from another profile or store
fn import_scan<'q>(
&'q mut self,
mut scan: Scan<'q, Entry>,
) -> BoxFuture<'_, Result<(), Error>> {
Box::pin(async move {
while let Some(rows) = scan.fetch_next().await? {
for entry in rows {
self.update(
entry.kind,
EntryOperation::Insert,
entry.category.as_str(),
entry.name.as_str(),
Some(entry.value.as_ref()),
Some(entry.tags.as_ref()),
None,
)
.await?;
}
}
Ok(())
})
}

/// Remove all matching records from the store
fn remove_all<'q>(
&'q mut self,
Expand All @@ -140,3 +173,47 @@ pub trait BackendSession: Debug + Send {
/// Close the current store session
fn close(&mut self, commit: bool) -> BoxFuture<'_, Result<(), Error>>;
}

/// Insert all records from a given profile
pub async fn copy_profile<A: Backend, B: Backend>(
from_backend: &A,
to_backend: &B,
from_profile: &str,
to_profile: &str,
) -> Result<(), Error> {
let scan = from_backend
.scan(Some(from_profile.into()), None, None, None, None, None)
.await?;
if let Err(e) = to_backend.create_profile(Some(to_profile.into())).await {
if e.kind() != ErrorKind::Duplicate {
return Err(e);
}
}
let mut txn = to_backend.session(Some(to_profile.into()), true)?;
let count = txn.count(None, None, None).await?;
if count > 0 {
return Err(err_msg!(Input, "Profile targeted for import is not empty"));
}
txn.import_scan(scan).await?;
txn.close(true).await?;
Ok(())
}

/// Export an entire Store to another location
pub async fn copy_store<'m, B: Backend, M: ManageBackend<'m>>(
source: &B,
target: M,
key_method: StoreKeyMethod,
pass_key: PassKey<'m>,
recreate: bool,
) -> Result<(), Error> {
let default_profile = source.get_default_profile().await?;
let profile_ids = source.list_profiles().await?;
let target = target
.provision_backend(key_method, pass_key, Some(default_profile), recreate)
.await?;
for profile in profile_ids {
copy_profile(source, &target, &profile, &profile).await?;
}
Ok(())
}
Loading

0 comments on commit 9172fe2

Please sign in to comment.