use async_trait::async_trait;
use clap::{Args, ValueEnum};
use locket_derive::LayeredConfig;
use secrecy::SecretString;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(not(any(
feature = "op",
feature = "connect",
feature = "bws",
feature = "infisical"
)))]
compile_error!(
"At least one provider feature must be enabled (e.g. --features op,connect,bws,infisical)"
);
#[cfg(feature = "bws")]
mod bws;
pub mod config;
#[cfg(feature = "connect")]
mod connect;
#[cfg(feature = "infisical")]
mod infisical;
pub mod managed;
#[cfg(feature = "op")]
mod op;
mod references;
mod types;
use managed::{ManagedProvider, ProviderFactory};
pub use references::{ReferenceParseError, ReferenceParser, SecretReference};
pub use types::{AuthToken, ConcurrencyLimit, TokenSource};
#[async_trait]
pub trait Signature: Send + Sync {
async fn signature(&self) -> Result<u64, ProviderError>;
}
#[derive(Debug, thiserror::Error)]
pub enum ProviderError {
#[error("network request failed: {0}")]
Network(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("secret not found: {0}")]
NotFound(String),
#[error("access denied: {0}")]
Unauthorized(String),
#[error("rate limited")]
RateLimit,
#[error("{0}")]
Other(String),
#[error("invalid config: {0}")]
InvalidConfig(String),
#[error("invalid id: {0}")]
InvalidId(String),
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("command '{program}' failed with status {status:?}: {stderr}")]
Exec {
program: &'static str,
status: Option<i32>,
stderr: String,
},
#[error("url error: {0}")]
Url(#[from] url::ParseError),
}
#[async_trait]
pub trait SecretsProvider: ReferenceParser + Send + Sync {
async fn fetch_map(
&self,
references: &[SecretReference],
) -> Result<HashMap<SecretReference, SecretString>, ProviderError>;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Provider {
#[cfg(feature = "op")]
Op(config::op::OpConfig),
#[cfg(feature = "connect")]
Connect(config::connect::ConnectConfig),
#[cfg(feature = "bws")]
Bws(config::bws::BwsConfig),
#[cfg(feature = "infisical")]
Infisical(config::infisical::InfisicalConfig),
}
impl Provider {
pub async fn build(self) -> Result<Arc<dyn SecretsProvider>, ProviderError> {
let managed = ManagedProvider::new(self).await?;
Ok(Arc::new(managed))
}
}
#[async_trait]
impl Signature for Provider {
async fn signature(&self) -> Result<u64, ProviderError> {
match self {
#[cfg(feature = "op")]
Self::Op(c) => c.signature().await,
#[cfg(feature = "connect")]
Self::Connect(c) => c.signature().await,
#[cfg(feature = "bws")]
Self::Bws(c) => c.signature().await,
#[cfg(feature = "infisical")]
Self::Infisical(c) => c.signature().await,
}
}
}
impl ReferenceParser for Provider {
fn parse(&self, raw: &str) -> Option<SecretReference> {
match self {
#[cfg(feature = "op")]
Self::Op(cfg) => cfg.parse(raw),
#[cfg(feature = "connect")]
Self::Connect(cfg) => cfg.parse(raw),
#[cfg(feature = "bws")]
Self::Bws(cfg) => cfg.parse(raw),
#[cfg(feature = "infisical")]
Self::Infisical(cfg) => cfg.parse(raw),
}
}
}
#[async_trait]
impl ProviderFactory for Provider {
async fn create(&self) -> Result<Arc<dyn SecretsProvider>, ProviderError> {
let provider: Arc<dyn SecretsProvider> = match self {
#[cfg(feature = "op")]
Self::Op(c) => Arc::new(op::OpProvider::new(c.clone()).await?),
#[cfg(feature = "connect")]
Self::Connect(c) => Arc::new(connect::OpConnectProvider::new(c.clone()).await?),
#[cfg(feature = "bws")]
Self::Bws(c) => Arc::new(bws::BwsProvider::new(c.clone()).await?),
#[cfg(feature = "infisical")]
Self::Infisical(c) => Arc::new(infisical::InfisicalProvider::new(c.clone()).await?),
};
Ok(provider)
}
}
#[derive(
Args, Debug, Clone, Hash, PartialEq, Eq, LayeredConfig, Deserialize, Serialize, Default,
)]
#[serde(rename_all = "kebab-case")]
pub struct ProviderArgs {
#[arg(long, env = "SECRETS_PROVIDER")]
pub provider: Option<ProviderKind>,
#[command(flatten)]
#[serde(flatten)]
pub config: ProviderConfigs,
}
impl TryFrom<ProviderArgs> for Provider {
type Error = crate::error::LocketError;
fn try_from(args: ProviderArgs) -> Result<Self, Self::Error> {
use crate::config::ApplyDefaults;
let args = args.apply_defaults();
let kind = args.provider.ok_or_else(|| {
crate::config::ConfigError::Validation(
"Missing required argument: --provider <kind>".into(),
)
})?;
match kind {
#[cfg(feature = "bws")]
ProviderKind::Bws => Ok(Provider::Bws(args.config.bws.try_into()?)),
#[cfg(feature = "op")]
ProviderKind::Op => Ok(Provider::Op(args.config.op.try_into()?)),
#[cfg(feature = "connect")]
ProviderKind::OpConnect => Ok(Provider::Connect(args.config.connect.try_into()?)),
#[cfg(feature = "infisical")]
ProviderKind::Infisical => Ok(Provider::Infisical(args.config.infisical.try_into()?)),
}
}
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, ValueEnum, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProviderKind {
#[cfg(feature = "op")]
Op,
#[cfg(feature = "connect")]
OpConnect,
#[cfg(feature = "bws")]
Bws,
#[cfg(feature = "infisical")]
Infisical,
}
#[derive(
Args, Debug, Clone, Hash, PartialEq, Eq, LayeredConfig, Deserialize, Serialize, Default,
)]
#[serde(rename_all = "kebab-case")]
pub struct ProviderConfigs {
#[cfg(feature = "op")]
#[command(flatten, next_help_heading = "1Password (op)")]
#[serde(flatten)]
pub op: config::op::OpArgs,
#[cfg(feature = "connect")]
#[command(flatten, next_help_heading = "1Password Connect")]
#[serde(flatten)]
pub connect: config::connect::ConnectArgs,
#[cfg(feature = "bws")]
#[command(flatten, next_help_heading = "Bitwarden Secrets Provider")]
#[serde(flatten)]
pub bws: config::bws::BwsArgs,
#[cfg(feature = "infisical")]
#[command(flatten, next_help_heading = "Infisical Secrets Provider")]
#[serde(flatten)]
pub infisical: config::infisical::InfisicalArgs,
}