use ferrule_sql::{resolve_proxy_from_env, DatabaseUrl, ProxyConfig, SqlError, SshConfig};
use secrecy::ExposeSecret;
#[derive(Debug, Clone)]
pub struct ResolvedConnection {
pub url: DatabaseUrl,
pub secret: Option<secrecy::SecretString>,
pub ssh_config: Option<SshConfig>,
pub proxy: Option<ProxyConfig>,
}
pub fn resolve_connection(
connection: &str,
password: Option<String>,
ssh_config: Option<SshConfig>,
proxy_url: Option<&str>,
global_config: &ferrule_config::profile::GlobalConfig,
) -> Result<ResolvedConnection, SqlError> {
let url = resolve_url(connection, password, global_config)?;
let proxy = resolve_proxy_config(connection, proxy_url, global_config, &url)?;
let secret = url.password();
Ok(ResolvedConnection {
url,
secret,
ssh_config,
proxy,
})
}
fn resolve_url(
connection: &str,
password: Option<String>,
global_config: &ferrule_config::profile::GlobalConfig,
) -> Result<DatabaseUrl, SqlError> {
match DatabaseUrl::parse(connection) {
Ok(mut url) => {
if let Some(pwd) = password {
url.set_password(Some(&pwd));
}
Ok(url)
}
Err(_) => {
if let Some(profile) = global_config.connection.get(connection) {
let mut url = DatabaseUrl::parse(&profile.url).map_err(|e| {
SqlError::InvalidUrl(format!(
"Invalid URL in profile for '{}': {}",
connection, e
))
})?;
let resolved = ferrule_config::credentials::resolve_password_stack(
connection,
password.map(|p| secrecy::SecretString::new(p.into())),
profile.password_url.as_deref(),
)
.map_err(|e| SqlError::RegistryError(e.to_string()))?;
if let Some(pwd) = resolved {
url.set_password(Some(pwd.expose_secret()));
}
return Ok(url);
}
let registry = ferrule_config::registry::ConnectionRegistry::load_default()
.map_err(|e| SqlError::RegistryError(e.to_string()))?;
let entry = registry.get(connection).ok_or_else(|| {
SqlError::InvalidUrl(format!(
"Connection '{}' is not a valid URL and not found in registry or profile.",
connection
))
})?;
let mut url = DatabaseUrl::parse(&entry.url).map_err(|e| {
SqlError::InvalidUrl(format!(
"Invalid URL in registry for '{}': {}",
connection, e
))
})?;
let resolved = ferrule_config::credentials::resolve_password_stack(
connection,
password.map(|p| secrecy::SecretString::new(p.into())),
None,
)
.map_err(|e| SqlError::RegistryError(e.to_string()))?;
if let Some(pwd) = resolved {
url.set_password(Some(pwd.expose_secret()));
}
Ok(url)
}
}
}
fn resolve_proxy_config(
connection_name: &str,
proxy_url: Option<&str>,
global_config: &ferrule_config::profile::GlobalConfig,
url: &DatabaseUrl,
) -> Result<Option<ProxyConfig>, SqlError> {
if let Some(raw) = proxy_url {
return ProxyConfig::parse(raw)
.map(Some)
.map_err(|e| SqlError::InvalidUrl(format!("Invalid --proxy-url: {e}")));
}
if let Some(profile) = global_config.connection.get(connection_name) {
if let Some(raw) = &profile.proxy_url {
return ProxyConfig::parse(raw).map(Some).map_err(|e| {
SqlError::InvalidUrl(format!(
"Invalid proxy_url in profile for '{connection_name}': {e}"
))
});
}
}
let env_name = format!(
"FERRULE_{}_PROXY_URL",
connection_name.to_ascii_uppercase().replace('-', "_")
);
if let Ok(raw) = std::env::var(&env_name) {
if !raw.is_empty() {
return ProxyConfig::parse(&raw)
.map(Some)
.map_err(|e| SqlError::InvalidUrl(format!("{env_name} is set but invalid: {e}")));
}
}
let target_scheme = url.scheme();
if let Some(cfg) = resolve_proxy_from_env(target_scheme) {
if let Some(host) = url.host() {
if ferrule_sql::is_no_proxy(host) {
return Ok(None);
}
}
return Ok(Some(cfg));
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use secrecy::ExposeSecret;
#[test]
fn url_password_is_surfaced_as_secret() {
let cfg = ferrule_config::profile::GlobalConfig::default();
let resolved = resolve_connection(
"postgres://user:url_pw@localhost/db",
None,
None,
None,
&cfg,
)
.expect("resolve a plain URL");
let secret = resolved.secret.expect("a surfaced secret");
assert_eq!(secret.expose_secret(), "url_pw");
assert_eq!(
resolved
.url
.password()
.map(|p| p.expose_secret().to_string()),
Some("url_pw".to_string()),
);
}
#[test]
fn explicit_password_overrides_url_and_is_surfaced() {
let cfg = ferrule_config::profile::GlobalConfig::default();
let resolved = resolve_connection(
"postgres://user:url_pw@localhost/db",
Some("flag_pw".to_string()),
None,
None,
&cfg,
)
.expect("resolve with an explicit password");
assert_eq!(
resolved.secret.map(|s| s.expose_secret().to_string()),
Some("flag_pw".to_string()),
);
}
#[test]
fn passwordless_url_surfaces_no_secret() {
let cfg = ferrule_config::profile::GlobalConfig::default();
let resolved = resolve_connection("postgres://user@localhost/db", None, None, None, &cfg)
.expect("resolve a passwordless URL");
assert!(resolved.secret.is_none());
}
}