1use ferrule_sql::{resolve_proxy_from_env, DatabaseUrl, ProxyConfig, SqlError, SshConfig};
9use secrecy::ExposeSecret;
10
11#[derive(Debug, Clone)]
29pub struct ResolvedConnection {
30 pub url: DatabaseUrl,
31 pub secret: Option<secrecy::SecretString>,
32 pub ssh_config: Option<SshConfig>,
33 pub proxy: Option<ProxyConfig>,
34}
35
36pub fn resolve_connection(
43 connection: &str,
44 password: Option<String>,
45 ssh_config: Option<SshConfig>,
46 proxy_url: Option<&str>,
47 global_config: &ferrule_config::profile::GlobalConfig,
48) -> Result<ResolvedConnection, SqlError> {
49 let url = resolve_url(connection, password, global_config)?;
50 let proxy = resolve_proxy_config(connection, proxy_url, global_config, &url)?;
51 let secret = url.password();
57 Ok(ResolvedConnection {
58 url,
59 secret,
60 ssh_config,
61 proxy,
62 })
63}
64
65fn resolve_url(
68 connection: &str,
69 password: Option<String>,
70 global_config: &ferrule_config::profile::GlobalConfig,
71) -> Result<DatabaseUrl, SqlError> {
72 match DatabaseUrl::parse(connection) {
73 Ok(mut url) => {
74 if let Some(pwd) = password {
75 url.set_password(Some(&pwd));
76 }
77 Ok(url)
78 }
79 Err(_) => {
80 if let Some(profile) = global_config.connection.get(connection) {
82 let mut url = DatabaseUrl::parse(&profile.url).map_err(|e| {
83 SqlError::InvalidUrl(format!(
84 "Invalid URL in profile for '{}': {}",
85 connection, e
86 ))
87 })?;
88 let resolved = ferrule_config::credentials::resolve_password_stack(
89 connection,
90 password.map(|p| secrecy::SecretString::new(p.into())),
91 profile.password_url.as_deref(),
92 )
93 .map_err(|e| SqlError::RegistryError(e.to_string()))?;
94 if let Some(pwd) = resolved {
95 url.set_password(Some(pwd.expose_secret()));
96 }
97 return Ok(url);
98 }
99
100 let registry = ferrule_config::registry::ConnectionRegistry::load_default()
102 .map_err(|e| SqlError::RegistryError(e.to_string()))?;
103 let entry = registry.get(connection).ok_or_else(|| {
104 SqlError::InvalidUrl(format!(
105 "Connection '{}' is not a valid URL and not found in registry or profile.",
106 connection
107 ))
108 })?;
109 let mut url = DatabaseUrl::parse(&entry.url).map_err(|e| {
110 SqlError::InvalidUrl(format!(
111 "Invalid URL in registry for '{}': {}",
112 connection, e
113 ))
114 })?;
115
116 let resolved = ferrule_config::credentials::resolve_password_stack(
117 connection,
118 password.map(|p| secrecy::SecretString::new(p.into())),
119 None,
120 )
121 .map_err(|e| SqlError::RegistryError(e.to_string()))?;
122 if let Some(pwd) = resolved {
123 url.set_password(Some(pwd.expose_secret()));
124 }
125 Ok(url)
126 }
127 }
128}
129
130fn resolve_proxy_config(
132 connection_name: &str,
133 proxy_url: Option<&str>,
134 global_config: &ferrule_config::profile::GlobalConfig,
135 url: &DatabaseUrl,
136) -> Result<Option<ProxyConfig>, SqlError> {
137 if let Some(raw) = proxy_url {
139 return ProxyConfig::parse(raw)
140 .map(Some)
141 .map_err(|e| SqlError::InvalidUrl(format!("Invalid --proxy-url: {e}")));
142 }
143
144 if let Some(profile) = global_config.connection.get(connection_name) {
146 if let Some(raw) = &profile.proxy_url {
147 return ProxyConfig::parse(raw).map(Some).map_err(|e| {
148 SqlError::InvalidUrl(format!(
149 "Invalid proxy_url in profile for '{connection_name}': {e}"
150 ))
151 });
152 }
153 }
154
155 let env_name = format!(
157 "FERRULE_{}_PROXY_URL",
158 connection_name.to_ascii_uppercase().replace('-', "_")
159 );
160 if let Ok(raw) = std::env::var(&env_name) {
161 if !raw.is_empty() {
162 return ProxyConfig::parse(&raw)
163 .map(Some)
164 .map_err(|e| SqlError::InvalidUrl(format!("{env_name} is set but invalid: {e}")));
165 }
166 }
167
168 let target_scheme = url.scheme();
170 if let Some(cfg) = resolve_proxy_from_env(target_scheme) {
171 if let Some(host) = url.host() {
172 if ferrule_sql::is_no_proxy(host) {
173 return Ok(None);
174 }
175 }
176 return Ok(Some(cfg));
177 }
178
179 Ok(None)
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use secrecy::ExposeSecret;
186
187 #[test]
193 fn url_password_is_surfaced_as_secret() {
194 let cfg = ferrule_config::profile::GlobalConfig::default();
195 let resolved = resolve_connection(
196 "postgres://user:url_pw@localhost/db",
197 None,
198 None,
199 None,
200 &cfg,
201 )
202 .expect("resolve a plain URL");
203
204 let secret = resolved.secret.expect("a surfaced secret");
205 assert_eq!(secret.expose_secret(), "url_pw");
206 assert_eq!(
207 resolved
208 .url
209 .password()
210 .map(|p| p.expose_secret().to_string()),
211 Some("url_pw".to_string()),
212 );
213 }
214
215 #[test]
218 fn explicit_password_overrides_url_and_is_surfaced() {
219 let cfg = ferrule_config::profile::GlobalConfig::default();
220 let resolved = resolve_connection(
221 "postgres://user:url_pw@localhost/db",
222 Some("flag_pw".to_string()),
223 None,
224 None,
225 &cfg,
226 )
227 .expect("resolve with an explicit password");
228
229 assert_eq!(
230 resolved.secret.map(|s| s.expose_secret().to_string()),
231 Some("flag_pw".to_string()),
232 );
233 }
234
235 #[test]
238 fn passwordless_url_surfaces_no_secret() {
239 let cfg = ferrule_config::profile::GlobalConfig::default();
240 let resolved = resolve_connection("postgres://user@localhost/db", None, None, None, &cfg)
241 .expect("resolve a passwordless URL");
242 assert!(resolved.secret.is_none());
243 }
244}