alien_bindings/providers/postgres/
mod.rs1#[cfg(feature = "aws")]
22pub(crate) mod aurora;
23#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))]
24pub(crate) mod cloud;
25#[cfg(feature = "gcp")]
26pub(crate) mod cloud_sql;
27#[cfg(feature = "azure")]
28pub(crate) mod flexible_server;
29pub mod local;
30pub(crate) mod runtime;
31
32use crate::error::{ErrorData, Result};
33#[cfg(any(feature = "gcp", test))]
34use crate::traits::SslMode;
35use crate::traits::{Binding, Postgres, PostgresConnectionParams, PostgresTlsPolicy};
36use alien_core::bindings::BindingValue;
37#[cfg(feature = "gcp")]
38use alien_error::AlienError;
39use alien_error::Context;
40#[cfg(any(feature = "aws", feature = "azure"))]
41use std::sync::OnceLock;
42
43#[cfg(feature = "aws")]
49pub(crate) const AWS_RDS_CA_CERTIFICATES: &[&str] = &[include_str!("ca/aws-rds-global-roots.pem")];
50
51#[cfg(feature = "azure")]
56pub(crate) const AZURE_POSTGRES_CA_CERTIFICATES: &[&str] =
57 &[include_str!("ca/azure-postgres-roots.pem")];
58
59#[derive(Debug)]
64pub struct ResolvedPostgres {
65 params: PostgresConnectionParams,
66}
67
68impl ResolvedPostgres {
69 pub fn new(params: PostgresConnectionParams) -> Self {
70 Self { params }
71 }
72}
73
74impl Binding for ResolvedPostgres {}
75
76impl Postgres for ResolvedPostgres {
77 fn connection_params(&self) -> &PostgresConnectionParams {
78 &self.params
79 }
80}
81
82pub(crate) struct PostgresConnectionInput<'a> {
84 pub(crate) host: &'a BindingValue<String>,
85 pub(crate) port: &'a BindingValue<u16>,
86 pub(crate) database: &'a BindingValue<String>,
87 pub(crate) username: &'a BindingValue<String>,
88 pub(crate) password: &'a str,
89 pub(crate) tls: PostgresTlsPolicy,
90}
91
92pub(crate) fn resolve_params(
101 binding_name: &str,
102 input: PostgresConnectionInput<'_>,
103) -> Result<PostgresConnectionParams> {
104 let invalid = |field: &str| ErrorData::BindingConfigInvalid {
105 env_var: crate::error::binding_env_var(binding_name),
106 binding_name: binding_name.to_string(),
107 reason: format!("Failed to extract '{}' from Postgres binding", field),
108 };
109
110 Ok(PostgresConnectionParams::new(
111 input
112 .host
113 .clone()
114 .into_value(binding_name, "host")
115 .context(invalid("host"))?,
116 input
117 .port
118 .clone()
119 .into_value(binding_name, "port")
120 .context(invalid("port"))?,
121 input
122 .database
123 .clone()
124 .into_value(binding_name, "database")
125 .context(invalid("database"))?,
126 input
127 .username
128 .clone()
129 .into_value(binding_name, "username")
130 .context(invalid("username"))?,
131 input.password.to_string(),
132 input.tls,
133 ))
134}
135
136#[cfg(feature = "gcp")]
138pub(crate) fn resolve_verify_ca_policy(
139 binding_name: &str,
140 certificates: &BindingValue<Vec<String>>,
141) -> Result<PostgresTlsPolicy> {
142 let certificates = certificates
143 .clone()
144 .into_value(binding_name, "serverCaCertificates")
145 .context(ErrorData::config_invalid(
146 binding_name,
147 "Failed to extract 'serverCaCertificates' from Postgres binding",
148 ))?;
149 verified_tls_policy(
150 binding_name,
151 SslMode::VerifyCa,
152 PostgresTlsPolicy::verify_ca(certificates),
153 )
154}
155
156#[cfg(feature = "gcp")]
157fn verified_tls_policy(
158 binding_name: &str,
159 sslmode: SslMode,
160 policy: std::result::Result<PostgresTlsPolicy, crate::traits::InvalidPostgresCaCertificates>,
161) -> Result<PostgresTlsPolicy> {
162 policy.map_err(|_| {
163 AlienError::new(ErrorData::config_invalid(
164 binding_name,
165 format!(
166 "Postgres sslmode '{}' requires one or more non-empty PEM server CA certificates",
167 sslmode.as_str()
168 ),
169 ))
170 })
171}
172
173#[cfg(feature = "aws")]
176pub(crate) fn aws_rds_tls_policy() -> PostgresTlsPolicy {
177 static POLICY: OnceLock<PostgresTlsPolicy> = OnceLock::new();
178 POLICY
179 .get_or_init(|| {
180 PostgresTlsPolicy::verify_full(
181 AWS_RDS_CA_CERTIFICATES
182 .iter()
183 .map(|certificate| (*certificate).to_string())
184 .collect(),
185 )
186 .expect("the embedded AWS RDS root bundle must contain valid PEM certificates")
187 })
188 .clone()
189}
190
191#[cfg(feature = "azure")]
194pub(crate) fn azure_postgres_tls_policy() -> PostgresTlsPolicy {
195 static POLICY: OnceLock<PostgresTlsPolicy> = OnceLock::new();
196 POLICY
197 .get_or_init(|| {
198 PostgresTlsPolicy::verify_full(
199 AZURE_POSTGRES_CA_CERTIFICATES
200 .iter()
201 .map(|certificate| (*certificate).to_string())
202 .collect(),
203 )
204 .expect("the embedded Azure Postgres roots must contain valid PEM certificates")
205 })
206 .clone()
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
216 fn unresolved_secret_ref_field_is_binding_config_invalid() {
217 let error = resolve_params(
218 "db",
219 PostgresConnectionInput {
220 host: &BindingValue::SecretRef {
221 secret_ref: alien_core::bindings::SecretReference {
222 name: "pg-credentials".to_string(),
223 key: "host".to_string(),
224 },
225 },
226 port: &BindingValue::value(5432),
227 database: &"db".into(),
228 username: &"alien".into(),
229 password: "pw",
230 tls: PostgresTlsPolicy::verify_full(vec![pem("root")]).unwrap(),
231 },
232 )
233 .expect_err("an unresolved SecretRef host must not resolve");
234
235 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
236 assert!(!error.retryable, "bad binding config is user-fixable");
237 assert!(
238 error.to_string().contains("host"),
239 "the error must name the offending field, got: {error}"
240 );
241 }
242
243 #[test]
247 fn debug_output_never_contains_the_password() {
248 let params = resolve_params(
249 "db",
250 PostgresConnectionInput {
251 host: &"h".into(),
252 port: &BindingValue::value(5432),
253 database: &"db".into(),
254 username: &"alien".into(),
255 password: "super-secret-password",
256 tls: PostgresTlsPolicy::verify_full(vec![pem("root")]).unwrap(),
257 },
258 )
259 .expect("concrete fields resolve");
260
261 let rendered = format!("{params:?}");
262 assert!(
263 !rendered.contains("super-secret-password"),
264 "password leaked into Debug output: {rendered}"
265 );
266 assert!(
267 !rendered.contains("BEGIN CERTIFICATE"),
268 "certificate bundle expanded into Debug output: {rendered}"
269 );
270 assert!(rendered.contains("<redacted>"), "got: {rendered}");
271 }
272
273 #[test]
274 fn verify_ca_requires_valid_ca_certificates() {
275 for ca_certificates in [
276 Vec::new(),
277 vec!["".to_string()],
278 vec!["not a certificate".to_string()],
279 ] {
280 let error = verified_tls_policy(
281 "db",
282 SslMode::VerifyCa,
283 PostgresTlsPolicy::verify_ca(ca_certificates),
284 )
285 .expect_err("verified TLS without a PEM root must fail closed");
286
287 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
288 assert!(!error.retryable);
289 }
290 }
291
292 #[test]
293 fn verify_full_can_use_the_system_trust_store() {
294 let params = resolve_params(
295 "db",
296 PostgresConnectionInput {
297 host: &"db.example.com".into(),
298 port: &BindingValue::value(5432),
299 database: &"db".into(),
300 username: &"alien".into(),
301 password: "pw",
302 tls: PostgresTlsPolicy::verify_full_with_system_roots(),
303 },
304 )
305 .expect("BYO verify-full can rely on the runtime trust store");
306
307 assert!(params.ca_certificates().is_empty());
308 assert_eq!(params.sslmode(), SslMode::VerifyFull);
309 }
310
311 #[test]
312 fn tls_policy_cannot_pair_plaintext_with_roots_or_verify_ca_without_roots() {
313 assert!(PostgresTlsPolicy::verify_ca(Vec::new()).is_err());
314 assert!(PostgresTlsPolicy::disabled().ca_certificates().is_empty());
315 assert_eq!(PostgresTlsPolicy::disabled().sslmode(), SslMode::Disable);
316 }
317
318 fn pem(body: &str) -> String {
319 format!("-----BEGIN CERTIFICATE-----\n{body}\n-----END CERTIFICATE-----")
320 }
321}