mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +08:00 
			
		
		
		
	refactor(drainer, router): KMS decrypt database password when kms feature is enabled (#733)
				
					
				
			This commit is contained in:
		
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -1527,6 +1527,7 @@ dependencies = [ | ||||
|  "config", | ||||
|  "diesel", | ||||
|  "error-stack", | ||||
|  "external_services", | ||||
|  "once_cell", | ||||
|  "redis_interface", | ||||
|  "router_env", | ||||
|  | ||||
| @ -20,22 +20,24 @@ request_body_limit = 16_384 | ||||
| # Main SQL data store credentials | ||||
| [master_database] | ||||
| username = "db_user"        # DB Username | ||||
| password = "db_pass"      # DB Password | ||||
| 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 | ||||
| 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] | ||||
|  | ||||
| @ -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,6 +27,7 @@ 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"] } | ||||
|  | ||||
| @ -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::<PgConnection>::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 | ||||
|  | ||||
| @ -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(), | ||||
|  | ||||
| @ -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,8 +99,32 @@ 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(), | ||||
|                 )) | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl DrainerSettings { | ||||
|  | ||||
| @ -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(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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)] | ||||
|  | ||||
| @ -61,24 +61,36 @@ 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(), | ||||
|             )) | ||||
|         })?; | ||||
|  | ||||
|         #[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(), || { | ||||
|         #[cfg(feature = "kms")] | ||||
|         { | ||||
|             when(self.kms_encrypted_password.is_default_or_empty(), || { | ||||
|                 Err(ApplicationError::InvalidConfigurationValueError( | ||||
|                 "database name must not be empty".into(), | ||||
|                     "database KMS encrypted password must not be empty".into(), | ||||
|                 )) | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl super::settings::SupportedConnectors { | ||||
|  | ||||
| @ -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::<PgConnection>::new(database_url); | ||||
|     let mut pool = bb8::Pool::builder() | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Sanchith Hegde
					Sanchith Hegde