use std::{collections::HashMap, path::Path, sync::Arc};
use anyhow::{Ok, Result};
use async_trait::async_trait;
use futures::future::try_join_all;
use itertools::Itertools;
use log::debug;
use rustc_hash::FxHashMap;
use url::Url;
use crate::Hydration;
use super::{Provider, Warnings, add_url, deserialize_output, run_cli};
#[derive(Default)]
pub struct Passbolt {
urls: FxHashMap<Url, String>,
}
impl Passbolt {
pub fn new() -> Self {
Default::default()
}
}
#[async_trait]
impl Provider for Passbolt {
fn add(&mut self, value: String) -> Result<()> {
add_url(&mut self.urls, value, "passbolt")
}
fn name(&self) -> &'static str {
"Passbolt"
}
fn install_url(&self) -> &'static str {
"https://github.com/passbolt/go-passbolt-cli"
}
fn has_work(&self) -> bool {
!self.urls.is_empty()
}
async fn resolve(
&self,
_: &Path,
extra_env: &HashMap<String, String>,
_: &Warnings,
) -> Result<Hydration> {
let extra_env = Arc::new(extra_env.clone());
let name = self.name();
let install_url = self.install_url();
let fetches = self
.urls
.iter()
.into_group_map_by(|(url, _)| url.host().expect("Missing host"))
.into_iter()
.flat_map(|(host, group)| {
let extra_env = Arc::clone(&extra_env);
group
.into_iter()
.into_group_map_by(|(url, _)| {
url.path().split('/').nth(1).expect("Missing resource id")
})
.into_iter()
.map(move |(resource_id, group)| {
let host = host.clone();
let extra_env = Arc::clone(&extra_env);
async move {
let cmd = [
"passbolt",
"get",
"resource",
&format!("--serverAddress=https://{}", host),
&format!("--id={}", resource_id),
"--json",
];
debug!("Lade run: {}", cmd.join(" "));
let child = run_cli(&cmd, &extra_env, name, install_url, None).await?;
let loaded: HashMap<String, String> = deserialize_output(&child, name)?;
let hydration = group
.into_iter()
.map(|(url, value)| {
let var = url.path().split('/').nth(2).expect("Missing field");
(
value.clone(),
loaded
.get(var)
.unwrap_or_else(|| {
panic!(
"Variable not found in Passbolt: {}",
resource_id
)
})
.clone(),
)
})
.collect::<Hydration>();
debug!("hydration: {:?}", hydration);
Ok(hydration)
}
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
Ok(try_join_all(fetches).await?.into_iter().flatten().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::providers::fake_cli;
use std::collections::HashMap;
use std::path::Path;
use tempfile::tempdir;
#[test]
fn test_add_valid_passbolt_scheme() {
let mut p = Passbolt::new();
assert!(
p.add("passbolt://passbolt.example.com/resource-uuid/password".to_string())
.is_ok()
);
}
#[test]
fn test_add_rejects_wrong_scheme() {
let mut p = Passbolt::new();
assert!(p.add("vault://host/mount/key/field".to_string()).is_err());
}
#[tokio::test]
#[cfg(unix)]
async fn test_resolve_fake_cli() {
let fake_bin = tempdir().unwrap();
fake_cli(
&fake_bin,
"passbolt",
r#"echo '{"password":"passbolt_value","username":"user"}'"#,
);
let mut p = Passbolt::new();
p.add("passbolt://passbolt.example.com/resource-uuid/password".to_string())
.unwrap();
let extra = HashMap::from([(
"PATH".to_string(),
fake_bin.path().to_string_lossy().into_owned(),
)]);
let result = p
.resolve(Path::new("."), &extra, &Warnings::default())
.await
.unwrap();
assert_eq!(
result
.get("passbolt://passbolt.example.com/resource-uuid/password")
.unwrap(),
"passbolt_value"
);
}
#[tokio::test]
#[cfg(unix)]
async fn test_resolve_malformed_json_error() {
let fake_bin = tempdir().unwrap();
fake_cli(&fake_bin, "passbolt", "echo 'not valid json'");
let mut p = Passbolt::new();
p.add("passbolt://passbolt.example.com/resource-uuid/password".to_string())
.unwrap();
let extra = HashMap::from([(
"PATH".to_string(),
fake_bin.path().to_string_lossy().into_owned(),
)]);
let result = p
.resolve(Path::new("."), &extra, &Warnings::default())
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Passbolt error"));
}
#[tokio::test]
#[cfg(unix)]
async fn test_resolve_cli_not_found() {
let empty_bin = tempdir().unwrap();
let mut p = Passbolt::new();
p.add("passbolt://passbolt.example.com/resource-uuid/password".to_string())
.unwrap();
let extra = HashMap::from([(
"PATH".to_string(),
empty_bin.path().to_string_lossy().into_owned(),
)]);
let result = p
.resolve(Path::new("."), &extra, &Warnings::default())
.await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Passbolt CLI not found")
);
}
}