use crate::error::{ErrorData, Result};
use crate::providers::postgres::{
aws_rds_tls_policy, cloud::resolve_secret_locator, resolve_params, PostgresConnectionInput,
};
use crate::traits::PostgresConnectionParams;
use alien_aws_clients::secrets_manager::{GetSecretValueRequest, SecretsManagerApi};
use alien_core::bindings::AuroraPostgresBinding;
use alien_error::{AlienError, Context};
use std::sync::Arc;
pub(crate) async fn resolve(
binding_name: &str,
binding: &AuroraPostgresBinding,
secrets: Arc<dyn SecretsManagerApi>,
) -> Result<PostgresConnectionParams> {
let secret_arn = resolve_secret_locator(
binding_name,
"passwordSecretArn",
&binding.password_secret_arn,
)?;
let password = read_password(binding_name, &secret_arn, secrets.as_ref()).await?;
resolve_params(
binding_name,
PostgresConnectionInput {
host: &binding.cluster_endpoint,
port: &binding.port,
database: &binding.database,
username: &binding.username,
password: &password,
tls: aws_rds_tls_policy(),
},
)
}
async fn read_password(
binding_name: &str,
secret_arn: &str,
secrets: &dyn SecretsManagerApi,
) -> Result<String> {
let read_failed = |reason: &str| ErrorData::PostgresSecretResolutionFailed {
binding_name: binding_name.to_string(),
secret: secret_arn.to_string(),
reason: reason.to_string(),
};
let invalid_value = |reason: &str| ErrorData::PostgresSecretValueInvalid {
binding_name: binding_name.to_string(),
secret: secret_arn.to_string(),
reason: reason.to_string(),
};
let response = secrets
.get_secret_value(
GetSecretValueRequest::builder()
.secret_id(secret_arn.to_string())
.build(),
)
.await
.context(read_failed("Secrets Manager GetSecretValue failed"))?;
response
.secret_string
.filter(|password| !password.is_empty())
.ok_or_else(|| AlienError::new(invalid_value("SecretString is missing or empty")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::SslMode;
use alien_aws_clients::secrets_manager::{GetSecretValueResponse, MockSecretsManagerApi};
use alien_core::bindings::BindingValue;
const SECRET_ARN: &str = "arn:aws:secretsmanager:us-east-1:000000000000:secret:pg-AbCdEf";
fn binding() -> AuroraPostgresBinding {
AuroraPostgresBinding {
cluster_endpoint: "cluster.cluster-abc.us-east-1.rds.amazonaws.com".into(),
port: BindingValue::value(5432),
database: "app".into(),
username: "alien".into(),
password_secret_arn: SECRET_ARN.into(),
}
}
fn response(secret_string: Option<&str>) -> GetSecretValueResponse {
GetSecretValueResponse {
arn: Some(SECRET_ARN.to_string()),
name: Some("pg".to_string()),
version_id: None,
secret_binary: None,
secret_string: secret_string.map(str::to_string),
version_stages: None,
created_date: None,
}
}
#[tokio::test]
async fn resolves_cluster_endpoint_with_verified_tls() {
let mut secrets = MockSecretsManagerApi::new();
secrets
.expect_get_secret_value()
.times(1)
.withf(|request| {
request.secret_id == SECRET_ARN
&& request.version_id.is_none()
&& request.version_stage.is_none()
})
.returning(|_| Ok(response(Some("a!b*c'd(e)f@/"))));
let params = resolve("db", &binding(), Arc::new(secrets))
.await
.expect("aurora binding resolves");
assert_eq!(
params.host, "cluster.cluster-abc.us-east-1.rds.amazonaws.com",
"Aurora dials the cluster writer endpoint, not a host field"
);
assert_eq!(params.port, 5432);
assert_eq!(params.database, "app");
assert_eq!(params.username, "alien");
assert_eq!(params.password, "a!b*c'd(e)f@/");
assert_eq!(params.sslmode(), SslMode::VerifyFull);
assert_eq!(params.ca_certificates().len(), 1);
assert_eq!(
params.ca_certificates()[0]
.matches("-----BEGIN CERTIFICATE-----")
.count(),
108,
"the embedded official global set must contain every validated regional root"
);
assert_eq!(
params.connection_string(),
"postgres://alien:a%21b%2Ac%27d%28e%29f%40%2F@\
cluster.cluster-abc.us-east-1.rds.amazonaws.com:5432/app?sslmode=verify-full"
);
}
#[tokio::test]
async fn failed_secret_read_is_retryable() {
let mut secrets = MockSecretsManagerApi::new();
secrets.expect_get_secret_value().times(1).returning(|_| {
Err(AlienError::new(
alien_client_core::ErrorData::RateLimitExceeded {
message: "GetSecretValue throttled".to_string(),
},
))
});
let error = resolve("db", &binding(), Arc::new(secrets))
.await
.expect_err("a failed secret read must not resolve a connection");
assert_eq!(error.code, "POSTGRES_SECRET_RESOLUTION_FAILED");
assert!(error.retryable, "an upstream read failure is retryable");
assert!(
error.to_string().contains(SECRET_ARN),
"the error must name the secret locator so operators can find it, got: {error}"
);
}
#[tokio::test]
async fn permanent_secret_read_preserves_source_metadata() {
let mut secrets = MockSecretsManagerApi::new();
secrets.expect_get_secret_value().times(1).returning(|_| {
Err(AlienError::new(
alien_client_core::ErrorData::SerializationError {
message: "Secrets Manager returned malformed JSON".to_string(),
},
))
});
let error = resolve("db", &binding(), Arc::new(secrets))
.await
.expect_err("a malformed provider response must not resolve a connection");
assert_eq!(error.code, "POSTGRES_SECRET_RESOLUTION_FAILED");
assert!(!error.retryable, "the provider error is permanent");
assert!(error.internal, "the provider error is internal");
}
#[tokio::test]
async fn empty_secret_string_fails_resolution() {
for stored in [None, Some("")] {
let mut secrets = MockSecretsManagerApi::new();
secrets
.expect_get_secret_value()
.times(1)
.returning(move |_| Ok(response(stored)));
let error = resolve("db", &binding(), Arc::new(secrets))
.await
.expect_err("an empty secret must not resolve a connection");
assert_eq!(error.code, "POSTGRES_SECRET_VALUE_INVALID");
assert!(!error.retryable);
assert!(!error.internal);
}
}
#[tokio::test]
async fn unresolved_secret_arn_is_non_retryable_config_error() {
let mut secrets = MockSecretsManagerApi::new();
secrets.expect_get_secret_value().never();
let mut malformed = binding();
malformed.password_secret_arn = BindingValue::Expression(serde_json::json!({
"Fn::GetAtt": ["PgSecret", "Id"]
}));
let error = resolve("db", &malformed, Arc::new(secrets))
.await
.expect_err("an unresolved secret ARN must not resolve a connection");
assert_eq!(error.code, "BINDING_CONFIG_INVALID");
assert!(!error.retryable, "bad binding config is user-fixable");
assert!(
error.to_string().contains("ALIEN_DB_BINDING"),
"the error must name the env var the user would edit, got: {error}"
);
}
}