diff --git a/Cargo.lock b/Cargo.lock index 3d84539f0d..4c53c4a934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,6 +1527,7 @@ dependencies = [ "config", "diesel", "error-stack", + "external_services", "once_cell", "redis_interface", "router_env", diff --git a/config/config.example.toml b/config/config.example.toml index 4cd4e75bc9..56d4500cba 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -19,23 +19,25 @@ request_body_limit = 16_384 # Main SQL data store credentials [master_database] -username = "db_user" # DB Username -password = "db_pass" # DB Password -host = "localhost" # DB Host -port = 5432 # DB Port -dbname = "hyperswitch_db" # Name of Database -pool_size = 5 # Number of connections to keep open -connection_timeout = 10 # Timeout for database connection in seconds +username = "db_user" # DB Username +password = "db_pass" # DB Password. Only applicable when KMS is disabled. +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +kms_encrypted_password = "" # Base64-encoded (KMS encrypted) ciphertext of the database password. Only applicable when KMS is enabled. # Replica SQL data store credentials [replica_database] -username = "replica_user" # DB Username -password = "replica_pass" # DB Password -host = "localhost" # DB Host -port = 5432 # DB Port -dbname = "hyperswitch_db" # Name of Database -pool_size = 5 # Number of connections to keep open -connection_timeout = 10 # Timeout for database connection in seconds +username = "replica_user" # DB Username +password = "replica_pass" # DB Password. Only applicable when KMS is disabled. +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +kms_encrypted_password = "" # Base64-encoded (KMS encrypted) ciphertext of the database password. Only applicable when KMS is enabled. # Redis credentials [redis] diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 387c0b796a..d67108ce84 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -8,7 +8,8 @@ readme = "README.md" license = "Apache-2.0" [features] -vergen = [ "router_env/vergen" ] +kms = ["external_services/kms"] +vergen = ["router_env/vergen"] [dependencies] async-bb8-diesel = { git = "https://github.com/juspay/async-bb8-diesel", rev = "9a71d142726dbc33f41c1fd935ddaa79841c7be5" } @@ -26,9 +27,10 @@ tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } # First Party Crates common_utils = { version = "0.1.0", path = "../common_utils" } +external_services = { version = "0.1.0", path = "../external_services" } redis_interface = { version = "0.1.0", path = "../redis_interface" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } storage_models = { version = "0.1.0", path = "../storage_models", features = ["kv_store"] } [build-dependencies] -router_env = { version = "0.1.0", path = "../router_env", default-features = false } \ No newline at end of file +router_env = { version = "0.1.0", path = "../router_env", default-features = false } diff --git a/crates/drainer/src/connection.rs b/crates/drainer/src/connection.rs index 0f05a2d86f..d4902c8f42 100644 --- a/crates/drainer/src/connection.rs +++ b/crates/drainer/src/connection.rs @@ -1,5 +1,7 @@ use bb8::PooledConnection; use diesel::PgConnection; +#[cfg(feature = "kms")] +use external_services::kms; use crate::settings::Database; @@ -15,13 +17,29 @@ pub async fn redis_connection( } #[allow(clippy::expect_used)] -pub async fn diesel_make_pg_pool(database: &Database, _test_transaction: bool) -> PgPool { +pub async fn diesel_make_pg_pool( + database: &Database, + _test_transaction: bool, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> PgPool { + #[cfg(feature = "kms")] + let password = kms::get_kms_client(kms_config) + .await + .decrypt(&database.kms_encrypted_password) + .await + .expect("Failed to KMS decrypt database password"); + + #[cfg(not(feature = "kms"))] + let password = &database.password; + let database_url = format!( "postgres://{}:{}@{}:{}/{}", - database.username, database.password, database.host, database.port, database.dbname + database.username, password, database.host, database.port, database.dbname ); let manager = async_bb8_diesel::ConnectionManager::::new(database_url); - let pool = bb8::Pool::builder().max_size(database.pool_size); + let pool = bb8::Pool::builder() + .max_size(database.pool_size) + .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)); pool.build(manager) .await diff --git a/crates/drainer/src/services.rs b/crates/drainer/src/services.rs index a9cbf12378..5d72d83621 100644 --- a/crates/drainer/src/services.rs +++ b/crates/drainer/src/services.rs @@ -18,7 +18,13 @@ pub struct StoreConfig { impl Store { pub async fn new(config: &crate::settings::Settings, test_transaction: bool) -> Self { Self { - master_pool: diesel_make_pg_pool(&config.master_database, test_transaction).await, + master_pool: diesel_make_pg_pool( + &config.master_database, + test_transaction, + #[cfg(feature = "kms")] + &config.kms, + ) + .await, redis_conn: Arc::new(crate::connection::redis_connection(config).await), config: StoreConfig { drainer_stream_name: config.drainer.stream_name.clone(), diff --git a/crates/drainer/src/settings.rs b/crates/drainer/src/settings.rs index 5367d0dd59..d78f495a19 100644 --- a/crates/drainer/src/settings.rs +++ b/crates/drainer/src/settings.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; +#[cfg(feature = "kms")] +use external_services::kms; use redis_interface as redis; pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; use router_env::{env, logger}; @@ -25,17 +27,23 @@ pub struct Settings { pub redis: redis::RedisSettings, pub log: Log, pub drainer: DrainerSettings, + #[cfg(feature = "kms")] + pub kms: kms::KmsConfig, } #[derive(Debug, Deserialize, Clone)] #[serde(default)] pub struct Database { pub username: String, + #[cfg(not(feature = "kms"))] pub password: String, pub host: String, pub port: u16, pub dbname: String, pub pool_size: u32, + pub connection_timeout: u64, + #[cfg(feature = "kms")] + pub kms_encrypted_password: String, } #[derive(Debug, Clone, Deserialize)] @@ -51,12 +59,16 @@ pub struct DrainerSettings { impl Default for Database { fn default() -> Self { Self { - username: String::default(), - password: String::default(), + username: String::new(), + #[cfg(not(feature = "kms"))] + password: String::new(), host: "localhost".into(), port: 5432, - dbname: String::default(), + dbname: String::new(), pool_size: 5, + connection_timeout: 10, + #[cfg(feature = "kms")] + kms_encrypted_password: String::new(), } } } @@ -77,18 +89,6 @@ impl Database { fn validate(&self) -> Result<(), errors::DrainerError> { use common_utils::fp_utils::when; - when(self.username.is_default_or_empty(), || { - Err(errors::DrainerError::ConfigParsingError( - "database username must not be empty".into(), - )) - })?; - - when(self.password.is_default_or_empty(), || { - Err(errors::DrainerError::ConfigParsingError( - "database user password must not be empty".into(), - )) - })?; - when(self.host.is_default_or_empty(), || { Err(errors::DrainerError::ConfigParsingError( "database host must not be empty".into(), @@ -99,7 +99,31 @@ impl Database { Err(errors::DrainerError::ConfigParsingError( "database name must not be empty".into(), )) - }) + })?; + + when(self.username.is_default_or_empty(), || { + Err(errors::DrainerError::ConfigParsingError( + "database user username must not be empty".into(), + )) + })?; + + #[cfg(not(feature = "kms"))] + { + when(self.password.is_default_or_empty(), || { + Err(errors::DrainerError::ConfigParsingError( + "database user password must not be empty".into(), + )) + }) + } + + #[cfg(feature = "kms")] + { + when(self.kms_encrypted_password.is_default_or_empty(), || { + Err(errors::DrainerError::ConfigParsingError( + "database KMS encrypted password must not be empty".into(), + )) + }) + } } } diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 465ceb4ef0..4b48d4292e 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -15,12 +15,15 @@ impl Default for super::settings::Database { fn default() -> Self { Self { username: String::new(), + #[cfg(not(feature = "kms"))] password: String::new(), host: "localhost".into(), port: 5432, dbname: String::new(), pool_size: 5, connection_timeout: 10, + #[cfg(feature = "kms")] + kms_encrypted_password: String::new(), } } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 883583a08b..1a20db82ff 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -223,12 +223,15 @@ pub struct Server { #[serde(default)] pub struct Database { pub username: String, + #[cfg(not(feature = "kms"))] pub password: String, pub host: String, pub port: u16, pub dbname: String, pub pool_size: u32, pub connection_timeout: u64, + #[cfg(feature = "kms")] + pub kms_encrypted_password: String, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 4cf28b3dce..d74ffe4b9b 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -61,23 +61,35 @@ impl super::settings::Database { )) })?; + when(self.dbname.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "database name must not be empty".into(), + )) + })?; + when(self.username.is_default_or_empty(), || { Err(ApplicationError::InvalidConfigurationValueError( "database user username must not be empty".into(), )) })?; - when(self.password.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "database user password must not be empty".into(), - )) - })?; + #[cfg(not(feature = "kms"))] + { + when(self.password.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "database user password must not be empty".into(), + )) + }) + } - when(self.dbname.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "database name must not be empty".into(), - )) - }) + #[cfg(feature = "kms")] + { + when(self.kms_encrypted_password.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "database KMS encrypted password must not be empty".into(), + )) + }) + } } } diff --git a/crates/router/src/connection.rs b/crates/router/src/connection.rs index f692d44691..a84039a903 100644 --- a/crates/router/src/connection.rs +++ b/crates/router/src/connection.rs @@ -2,6 +2,8 @@ use async_bb8_diesel::{AsyncConnection, ConnectionError}; use bb8::{CustomizeConnection, PooledConnection}; use diesel::PgConnection; use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; use crate::{configs::settings::Database, errors}; @@ -37,10 +39,24 @@ pub async fn redis_connection( } #[allow(clippy::expect_used)] -pub async fn diesel_make_pg_pool(database: &Database, test_transaction: bool) -> PgPool { +pub async fn diesel_make_pg_pool( + database: &Database, + test_transaction: bool, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> PgPool { + #[cfg(feature = "kms")] + let password = kms::get_kms_client(kms_config) + .await + .decrypt(&database.kms_encrypted_password) + .await + .expect("Failed to KMS decrypt database password"); + + #[cfg(not(feature = "kms"))] + let password = &database.password; + let database_url = format!( "postgres://{}:{}@{}:{}/{}", - database.username, database.password, database.host, database.port, database.dbname + database.username, password, database.host, database.port, database.dbname ); let manager = async_bb8_diesel::ConnectionManager::::new(database_url); let mut pool = bb8::Pool::builder() diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index a9021a3437..74c4a71c6d 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -109,9 +109,21 @@ impl Store { }); Self { - master_pool: diesel_make_pg_pool(&config.master_database, test_transaction).await, + master_pool: diesel_make_pg_pool( + &config.master_database, + test_transaction, + #[cfg(feature = "kms")] + &config.kms, + ) + .await, #[cfg(feature = "olap")] - replica_pool: diesel_make_pg_pool(&config.replica_database, test_transaction).await, + replica_pool: diesel_make_pg_pool( + &config.replica_database, + test_transaction, + #[cfg(feature = "kms")] + &config.kms, + ) + .await, redis_conn, #[cfg(feature = "kv_store")] config: StoreConfig {