use super::ConcurrencyLimit;
use super::references::SecretReference;
use crate::provider::config::bws::BwsConfig;
use crate::provider::references::{BwsReference, Extract, HasReference};
use crate::provider::{ProviderError, SecretsProvider};
use async_trait::async_trait;
use bitwarden::{
Client,
auth::login::AccessTokenLoginRequest,
client::client_settings::{ClientSettings, DeviceType},
secrets_manager::{ClientSecretsExt, secrets::SecretGetRequest},
};
use futures::stream::{self, StreamExt};
use secrecy::ExposeSecret;
use secrecy::SecretString;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use url::Url;
use uuid::Uuid;
pub struct BwsProvider {
client: Client,
max_concurrent: ConcurrencyLimit,
}
impl BwsProvider {
pub async fn new(cfg: BwsConfig) -> Result<Self, ProviderError> {
let settings = ClientSettings {
identity_url: cfg.bws_identity_url.to_string(),
api_url: cfg.bws_api_url.to_string(),
user_agent: cfg.bws_user_agent,
device_type: DeviceType::SDK,
device_identifier: None,
bitwarden_package_type: None,
bitwarden_client_version: Some(env!("CARGO_PKG_VERSION").to_string()),
};
let client = Client::new(Some(settings));
let token = cfg.bws_token.resolve().await?;
let auth_req = AccessTokenLoginRequest {
access_token: token.expose_secret().to_string(),
state_file: None, };
client
.auth()
.login_access_token(&auth_req)
.await
.map_err(|e| ProviderError::Unauthorized(format!("BWS login failed: {:#?}", e)))?;
Ok(Self {
client,
max_concurrent: cfg.bws_max_concurrent,
})
}
}
impl HasReference for BwsProvider {
type Reference = BwsReference;
}
#[async_trait]
impl SecretsProvider for BwsProvider {
async fn fetch_map(
&self,
references: &[SecretReference],
) -> Result<HashMap<SecretReference, SecretString>, ProviderError> {
let refs: Vec<BwsReference> = references
.iter()
.filter_map(BwsReference::extract)
.copied()
.collect();
if refs.is_empty() {
return Ok(HashMap::new());
}
let mut map = HashMap::with_capacity(refs.len());
let client = &self.client;
let mut stream = stream::iter(refs)
.map(|reference| async move {
let id = Uuid::from(reference);
let req = SecretGetRequest { id };
let resp = client
.secrets()
.get(&req)
.await
.map_err(|e| ProviderError::NotFound(format!("{} ({})", id, e)))?;
Ok((reference.into(), SecretString::new(resp.value.into())))
})
.buffer_unordered(self.max_concurrent.into_inner());
while let Some(result) = stream.next().await {
match result {
Ok((key, value)) => {
map.insert(key, value);
}
Err(e) => return Err(e),
}
}
Ok(map)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub struct BwsUrl(Url);
impl BwsUrl {
pub fn as_bws_string(&self) -> &str {
let s = self.0.as_str();
s.strip_suffix('/').unwrap_or(s)
}
}
impl std::str::FromStr for BwsUrl {
type Err = ProviderError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(BwsUrl(Url::parse(s)?))
}
}
impl std::fmt::Display for BwsUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_bws_string())
}
}
impl AsRef<str> for BwsUrl {
fn as_ref(&self) -> &str {
self.as_bws_string()
}
}
impl From<Url> for BwsUrl {
fn from(url: Url) -> Self {
BwsUrl(url)
}
}