use crate::{Result, SecretSpecError};
use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, percent_encode};
use secrecy::SecretString;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::sync::OnceLock;
use url::Url;
pub(crate) const URI_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'<')
.add(b'>')
.add(b'[')
.add(b']')
.add(b'|')
.add(b'^')
.add(b'\\');
pub(crate) struct ProviderUrl(Url);
impl ProviderUrl {
pub fn new(url: Url) -> Self {
Self(url)
}
pub fn scheme(&self) -> &str {
self.0.scheme()
}
pub fn host(&self) -> Option<String> {
self.0
.host_str()
.map(|h| percent_decode_str(h).decode_utf8_lossy().into_owned())
}
pub fn username(&self) -> String {
percent_decode_str(self.0.username())
.decode_utf8_lossy()
.into_owned()
}
pub fn password(&self) -> Option<String> {
self.0
.password()
.map(|p| percent_decode_str(p).decode_utf8_lossy().into_owned())
}
pub fn path(&self) -> String {
percent_decode_str(self.0.path())
.decode_utf8_lossy()
.into_owned()
}
pub fn port(&self) -> Option<u16> {
self.0.port()
}
pub fn query_pairs(&self) -> url::form_urlencoded::Parse<'_> {
self.0.query_pairs()
}
pub fn encode(value: &str) -> String {
percent_encode(value.as_bytes(), URI_ENCODE_SET).to_string()
}
}
#[allow(dead_code)]
pub(crate) fn block_on<F: std::future::Future>(future: F) -> F::Output {
match tokio::runtime::Handle::try_current() {
Ok(handle) => tokio::task::block_in_place(|| handle.block_on(future)),
Err(_) => tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create tokio runtime")
.block_on(future),
}
}
#[cfg(feature = "awssm")]
pub mod awssm;
#[cfg(feature = "bws")]
pub mod bws;
pub mod dotenv;
pub mod env;
#[cfg(feature = "gcsm")]
pub mod gcsm;
#[cfg(feature = "keyring")]
pub mod keyring;
pub mod lastpass;
pub mod onepassword;
pub mod pass;
#[cfg(feature = "vault")]
pub mod vault;
#[macro_use]
pub mod macros;
#[cfg(test)]
pub(crate) mod tests;
#[derive(Debug, Clone)]
pub struct ProviderInfo {
pub name: &'static str,
pub description: &'static str,
pub examples: &'static [&'static str],
}
impl ProviderInfo {
pub fn display_with_examples(&self) -> String {
if self.examples.is_empty() {
format!("{}: {}", self.name, self.description)
} else {
format!(
"{}: {} (e.g., {})",
self.name,
self.description,
self.examples.join(", ")
)
}
}
}
pub use macros::{PROVIDER_REGISTRY, ProviderRegistration};
pub fn providers() -> Vec<ProviderInfo> {
PROVIDER_REGISTRY
.iter()
.map(|reg| reg.info.clone())
.collect()
}
pub trait Provider: Send + Sync {
fn get(&self, project: &str, key: &str, profile: &str) -> Result<Option<SecretString>>;
fn set(&self, project: &str, key: &str, value: &SecretString, profile: &str) -> Result<()>;
fn allows_set(&self) -> bool {
true
}
fn name(&self) -> &'static str;
fn uri(&self) -> String;
fn reflect(&self) -> Result<HashMap<String, crate::config::Secret>> {
Err(SecretSpecError::ProviderOperationFailed(format!(
"Provider '{}' does not support reflection",
self.name()
)))
}
fn get_batch(
&self,
project: &str,
keys: &[&str],
profile: &str,
) -> Result<HashMap<String, SecretString>> {
let mut results = HashMap::new();
for key in keys {
if let Some(value) = self.get(project, key, profile)? {
results.insert((*key).to_string(), value);
}
}
Ok(results)
}
}
impl<T: Provider> Provider for std::sync::Arc<T> {
fn get(&self, project: &str, key: &str, profile: &str) -> Result<Option<SecretString>> {
(**self).get(project, key, profile)
}
fn set(&self, project: &str, key: &str, value: &SecretString, profile: &str) -> Result<()> {
(**self).set(project, key, value, profile)
}
fn allows_set(&self) -> bool {
(**self).allows_set()
}
fn name(&self) -> &'static str {
(**self).name()
}
fn uri(&self) -> String {
(**self).uri()
}
fn reflect(&self) -> Result<HashMap<String, crate::config::Secret>> {
(**self).reflect()
}
fn get_batch(
&self,
project: &str,
keys: &[&str],
profile: &str,
) -> Result<HashMap<String, SecretString>> {
(**self).get_batch(project, keys, profile)
}
}
pub(crate) struct ProviderWithPreflight {
pub provider: Box<dyn Provider>,
pub preflight: Option<Box<dyn Fn() -> Result<()> + Send + Sync>>,
}
struct PreflightGuard {
inner: Box<dyn Provider>,
preflight: Option<Box<dyn Fn() -> Result<()> + Send + Sync>>,
result: OnceLock<std::result::Result<(), String>>,
}
impl PreflightGuard {
fn new(pwp: ProviderWithPreflight) -> Self {
Self {
inner: pwp.provider,
preflight: pwp.preflight,
result: OnceLock::new(),
}
}
fn check(&self) -> Result<()> {
let result = self.result.get_or_init(|| {
if let Some(f) = &self.preflight {
f().map_err(|e| e.to_string())
} else {
Ok(())
}
});
match result {
Ok(()) => Ok(()),
Err(msg) => Err(SecretSpecError::ProviderOperationFailed(msg.clone())),
}
}
}
impl Provider for PreflightGuard {
fn get(&self, project: &str, key: &str, profile: &str) -> Result<Option<SecretString>> {
self.check()?;
self.inner.get(project, key, profile)
}
fn set(&self, project: &str, key: &str, value: &SecretString, profile: &str) -> Result<()> {
self.check()?;
self.inner.set(project, key, value, profile)
}
fn allows_set(&self) -> bool {
self.inner.allows_set()
}
fn name(&self) -> &'static str {
self.inner.name()
}
fn uri(&self) -> String {
self.inner.uri()
}
fn reflect(&self) -> Result<HashMap<String, crate::config::Secret>> {
self.check()?;
self.inner.reflect()
}
fn get_batch(
&self,
project: &str,
keys: &[&str],
profile: &str,
) -> Result<HashMap<String, SecretString>> {
self.check()?;
self.inner.get_batch(project, keys, profile)
}
}
impl TryFrom<String> for Box<dyn Provider> {
type Error = SecretSpecError;
fn try_from(s: String) -> Result<Self> {
Self::try_from(&s as &str)
}
}
impl TryFrom<&str> for Box<dyn Provider> {
type Error = SecretSpecError;
fn try_from(s: &str) -> Result<Self> {
let (scheme, rest) = if let Some(pos) = s.find(':') {
let scheme = &s[..pos];
let rest = &s[pos + 1..];
(scheme, rest)
} else {
(s, "")
};
if scheme == "1password" {
return Err(SecretSpecError::ProviderOperationFailed(
"Invalid scheme '1password'. Use 'onepassword' instead (e.g., onepassword://vault/path)".to_string()
));
}
let is_valid_scheme = PROVIDER_REGISTRY
.iter()
.any(|reg| reg.schemes.contains(&scheme));
if !is_valid_scheme {
if PROVIDER_REGISTRY.iter().any(|reg| reg.info.name == scheme) {
return Err(SecretSpecError::ProviderOperationFailed(format!(
"Provider '{}' exists but URI parsing failed",
scheme
)));
} else {
return Err(SecretSpecError::ProviderNotFound(scheme.to_string()));
}
}
let url_string = match rest {
"" | ":" => format!("{}://", scheme),
s if s.starts_with("//") => format!("{}:{}", scheme, s),
s if s.starts_with('/') => format!("{}://{}", scheme, s),
s => format!("{}://{}", scheme, s),
};
let url_string = {
let scheme_end = url_string.find("://").unwrap() + 3;
let (prefix, rest) = url_string.split_at(scheme_end);
format!(
"{}{}",
prefix,
percent_encode(rest.as_bytes(), URI_ENCODE_SET)
)
};
let proper_url = Url::parse(&url_string).map_err(|e| {
SecretSpecError::ProviderOperationFailed(format!(
"Invalid provider specification '{}': {}",
s, e
))
})?;
provider_from_url(&ProviderUrl::new(proper_url))
}
}
impl TryFrom<&Url> for Box<dyn Provider> {
type Error = SecretSpecError;
fn try_from(url: &Url) -> Result<Self> {
provider_from_url(&ProviderUrl::new(url.clone()))
}
}
fn provider_from_url(url: &ProviderUrl) -> Result<Box<dyn Provider>> {
let scheme = url.scheme();
let registration = PROVIDER_REGISTRY
.iter()
.find(|reg| reg.schemes.contains(&scheme))
.ok_or_else(|| SecretSpecError::ProviderNotFound(scheme.to_string()))?;
let pwp = (registration.factory)(url)?;
if pwp.preflight.is_some() {
Ok(Box::new(PreflightGuard::new(pwp)))
} else {
Ok(pwp.provider)
}
}