use std::convert::TryFrom;
use std::sync::Arc;
use percent_encoding::percent_encode;
use url::Url;
use super::PROVIDER_REGISTRY;
use super::Provider;
use super::ProviderCredentials;
use super::ProviderUrl;
use super::URI_ENCODE_SET;
use super::preflight::PreflightGuard;
use super::registry::registration_for_scheme;
use super::registry::split_spec;
use super::spec_names_known_provider;
use super::url::WINDOWS_PATH_ENCODE_SET;
use super::url::is_windows_abs_path;
use crate::MonosecretError;
use crate::Result;
impl TryFrom<String> for Box<dyn Provider> {
type Error = MonosecretError;
fn try_from(s: String) -> Result<Self> {
Self::try_from(&s as &str)
}
}
impl TryFrom<&str> for Box<dyn Provider> {
type Error = MonosecretError;
fn try_from(s: &str) -> Result<Self> {
provider_from_spec(s, ProviderCredentials::new())
}
}
pub(crate) fn provider_from_spec(
s: &str,
credentials: ProviderCredentials,
) -> Result<Box<dyn Provider>> {
let url = provider_url_from_spec(s)?;
provider_from_url(&url, credentials)
}
pub(crate) fn external_provider_from_spec(
s: &str,
broker: Arc<dyn super::external::ProviderCredentialBroker>,
) -> Result<Box<dyn Provider>> {
let url = provider_url_from_spec(s)?;
reject_uri_credential(&url)?;
let scheme = url.scheme();
if registration_for_scheme(scheme).is_some() {
return Err(MonosecretError::ProviderOperationFailed(format!(
"provider '{scheme}' does not use runtime credential negotiation"
)));
}
let endpoint = super::external::discover(scheme)?
.ok_or_else(|| MonosecretError::ProviderNotFound(scheme.to_string()))?;
let mut provider = super::external::ExternalProvider::from_url(endpoint, &url);
provider.with_credential_broker(broker);
Ok(Box::new(provider))
}
pub(crate) fn provider_url_from_spec(s: &str) -> Result<ProviderUrl> {
let (scheme, rest) = split_spec(s);
if !spec_names_known_provider(s)? {
if PROVIDER_REGISTRY
.iter()
.any(|reg| reg.metadata.info.name == scheme)
{
return Err(MonosecretError::ProviderOperationFailed(format!(
"Provider '{scheme}' exists but URI parsing failed"
)));
}
return Err(MonosecretError::ProviderNotFound(scheme.to_string()));
}
let path_candidate = rest.trim_start_matches('/');
let url_string = if is_windows_abs_path(path_candidate) {
if scheme == "file" {
let path = path_candidate.replace('\\', "/");
format!(
"file:///{}",
percent_encode(path.as_bytes(), URI_ENCODE_SET)
)
} else {
format!(
"{}://{}",
scheme,
percent_encode(path_candidate.as_bytes(), WINDOWS_PATH_ENCODE_SET)
)
}
} else {
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 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| {
MonosecretError::ProviderOperationFailed(format!(
"Invalid provider specification '{}': {}",
crate::audit::redact_uri_strict(s),
e
))
})?;
Ok(ProviderUrl::new(proper_url))
}
impl TryFrom<&Url> for Box<dyn Provider> {
type Error = MonosecretError;
fn try_from(url: &Url) -> Result<Self> {
provider_from_url(&ProviderUrl::new(url.clone()), ProviderCredentials::new())
}
}
pub(crate) fn reject_uri_credential(url: &ProviderUrl) -> Result<()> {
if url.password().is_none() {
return Ok(());
}
let scheme = url.scheme();
let registration = registration_for_scheme(scheme);
let remedy = match registration {
Some(reg) if !reg.metadata.credential_names.is_empty() => {
let names = reg
.metadata
.credential_names
.iter()
.map(|name| format!("`{name}`"))
.collect::<Vec<_>>()
.join(", ");
format!(
"Supply it as the {names} provider credential instead \
(`monosecret config provider login <alias>`, or `credentials = \
{{ ... }}` on the alias), or use the provider's environment \
variable. See https://monosecret.dev/providers/{}/",
reg.metadata.info.name
)
}
Some(reg) => {
format!(
"The {} provider takes no credentials, so remove the userinfo from \
the URI. See https://monosecret.dev/providers/{}/",
reg.metadata.info.name, reg.metadata.info.name
)
}
None => "See https://monosecret.dev/reference/provider-credentials/".to_string(),
};
Err(MonosecretError::ProviderOperationFailed(format!(
"provider URI '{}' carries a password. Monosecret does not accept \
credentials in URIs: a URI reaches committed manifests, shell history, \
and CI logs, so a credential written there is already disclosed. {remedy}",
crate::audit::redact_uri_strict(url.as_url().as_str()),
)))
}
pub(crate) fn provider_from_url(
url: &ProviderUrl,
credentials: ProviderCredentials,
) -> Result<Box<dyn Provider>> {
provider_from_url_with_discovery(url, credentials, super::external::discover)
}
pub(crate) fn provider_from_url_with_discovery(
url: &ProviderUrl,
credentials: ProviderCredentials,
discover: impl FnOnce(&str) -> Result<Option<super::external::ProviderEndpoint>>,
) -> Result<Box<dyn Provider>> {
reject_uri_credential(url)?;
let scheme = url.scheme();
if let Some(registration) = registration_for_scheme(scheme) {
let pwp = (registration.factory)(url, credentials)?;
if pwp.preflight.is_some() {
Ok(Box::new(PreflightGuard::new(pwp)))
} else {
Ok(pwp.provider)
}
} else if let Some(endpoint) = discover(scheme)? {
let mut provider = super::external::ExternalProvider::from_url(endpoint, url);
provider.with_credentials(credentials);
Ok(Box::new(provider))
} else {
Err(MonosecretError::ProviderNotFound(scheme.to_string()))
}
}