mod onepassword;
mod pass;
pub(crate) use onepassword::OnePassword;
pub(crate) use pass::Pass;
use crate::ssh;
use crate::store::{Secret, Store, new_id, now};
use age::x25519;
use anyhow::Result;
use std::collections::{HashMap, HashSet};
pub(crate) trait Source {
type Handle;
fn name(&self) -> &'static str;
fn entries(&self, from: Option<&str>, prefix: Option<&str>)
-> Result<Vec<Entry<Self::Handle>>>;
fn fetch(&self, handle: &Self::Handle) -> Result<Fetched>;
}
pub(crate) struct Entry<H> {
pub(crate) label: String,
pub(crate) name: String,
pub(crate) handle: H,
}
pub(crate) enum Fetched {
Value {
value: String,
notes: Vec<String>,
blob: bool,
},
Skip(String),
}
pub(crate) enum Outcome {
Imported(Vec<String>),
Exists,
Skipped(String),
}
pub(crate) struct Line {
pub(crate) item: String,
pub(crate) name: String,
pub(crate) outcome: Outcome,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
Write,
DryRun,
}
pub(crate) fn import<S: Source>(
store: &Store,
source: &S,
entries: Vec<Entry<S::Handle>>,
mode: Mode,
key: Option<&x25519::Identity>,
) -> Result<Vec<Line>> {
let taken: HashSet<String> = match key {
Some(key) => store.names(key)?.into_iter().collect(),
None => HashSet::new(),
};
let mut claimed: HashMap<String, String> = HashMap::new();
let recipient = store.recipient()?;
let mut report = Vec::new();
for entry in entries {
let Entry {
label,
name,
handle,
} = entry;
if taken.contains(&name) {
report.push(Line {
item: label,
name,
outcome: Outcome::Exists,
});
continue;
}
if let Some(first) = claimed.get(&name) {
report.push(Line {
outcome: Outcome::Skipped(format!("{name} is already taken by {first}")),
item: label,
name,
});
continue;
}
let outcome = match source.fetch(&handle) {
Err(e) => Outcome::Skipped(format!("{e:#}")),
Ok(Fetched::Skip(reason)) => Outcome::Skipped(reason),
Ok(Fetched::Value { value, notes, blob }) => {
if mode == Mode::Write {
let secret = Secret {
name: name.clone(),
value,
created: now(),
updated: now(),
blob,
};
store.put(&new_id(), &secret, &recipient)?;
ssh::remember(store, &secret.value)?;
}
Outcome::Imported(notes)
}
};
if matches!(outcome, Outcome::Imported(_)) {
claimed.insert(name.clone(), label.clone());
}
report.push(Line {
item: label,
name,
outcome,
});
}
Ok(report)
}
pub(crate) fn under(prefix: Option<&str>, name: &str) -> String {
match prefix
.map(|p| p.trim_matches('/'))
.filter(|p| !p.is_empty())
{
Some(p) => format!("{p}/{name}"),
None => name.to_string(),
}
}