diff --git a/.typos.toml b/.typos.toml index 4ce2152660..40acb13058 100644 --- a/.typos.toml +++ b/.typos.toml @@ -36,6 +36,7 @@ ba = "ba" # ignore minor commit conversions ede = "ede" # ignore minor commit conversions daa = "daa" # Commit id afe = "afe" # Commit id +Hashi = "Hashi" # HashiCorp [files] extend-exclude = [ diff --git a/Cargo.lock b/Cargo.lock index 2cd3cd65c3..5623fd9f72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,7 +114,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d" dependencies = [ - "darling", + "darling 0.20.3", "parse-size", "proc-macro2", "quote", @@ -189,10 +189,10 @@ dependencies = [ "rustls 0.21.7", "rustls-webpki", "tokio 1.35.1", - "tokio-rustls", + "tokio-rustls 0.23.4", "tokio-util", "tracing", - "webpki-roots", + "webpki-roots 0.22.6", ] [[package]] @@ -954,7 +954,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls", + "hyper-rustls 0.23.2", "lazy_static", "pin-project-lite", "rustls 0.20.9", @@ -1990,14 +1990,38 @@ dependencies = [ "thiserror", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + [[package]] name = "darling" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.3", + "darling_macro 0.20.3", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", ] [[package]] @@ -2014,13 +2038,24 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ - "darling_core", + "darling_core 0.20.3", "quote", "syn 2.0.48", ] @@ -2104,6 +2139,37 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "derive_deref" version = "1.1.1" @@ -2421,6 +2487,7 @@ dependencies = [ "common_utils", "dyn-clone", "error-stack", + "hex", "hyper", "hyper-proxy", "masking", @@ -2429,6 +2496,7 @@ dependencies = [ "serde", "thiserror", "tokio 1.35.1", + "vaultrs", ] [[package]] @@ -2453,7 +2521,7 @@ dependencies = [ "futures-util", "http", "hyper", - "hyper-rustls", + "hyper-rustls 0.23.2", "mime", "serde", "serde_json", @@ -3098,7 +3166,21 @@ dependencies = [ "rustls 0.20.9", "rustls-native-certs", "tokio 1.35.1", - "tokio-rustls", + "tokio-rustls 0.23.4", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls 0.21.7", + "tokio 1.35.1", + "tokio-rustls 0.24.1", ] [[package]] @@ -4945,6 +5027,7 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-rustls 0.24.2", "hyper-tls", "ipnet", "js-sys", @@ -4955,18 +5038,22 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.7", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "system-configuration", "tokio 1.35.1", "tokio-native-tls", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 0.25.3", "winreg", ] @@ -5302,6 +5389,40 @@ dependencies = [ "nom", ] +[[package]] +name = "rustify" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c02e25271068de581e03ac3bb44db60165ff1a10d92b9530192ccb898bc706" +dependencies = [ + "anyhow", + "async-trait", + "bytes 1.5.0", + "http", + "reqwest", + "rustify_derive", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "rustify_derive" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58135536c18c04f4634bedad182a3f41baf33ef811cc38a3ec7b7061c57134c8" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "serde_urlencoded", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "rustix" version = "0.38.28" @@ -5670,7 +5791,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" dependencies = [ - "darling", + "darling 0.20.3", "proc-macro2", "quote", "syn 2.0.48", @@ -6561,6 +6682,16 @@ dependencies = [ "webpki", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.7", + "tokio 1.35.1", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -6794,11 +6925,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-attributes", @@ -6833,20 +6963,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -7155,6 +7285,26 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vaultrs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28084ac780b443e7f3514df984a2933bd3ab39e71914d951cdf8e4d298a7c9bc" +dependencies = [ + "async-trait", + "bytes 1.5.0", + "derive_builder", + "http", + "reqwest", + "rustify", + "rustify_derive", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -7346,6 +7496,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "weezl" version = "0.1.7" diff --git a/Makefile b/Makefile index 780d5a993c..a39fc4c226 100644 --- a/Makefile +++ b/Makefile @@ -34,11 +34,18 @@ ROOT_DIR := $(realpath $(ROOT_DIR_WITH_SLASH)) release +# Check a local package and all of its dependencies for errors +# +# Usage : +# make check +check: + cargo check + + # Compile application for running on local machine # # Usage : # make build - build : cargo build diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index f26c31f0e7..67169a1510 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] release = ["kms", "vergen"] kms = ["external_services/kms"] +hashicorp-vault = ["external_services/hashicorp-vault"] vergen = ["router_env/vergen"] [dependencies] diff --git a/crates/drainer/src/connection.rs b/crates/drainer/src/connection.rs index 7b273244cb..6af0a97822 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 = "hashicorp-vault")] +use external_services::hashicorp_vault::{self, decrypt::VaultFetch, Kv2}; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; #[cfg(not(feature = "kms"))] @@ -27,16 +29,23 @@ pub async fn diesel_make_pg_pool( database: &Database, _test_transaction: bool, #[cfg(feature = "kms")] kms_client: &'static kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] hashicorp_client: &'static hashicorp_vault::HashiCorpVault, ) -> PgPool { + let password = database.password.clone(); + #[cfg(feature = "hashicorp-vault")] + let password = password + .fetch_inner::(hashicorp_client) + .await + .expect("Failed while fetching db password"); + #[cfg(feature = "kms")] - let password = database - .password + let password = password .decrypt_inner(kms_client) .await .expect("Failed to decrypt password"); #[cfg(not(feature = "kms"))] - let password = &database.password.peek(); + let password = &password.peek(); let database_url = format!( "postgres://{}:{}@{}:{}/{}", diff --git a/crates/drainer/src/services.rs b/crates/drainer/src/services.rs index 481fcc0722..4393ebb9dc 100644 --- a/crates/drainer/src/services.rs +++ b/crates/drainer/src/services.rs @@ -17,6 +17,11 @@ pub struct StoreConfig { } impl Store { + /// # Panics + /// + /// Panics if there is a failure while obtaining the HashiCorp client using the provided configuration. + /// This panic indicates a critical failure in setting up external services, and the application cannot proceed without a valid HashiCorp client. + /// pub async fn new(config: &crate::settings::Settings, test_transaction: bool) -> Self { Self { master_pool: diesel_make_pg_pool( @@ -24,6 +29,11 @@ impl Store { test_transaction, #[cfg(feature = "kms")] external_services::kms::get_kms_client(&config.kms).await, + #[cfg(feature = "hashicorp-vault")] + #[allow(clippy::expect_used)] + external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + .await + .expect("Failed while getting hashicorp client"), ) .await, redis_conn: Arc::new(crate::connection::redis_connection(config).await), diff --git a/crates/drainer/src/settings.rs b/crates/drainer/src/settings.rs index 8101abf502..5b80ee375f 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 = "hashicorp-vault")] +use external_services::hashicorp_vault; #[cfg(feature = "kms")] use external_services::kms; use redis_interface as redis; @@ -34,6 +36,8 @@ pub struct Settings { pub drainer: DrainerSettings, #[cfg(feature = "kms")] pub kms: kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + pub hc_vault: hashicorp_vault::HashiCorpVaultConfig, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 90e5df5380..6552b57b0e 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] kms = ["dep:aws-config", "dep:aws-sdk-kms"] email = ["dep:aws-config"] +hashicorp-vault = [ "dep:vaultrs" ] [dependencies] async-trait = "0.1.68" @@ -27,6 +28,8 @@ thiserror = "1.0.40" tokio = "1.35.1" hyper-proxy = "0.9.1" hyper = "0.14.26" +vaultrs = { version = "0.7.0", optional = true } +hex = "0.4.3" # First party crates common_utils = { version = "0.1.0", path = "../common_utils" } diff --git a/crates/external_services/src/hashicorp_vault.rs b/crates/external_services/src/hashicorp_vault.rs new file mode 100644 index 0000000000..e31c8f0139 --- /dev/null +++ b/crates/external_services/src/hashicorp_vault.rs @@ -0,0 +1,215 @@ +//! Interactions with the HashiCorp Vault + +use std::{collections::HashMap, future::Future, pin::Pin}; + +use error_stack::{Report, ResultExt}; +use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; + +/// Utilities for supporting decryption of data +pub mod decrypt; + +static HC_CLIENT: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); + +#[allow(missing_debug_implementations)] +/// A struct representing a connection to HashiCorp Vault. +pub struct HashiCorpVault { + /// The underlying client used for interacting with HashiCorp Vault. + client: VaultClient, +} + +/// Configuration for connecting to HashiCorp Vault. +#[derive(Clone, Debug, Default, serde::Deserialize)] +#[serde(default)] +pub struct HashiCorpVaultConfig { + /// The URL of the HashiCorp Vault server. + pub url: String, + /// The authentication token used to access HashiCorp Vault. + pub token: String, +} + +/// Asynchronously retrieves a HashiCorp Vault client based on the provided configuration. +/// +/// # Parameters +/// +/// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. +pub async fn get_hashicorp_client( + config: &HashiCorpVaultConfig, +) -> error_stack::Result<&'static HashiCorpVault, HashiCorpError> { + HC_CLIENT + .get_or_try_init(|| async { HashiCorpVault::new(config) }) + .await +} + +/// A trait defining an engine for interacting with HashiCorp Vault. +pub trait Engine: Sized { + /// The associated type representing the return type of the engine's operations. + type ReturnType<'b, T> + where + T: 'b, + Self: 'b; + /// Reads data from HashiCorp Vault at the specified location. + /// + /// # Parameters + /// + /// - `client`: A reference to the HashiCorpVault client. + /// - `location`: The location in HashiCorp Vault to read data from. + /// + /// # Returns + /// + /// A future representing the result of the read operation. + fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String>; +} + +/// An implementation of the `Engine` trait for the Key-Value version 2 (Kv2) engine. +#[derive(Debug)] +pub enum Kv2 {} + +impl Engine for Kv2 { + type ReturnType<'b, T: 'b> = + Pin> + Send + 'b>>; + fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String> { + Box::pin(async move { + let mut split = location.split(':'); + let mount = split.next().ok_or(HashiCorpError::IncompleteData)?; + let path = split.next().ok_or(HashiCorpError::IncompleteData)?; + let key = split.next().unwrap_or("value"); + + let mut output = + vaultrs::kv2::read::>(&client.client, mount, path) + .await + .map_err(Into::>::into) + .change_context(HashiCorpError::FetchFailed)?; + + Ok(output.remove(key).ok_or(HashiCorpError::ParseError)?) + }) + } +} + +impl HashiCorpVault { + /// Creates a new instance of HashiCorpVault based on the provided configuration. + /// + /// # Parameters + /// + /// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. + /// + pub fn new(config: &HashiCorpVaultConfig) -> error_stack::Result { + VaultClient::new( + VaultClientSettingsBuilder::default() + .address(&config.url) + .token(&config.token) + .build() + .map_err(Into::>::into) + .change_context(HashiCorpError::ClientCreationFailed) + .attach_printable("Failed while building vault settings")?, + ) + .map_err(Into::>::into) + .change_context(HashiCorpError::ClientCreationFailed) + .map(|client| Self { client }) + } + + /// Asynchronously fetches data from HashiCorp Vault using the specified engine. + /// + /// # Parameters + /// + /// - `data`: A String representing the location or identifier of the data in HashiCorp Vault. + /// + /// # Type Parameters + /// + /// - `En`: The engine type that implements the `Engine` trait. + /// - `I`: The type that can be constructed from the retrieved encoded data. + /// + pub async fn fetch(&self, data: String) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a, + I: FromEncoded, + { + let output = En::read(self, data).await?; + I::from_encoded(output).ok_or(error_stack::report!(HashiCorpError::HexDecodingFailed)) + } +} + +/// A trait for types that can be constructed from encoded data in the form of a String. +pub trait FromEncoded: Sized { + /// Constructs an instance of the type from the provided encoded input. + /// + /// # Parameters + /// + /// - `input`: A String containing the encoded data. + /// + /// # Returns + /// + /// An `Option` representing the constructed instance if successful, or `None` otherwise. + /// + /// # Example + /// + /// ```rust + /// # use your_module::{FromEncoded, masking::Secret, Vec}; + /// let secret_instance = Secret::::from_encoded("encoded_secret_string".to_string()); + /// let vec_instance = Vec::::from_encoded("68656c6c6f".to_string()); + /// ``` + fn from_encoded(input: String) -> Option; +} + +impl FromEncoded for masking::Secret { + fn from_encoded(input: String) -> Option { + Some(input.into()) + } +} + +impl FromEncoded for Vec { + fn from_encoded(input: String) -> Option { + hex::decode(input).ok() + } +} + +/// An enumeration representing various errors that can occur in interactions with HashiCorp Vault. +#[derive(Debug, thiserror::Error)] +pub enum HashiCorpError { + /// Failed while creating hashicorp client + #[error("Failed while creating a new client")] + ClientCreationFailed, + + /// Failed while building configurations for hashicorp client + #[error("Failed while building configuration")] + ConfigurationBuildFailed, + + /// Failed while decoding data to hex format + #[error("Failed while decoding hex data")] + HexDecodingFailed, + + /// An error occurred when base64 decoding input data. + #[error("Failed to base64 decode input data")] + Base64DecodingFailed, + + /// An error occurred when KMS decrypting input data. + #[error("Failed to KMS decrypt input data")] + DecryptionFailed, + + /// The KMS decrypted output does not include a plaintext output. + #[error("Missing plaintext KMS decryption output")] + MissingPlaintextDecryptionOutput, + + /// An error occurred UTF-8 decoding KMS decrypted output. + #[error("Failed to UTF-8 decode decryption output")] + Utf8DecodingFailed, + + /// Incomplete data provided to fetch data from hasicorp + #[error("Provided information about the value is incomplete")] + IncompleteData, + + /// Failed while fetching data from vault + #[error("Failed while fetching data from the server")] + FetchFailed, + + /// Failed while parsing received data + #[error("Failed while parsing the response")] + ParseError, +} diff --git a/crates/external_services/src/hashicorp_vault/decrypt.rs b/crates/external_services/src/hashicorp_vault/decrypt.rs new file mode 100644 index 0000000000..1bc1b6ffa1 --- /dev/null +++ b/crates/external_services/src/hashicorp_vault/decrypt.rs @@ -0,0 +1,50 @@ +use std::{future::Future, pin::Pin}; + +use masking::ExposeInterface; + +/// A trait for types that can be asynchronously fetched and decrypted from HashiCorp Vault. +#[async_trait::async_trait] +pub trait VaultFetch: Sized { + /// Asynchronously decrypts the inner content of the type. + /// + /// # Returns + /// + /// An `Result` representing the decrypted instance if successful, + /// or an `super::HashiCorpError` with details about the encountered error. + /// + async fn fetch_inner( + self, + client: &super::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: super::Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a; +} + +#[async_trait::async_trait] +impl VaultFetch for masking::Secret { + async fn fetch_inner( + self, + client: &super::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: super::Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a, + { + client.fetch::(self.expose()).await + } +} diff --git a/crates/external_services/src/kms.rs b/crates/external_services/src/kms.rs index 04a58e4b23..740bca4d82 100644 --- a/crates/external_services/src/kms.rs +++ b/crates/external_services/src/kms.rs @@ -190,6 +190,44 @@ impl KmsConfig { #[serde(transparent)] pub struct KmsValue(Secret); +impl From for KmsValue { + fn from(value: String) -> Self { + Self(Secret::new(value)) + } +} + +impl From> for KmsValue { + fn from(value: Secret) -> Self { + Self(value) + } +} + +#[cfg(feature = "hashicorp-vault")] +#[async_trait::async_trait] +impl super::hashicorp_vault::decrypt::VaultFetch for KmsValue { + async fn fetch_inner( + self, + client: &super::hashicorp_vault::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: super::hashicorp_vault::Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result< + String, + super::hashicorp_vault::HashiCorpError, + >, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.0.fetch_inner::(client).await.map(KmsValue) + } +} + impl common_utils::ext_traits::ConfigExt for KmsValue { fn is_empty_after_trim(&self) -> bool { self.0.peek().is_empty_after_trim() diff --git a/crates/external_services/src/lib.rs b/crates/external_services/src/lib.rs index ccf1db47a3..9bf4916eec 100644 --- a/crates/external_services/src/lib.rs +++ b/crates/external_services/src/lib.rs @@ -9,6 +9,9 @@ pub mod email; #[cfg(feature = "kms")] pub mod kms; +#[cfg(feature = "hashicorp-vault")] +pub mod hashicorp_vault; + /// Crate specific constants #[cfg(feature = "kms")] pub mod consts { diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index acc6b70a2e..ef6ea41d52 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] aws_s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] +hashicorp-vault = ["external_services/hashicorp-vault"] email = ["external_services/email", "dep:aws-config", "olap"] frm = [] stripe = ["dep:serde_qs"] diff --git a/crates/router/src/configs.rs b/crates/router/src/configs.rs index bb8f61646f..5cb1df0644 100644 --- a/crates/router/src/configs.rs +++ b/crates/router/src/configs.rs @@ -1,4 +1,6 @@ mod defaults; +#[cfg(feature = "hashicorp-vault")] +pub mod hc_vault; #[cfg(feature = "kms")] pub mod kms; pub mod settings; diff --git a/crates/router/src/configs/hc_vault.rs b/crates/router/src/configs/hc_vault.rs new file mode 100644 index 0000000000..f20d8e79ed --- /dev/null +++ b/crates/router/src/configs/hc_vault.rs @@ -0,0 +1,134 @@ +use external_services::hashicorp_vault::{ + decrypt::VaultFetch, Engine, HashiCorpError, HashiCorpVault, +}; +use masking::ExposeInterface; + +use crate::configs::settings; + +#[async_trait::async_trait] +impl VaultFetch for settings::Jwekey { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + ( + self.vault_encryption_key, + self.rust_locker_encryption_key, + self.vault_private_key, + self.tunnel_private_key, + ) = ( + masking::Secret::new(self.vault_encryption_key) + .fetch_inner::(client) + .await? + .expose(), + masking::Secret::new(self.rust_locker_encryption_key) + .fetch_inner::(client) + .await? + .expose(), + masking::Secret::new(self.vault_private_key) + .fetch_inner::(client) + .await? + .expose(), + masking::Secret::new(self.tunnel_private_key) + .fetch_inner::(client) + .await? + .expose(), + ); + Ok(self) + } +} + +#[async_trait::async_trait] +impl VaultFetch for settings::Database { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + Ok(Self { + host: self.host, + port: self.port, + dbname: self.dbname, + username: self.username, + password: self.password.fetch_inner::(client).await?, + pool_size: self.pool_size, + connection_timeout: self.connection_timeout, + queue_strategy: self.queue_strategy, + min_idle: self.min_idle, + max_lifetime: self.max_lifetime, + }) + } +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl VaultFetch for settings::PayPalOnboarding { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.client_id = self.client_id.fetch_inner::(client).await?; + self.client_secret = self.client_secret.fetch_inner::(client).await?; + self.partner_id = self.partner_id.fetch_inner::(client).await?; + Ok(self) + } +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl VaultFetch for settings::ConnectorOnboarding { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.paypal = self.paypal.fetch_inner::(client).await?; + Ok(self) + } +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index cb4fdd70eb..3c1d9f7d39 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -11,6 +11,8 @@ use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; #[cfg(feature = "email")] use external_services::email::EmailSettings; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; #[cfg(feature = "kms")] use external_services::kms; use redis_interface::RedisSettings; @@ -88,6 +90,8 @@ pub struct Settings { pub api_keys: ApiKeys, #[cfg(feature = "kms")] pub kms: kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + pub hc_vault: hashicorp_vault::HashiCorpVaultConfig, #[cfg(feature = "aws_s3")] pub file_upload_config: FileUploadConfig, pub tokenization: TokenizationConfig, diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index 78d4e801e8..f28d845609 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -2,8 +2,12 @@ use common_utils::date_time; #[cfg(feature = "email")] use diesel_models::{api_keys::ApiKey, enums as storage_enums}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms; +#[cfg(not(feature = "kms"))] +use masking::ExposeInterface; use masking::{PeekInterface, StrongSecret}; use router_env::{instrument, tracing}; @@ -35,19 +39,37 @@ static HASH_KEY: tokio::sync::OnceCell errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> { HASH_KEY .get_or_try_init(|| async { + let hash_key = { + #[cfg(feature = "kms")] + { + api_key_config.kms_encrypted_hash_key.clone() + } + #[cfg(not(feature = "kms"))] + { + masking::Secret::<_, masking::WithType>::new(api_key_config.hash_key.clone()) + } + }; + + #[cfg(feature = "hashicorp-vault")] + let hash_key = hash_key + .fetch_inner::(hc_client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + #[cfg(feature = "kms")] - let hash_key = api_key_config - .kms_encrypted_hash_key + let hash_key = hash_key .decrypt_inner(kms_client) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to KMS decrypt API key hashing key")?; #[cfg(not(feature = "kms"))] - let hash_key = &api_key_config.hash_key; + let hash_key = hash_key.expose(); <[u8; PlaintextApiKey::HASH_KEY_LEN]>::try_from( hex::decode(hash_key) @@ -132,6 +154,8 @@ impl PlaintextApiKey { pub async fn create_api_key( state: AppState, #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] + hc_client: &external_services::hashicorp_vault::HashiCorpVault, api_key: api::CreateApiKeyRequest, merchant_id: String, ) -> RouterResponse { @@ -153,6 +177,8 @@ pub async fn create_api_key( api_key_config, #[cfg(feature = "kms")] kms_client, + #[cfg(feature = "hashicorp-vault")] + hc_client, ) .await?; let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH); @@ -565,6 +591,10 @@ mod tests { &settings.api_keys, #[cfg(feature = "kms")] external_services::kms::get_kms_client(&settings.kms).await, + #[cfg(feature = "hashicorp-vault")] + external_services::hashicorp_vault::get_hashicorp_client(&settings.hc_vault) + .await + .unwrap(), ) .await .unwrap(); diff --git a/crates/router/src/core/currency.rs b/crates/router/src/core/currency.rs index 1ea9454f00..41699df47a 100644 --- a/crates/router/src/core/currency.rs +++ b/crates/router/src/core/currency.rs @@ -19,6 +19,8 @@ pub async fn retrieve_forex( state.conf.forex_api.local_fetch_retry_count, #[cfg(feature = "kms")] &state.conf.kms, + #[cfg(feature = "hashicorp-vault")] + &state.conf.hc_vault, ) .await .change_context(ApiErrorResponse::GenericNotFoundError { @@ -44,6 +46,8 @@ pub async fn convert_forex( from_currency, #[cfg(feature = "kms")] &state.conf.kms, + #[cfg(feature = "hashicorp-vault")] + &state.conf.hc_vault, )) .await .change_context(ApiErrorResponse::InternalServerError)?, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 10aa00f596..043863a98f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -68,7 +68,7 @@ use crate::{ workflows::payment_sync, }; -#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments, clippy::type_complexity)] #[instrument(skip_all, fields(payment_id, merchant_id))] pub async fn payments_operation_core( state: &AppState, diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index de697e02f7..099c266e04 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -2,8 +2,14 @@ use api_models::payments as payment_types; use async_trait::async_trait; use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, Report, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms; +#[cfg(feature = "hashicorp-vault")] +use masking::ExposeInterface; use super::{ConstructFlowSpecificData, Feature}; use crate::{ @@ -177,10 +183,85 @@ async fn create_applepay_session_token( payment_request_data, session_token_data, } => { + let ( + apple_pay_merchant_cert, + apple_pay_merchant_cert_key, + common_merchant_identifier, + ) = async { + #[cfg(feature = "hashicorp-vault")] + let client = external_services::hashicorp_vault::get_hashicorp_client( + &state.conf.hc_vault, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while building hashicorp client")?; + + #[cfg(feature = "hashicorp-vault")] + { + Ok::<_, Report>(( + masking::Secret::new( + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert + .clone(), + ) + .fetch_inner::(client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + .expose(), + masking::Secret::new( + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert_key + .clone(), + ) + .fetch_inner::(client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + .expose(), + masking::Secret::new( + state + .conf + .applepay_merchant_configs + .common_merchant_identifier + .clone(), + ) + .fetch_inner::(client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + .expose(), + )) + } + + #[cfg(not(feature = "hashicorp-vault"))] + { + Ok::<_, Report>(( + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert + .clone(), + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert_key + .clone(), + state + .conf + .applepay_merchant_configs + .common_merchant_identifier + .clone(), + )) + } + } + .await?; + #[cfg(feature = "kms")] let decrypted_apple_pay_merchant_cert = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_merchant_cert) + .decrypt(apple_pay_merchant_cert) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Apple pay merchant certificate decryption failed")?; @@ -189,7 +270,7 @@ async fn create_applepay_session_token( let decrypted_apple_pay_merchant_cert_key = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_merchant_cert_key) + .decrypt(apple_pay_merchant_cert_key) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( @@ -199,21 +280,13 @@ async fn create_applepay_session_token( #[cfg(feature = "kms")] let decrypted_merchant_identifier = kms::get_kms_client(&state.conf.kms) .await - .decrypt( - &state - .conf - .applepay_merchant_configs - .common_merchant_identifier, - ) + .decrypt(common_merchant_identifier) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Apple pay merchant identifier decryption failed")?; #[cfg(not(feature = "kms"))] - let decrypted_merchant_identifier = &state - .conf - .applepay_merchant_configs - .common_merchant_identifier; + let decrypted_merchant_identifier = common_merchant_identifier; let apple_pay_session_request = get_session_request_for_simplified_apple_pay( decrypted_merchant_identifier.to_string(), @@ -221,12 +294,10 @@ async fn create_applepay_session_token( ); #[cfg(not(feature = "kms"))] - let decrypted_apple_pay_merchant_cert = - &state.conf.applepay_decrypt_keys.apple_pay_merchant_cert; + let decrypted_apple_pay_merchant_cert = apple_pay_merchant_cert; #[cfg(not(feature = "kms"))] - let decrypted_apple_pay_merchant_cert_key = - &state.conf.applepay_decrypt_keys.apple_pay_merchant_cert_key; + let decrypted_apple_pay_merchant_cert_key = apple_pay_merchant_cert_key; ( payment_request_data, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 9d3da6c78e..213adc79fb 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -13,6 +13,10 @@ use data_models::{ use diesel_models::enums; // TODO : Evaluate all the helper functions () use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms; use josekit::jwe; @@ -1250,6 +1254,7 @@ pub async fn get_connector_default( } #[instrument(skip_all)] +#[allow(clippy::type_complexity)] pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>( operation: BoxedOperation<'a, F, R, Ctx>, db: &dyn StorageInterface, @@ -3501,15 +3506,38 @@ impl ApplePayData { &self, state: &AppState, ) -> CustomResult { + let apple_pay_ppc = async { + #[cfg(feature = "hashicorp-vault")] + let client = + external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")?; + + #[cfg(feature = "hashicorp-vault")] + let output = + masking::Secret::new(state.conf.applepay_decrypt_keys.apple_pay_ppc.clone()) + .fetch_inner::(client) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed)? + .expose(); + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.applepay_decrypt_keys.apple_pay_ppc.clone(); + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] let cert_data = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc) + .decrypt(&apple_pay_ppc) .await .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; #[cfg(not(feature = "kms"))] - let cert_data = &state.conf.applepay_decrypt_keys.apple_pay_ppc; + let cert_data = &apple_pay_ppc; let base64_decode_cert_data = BASE64_ENGINE .decode(cert_data) @@ -3561,15 +3589,39 @@ impl ApplePayData { .change_context(errors::ApplePayDecryptionError::KeyDeserializationFailed) .attach_printable("Failed to deserialize the public key")?; + let apple_pay_ppc_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = + external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")?; + + #[cfg(feature = "hashicorp-vault")] + let output = + masking::Secret::new(state.conf.applepay_decrypt_keys.apple_pay_ppc_key.clone()) + .fetch_inner::(client) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")? + .expose(); + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.applepay_decrypt_keys.apple_pay_ppc_key.clone(); + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] let decrypted_apple_pay_ppc_key = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc_key) + .decrypt(&apple_pay_ppc_key) .await .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; #[cfg(not(feature = "kms"))] - let decrypted_apple_pay_ppc_key = &state.conf.applepay_decrypt_keys.apple_pay_ppc_key; + let decrypted_apple_pay_ppc_key = &apple_pay_ppc_key; // Create PKey objects from EcKey let private_key = PKey::private_key_from_pem(decrypted_apple_pay_ppc_key.as_bytes()) .into_report() diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index 0750ff82bf..d805925f31 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -5,6 +5,8 @@ use api_models::{ payment_methods::{self, BankAccountAccessCreds}, payments::{AddressDetails, BankDebitBilling, BankDebitData, PaymentMethodData}, }; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::{self, decrypt::VaultFetch}; use hex; pub mod helpers; pub mod transformers; @@ -345,15 +347,36 @@ async fn store_bank_details_in_payment_methods( } } + let pm_auth_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while creating client")?; + + #[cfg(feature = "hashicorp-vault")] + let output = masking::Secret::new(state.conf.payment_method_auth.pm_auth_key.clone()) + .fetch_inner::(client) + .await + .change_context(ApiErrorResponse::InternalServerError)? + .expose(); + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.payment_method_auth.pm_auth_key.clone(); + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] let pm_auth_key = kms::get_kms_client(&state.conf.kms) .await - .decrypt(state.conf.payment_method_auth.pm_auth_key.clone()) + .decrypt(pm_auth_key) .await .change_context(ApiErrorResponse::InternalServerError)?; #[cfg(not(feature = "kms"))] - let pm_auth_key = state.conf.payment_method_auth.pm_auth_key.clone(); + let pm_auth_key = pm_auth_key; let mut update_entries: Vec<(storage::PaymentMethod, storage::PaymentMethodUpdate)> = Vec::new(); diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 9293d6e114..fb1851af00 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -1,4 +1,6 @@ use actix_web::{web, HttpRequest, Responder}; +#[cfg(feature = "hashicorp-vault")] +use error_stack::ResultExt; use router_env::{instrument, tracing, Flow}; use super::app::AppState; @@ -44,10 +46,20 @@ pub async fn api_key_create( |state, _, payload| async { #[cfg(feature = "kms")] let kms_client = external_services::kms::get_kms_client(&state.clone().conf.kms).await; + + #[cfg(feature = "hashicorp-vault")] + let hc_client = external_services::hashicorp_vault::get_hashicorp_client( + &state.clone().conf.hc_vault, + ) + .await + .change_context(crate::core::errors::ApiErrorResponse::InternalServerError)?; + api_keys::create_api_key( state, #[cfg(feature = "kms")] kms_client, + #[cfg(feature = "hashicorp-vault")] + hc_client, payload, merchant_id.clone(), ) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 4345109a67..d3a43f0f49 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1,10 +1,12 @@ use std::sync::Arc; use actix_web::{web, Scope}; -#[cfg(all(feature = "kms", feature = "olap"))] +#[cfg(all(feature = "olap", any(feature = "hashicorp-vault", feature = "kms")))] use analytics::AnalyticsConfig; #[cfg(feature = "email")] use external_services::email::{ses::AwsSes, EmailService}; +#[cfg(all(feature = "olap", feature = "hashicorp-vault"))] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; #[cfg(all(feature = "olap", feature = "kms"))] @@ -146,6 +148,12 @@ impl AppState { Box::pin(async move { #[cfg(feature = "kms")] let kms_client = kms::get_kms_client(&conf.kms).await; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + let hc_client = + external_services::hashicorp_vault::get_hashicorp_client(&conf.hc_vault) + .await + .expect("Failed while creating hashicorp_client"); let testable = storage_impl == StorageImpl::PostgresqlTest; #[allow(clippy::expect_used)] let event_handler = conf @@ -153,6 +161,7 @@ impl AppState { .get_event_handler() .await .expect("Failed to create event handler"); + let store: Box = match storage_impl { StorageImpl::Postgresql | StorageImpl::PostgresqlTest => match &event_handler { EventsHandler::Kafka(kafka_client) => Box::new( @@ -180,6 +189,22 @@ impl AppState { ), }; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + match conf.analytics { + AnalyticsConfig::Clickhouse { .. } => {} + AnalyticsConfig::Sqlx { ref mut sqlx } + | AnalyticsConfig::CombinedCkh { ref mut sqlx, .. } + | AnalyticsConfig::CombinedSqlx { ref mut sqlx, .. } => { + sqlx.password = sqlx + .password + .clone() + .fetch_inner::(hc_client) + .await + .expect("Failed while fetching from hashicorp vault"); + } + }; + #[cfg(all(feature = "kms", feature = "olap"))] #[allow(clippy::expect_used)] match conf.analytics { @@ -195,6 +220,16 @@ impl AppState { } }; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + { + conf.connector_onboarding = conf + .connector_onboarding + .fetch_inner::(hc_client) + .await + .expect("Failed to decrypt connector onboarding credentials"); + } + #[cfg(all(feature = "kms", feature = "olap"))] #[allow(clippy::expect_used)] { @@ -208,6 +243,17 @@ impl AppState { #[cfg(feature = "olap")] let pool = crate::analytics::AnalyticsProvider::from_conf(&conf.analytics).await; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + { + conf.jwekey = conf + .jwekey + .clone() + .fetch_inner::(hc_client) + .await + .expect("Failed to decrypt connector onboarding credentials"); + } + #[cfg(feature = "kms")] #[allow(clippy::expect_used)] let kms_secrets = settings::ActiveKmsSecrets { diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 8c973105d5..c0ed2b442d 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -13,15 +13,15 @@ pub mod recon; #[cfg(feature = "email")] pub mod email; -#[cfg(feature = "kms")] +#[cfg(any(feature = "kms", feature = "hashicorp-vault"))] use data_models::errors::StorageError; use data_models::errors::StorageResult; use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; -#[cfg(not(feature = "kms"))] -use masking::PeekInterface; -use masking::StrongSecret; +use masking::{PeekInterface, StrongSecret}; #[cfg(feature = "kv_store")] use storage_impl::KVRouterStore; use storage_impl::RouterStore; @@ -48,39 +48,58 @@ pub async fn get_store( #[cfg(feature = "kms")] let kms_client = kms::get_kms_client(&config.kms).await; + #[cfg(feature = "hashicorp-vault")] + let hc_client = external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + .await + .change_context(StorageError::InitializationError)?; + + let master_config = config.master_database.clone(); + + #[cfg(feature = "hashicorp-vault")] + let master_config = master_config + .fetch_inner::(hc_client) + .await + .change_context(StorageError::InitializationError) + .attach_printable("Failed to fetch data from hashicorp vault")?; + #[cfg(feature = "kms")] - let master_config = config - .master_database - .clone() + let master_config = master_config .decrypt_inner(kms_client) .await .change_context(StorageError::InitializationError) .attach_printable("Failed to decrypt master database config")?; - #[cfg(not(feature = "kms"))] - let master_config = config.master_database.clone().into(); + + #[cfg(feature = "olap")] + let replica_config = config.replica_database.clone(); + + #[cfg(all(feature = "olap", feature = "hashicorp-vault"))] + let replica_config = replica_config + .fetch_inner::(hc_client) + .await + .change_context(StorageError::InitializationError) + .attach_printable("Failed to fetch data from hashicorp vault")?; #[cfg(all(feature = "olap", feature = "kms"))] - let replica_config = config - .replica_database - .clone() + let replica_config = replica_config .decrypt_inner(kms_client) .await .change_context(StorageError::InitializationError) .attach_printable("Failed to decrypt replica database config")?; - #[cfg(all(feature = "olap", not(feature = "kms")))] - let replica_config = config.replica_database.clone().into(); - let master_enc_key = get_master_enc_key( config, #[cfg(feature = "kms")] kms_client, + #[cfg(feature = "hashicorp-vault")] + hc_client, ) .await; #[cfg(not(feature = "olap"))] - let conf = master_config; + let conf = master_config.into(); #[cfg(feature = "olap")] - let conf = (master_config, replica_config); + // this would get abstracted, for all cases + #[allow(clippy::useless_conversion)] + let conf = (master_config.into(), replica_config.into()); let store: RouterStore = if test_transaction { RouterStore::test_store(conf, &config.redis, master_enc_key).await? @@ -110,21 +129,26 @@ pub async fn get_store( async fn get_master_enc_key( conf: &crate::configs::settings::Settings, #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] + hc_client: &external_services::hashicorp_vault::HashiCorpVault, ) -> StrongSecret> { + let master_enc_key = conf.secrets.master_enc_key.clone(); + + #[cfg(feature = "hashicorp-vault")] + let master_enc_key = master_enc_key + .fetch_inner::(hc_client) + .await + .expect("Failed to fetch master enc key"); + #[cfg(feature = "kms")] - let master_enc_key = hex::decode( - conf.secrets - .master_enc_key - .clone() + let master_enc_key = masking::Secret::<_, masking::WithType>::new( + master_enc_key .decrypt_inner(kms_client) .await .expect("Failed to decrypt master enc key"), - ) - .expect("Failed to decode from hex"); + ); - #[cfg(not(feature = "kms"))] - let master_enc_key = - hex::decode(conf.secrets.master_enc_key.peek()).expect("Failed to decode from hex"); + let master_enc_key = hex::decode(master_enc_key.peek()).expect("Failed to decode from hex"); StrongSecret::new(master_enc_key) } diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index eaadc0d5c7..7f1e078ad5 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -3,9 +3,13 @@ use api_models::{payment_methods::PaymentMethodListRequest, payments}; use async_trait::async_trait; use common_utils::date_time; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +#[cfg(feature = "hashicorp-vault")] +use masking::ExposeInterface; use masking::{PeekInterface, StrongSecret}; use serde::Serialize; @@ -222,6 +226,10 @@ where &config.api_keys, #[cfg(feature = "kms")] kms::get_kms_client(&config.kms).await, + #[cfg(feature = "hashicorp-vault")] + external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?, ) .await? }; @@ -281,9 +289,14 @@ static ADMIN_API_KEY: tokio::sync::OnceCell> = pub async fn get_admin_api_key( secrets: &settings::Secrets, #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] + hc_client: &external_services::hashicorp_vault::HashiCorpVault, ) -> RouterResult<&'static StrongSecret> { ADMIN_API_KEY .get_or_try_init(|| async { + #[cfg(not(feature = "kms"))] + let admin_api_key = secrets.admin_api_key.clone(); + #[cfg(feature = "kms")] let admin_api_key = secrets .kms_encrypted_admin_api_key @@ -292,8 +305,13 @@ pub async fn get_admin_api_key( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to KMS decrypt admin API key")?; - #[cfg(not(feature = "kms"))] - let admin_api_key = secrets.admin_api_key.clone(); + #[cfg(feature = "hashicorp-vault")] + let admin_api_key = masking::Secret::new(admin_api_key) + .fetch_inner::(hc_client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to KMS decrypt admin API key")? + .expose(); Ok(StrongSecret::new(admin_api_key)) }) @@ -348,6 +366,11 @@ where &conf.secrets, #[cfg(feature = "kms")] kms::get_kms_client(&conf.kms).await, + #[cfg(feature = "hashicorp-vault")] + external_services::hashicorp_vault::get_hashicorp_client(&conf.hc_vault) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting admin api key")?, ) .await?; diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs index 118d9df28e..a01f2520b6 100644 --- a/crates/router/src/utils/currency.rs +++ b/crates/router/src/utils/currency.rs @@ -4,6 +4,8 @@ use api_models::enums; use common_utils::{date_time, errors::CustomResult, events::ApiEventMetric, ext_traits::AsyncExt}; use currency_conversion::types::{CurrencyFactors, ExchangeRates}; use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::{self, decrypt::VaultFetch}; #[cfg(feature = "kms")] use external_services::kms; use masking::PeekInterface; @@ -127,6 +129,8 @@ async fn waited_fetch_and_update_caches( local_fetch_retry_delay: u64, local_fetch_retry_count: u64, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { for _n in 1..local_fetch_retry_count { sleep(Duration::from_millis(local_fetch_retry_delay)).await; @@ -149,6 +153,8 @@ async fn waited_fetch_and_update_caches( None, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -187,6 +193,8 @@ pub async fn get_forex_rates( local_fetch_retry_delay: u64, local_fetch_retry_count: u64, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { if let Some(local_rates) = retrieve_forex_from_local().await { if local_rates.is_expired(call_delay) { @@ -197,6 +205,8 @@ pub async fn get_forex_rates( local_rates, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } else { @@ -212,6 +222,8 @@ pub async fn get_forex_rates( local_fetch_retry_count, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -223,6 +235,8 @@ async fn handler_local_no_data( _local_fetch_retry_delay: u64, _local_fetch_retry_count: u64, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { match retrieve_forex_from_redis(state).await { Ok(Some(data)) => { @@ -232,6 +246,8 @@ async fn handler_local_no_data( call_delay, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -242,6 +258,8 @@ async fn handler_local_no_data( None, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await?) } @@ -252,6 +270,8 @@ async fn handler_local_no_data( None, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await?) } @@ -262,6 +282,8 @@ async fn successive_fetch_and_save_forex( state: &AppState, stale_redis_data: Option, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { match acquire_redis_lock(state).await { Ok(lock_acquired) => { @@ -272,6 +294,8 @@ async fn successive_fetch_and_save_forex( state, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await; match api_rates { @@ -283,6 +307,8 @@ async fn successive_fetch_and_save_forex( state, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await; match secondary_api_rates { @@ -326,6 +352,8 @@ async fn fallback_forex_redis_check( redis_data: FxExchangeRatesCacheEntry, call_delay: i64, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { match is_redis_expired(Some(redis_data.clone()).as_ref(), call_delay).await { Some(redis_forex) => { @@ -341,6 +369,8 @@ async fn fallback_forex_redis_check( Some(redis_data), #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -352,6 +382,8 @@ async fn handler_local_expired( call_delay: i64, local_rates: FxExchangeRatesCacheEntry, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { match retrieve_forex_from_redis(state).await { Ok(redis_data) => { @@ -370,6 +402,8 @@ async fn handler_local_expired( Some(local_rates), #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -383,6 +417,8 @@ async fn handler_local_expired( Some(local_rates), #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -392,16 +428,40 @@ async fn handler_local_expired( async fn fetch_forex_rates( state: &AppState, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> Result> { + let forex_api_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = hashicorp_vault::get_hashicorp_client(hc_config) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.forex_api.api_key.clone(); + #[cfg(feature = "hashicorp-vault")] + let output = state + .conf + .forex_api + .api_key + .clone() + .fetch_inner::(client) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + Ok::<_, error_stack::Report>(output) + } + .await?; #[cfg(feature = "kms")] let forex_api_key = kms::get_kms_client(kms_config) .await - .decrypt(state.conf.forex_api.api_key.peek()) + .decrypt(forex_api_key.peek()) .await .change_context(ForexCacheError::KmsDecryptionFailed)?; #[cfg(not(feature = "kms"))] - let forex_api_key = state.conf.forex_api.api_key.peek(); + let forex_api_key = forex_api_key.peek(); let forex_url: String = format!("{}{}{}", FOREX_BASE_URL, forex_api_key, FOREX_BASE_CURRENCY); let forex_request = services::RequestBuilder::new() @@ -457,16 +517,39 @@ async fn fetch_forex_rates( pub async fn fallback_fetch_forex_rates( state: &AppState, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { + let fallback_api_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = hashicorp_vault::get_hashicorp_client(hc_config) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.forex_api.fallback_api_key.clone(); + #[cfg(feature = "hashicorp-vault")] + let output = state + .conf + .forex_api + .fallback_api_key + .clone() + .fetch_inner::(client) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + Ok::<_, error_stack::Report>(output) + } + .await?; #[cfg(feature = "kms")] let fallback_forex_api_key = kms::get_kms_client(kms_config) .await - .decrypt(state.conf.forex_api.fallback_api_key.peek()) + .decrypt(fallback_api_key.peek()) .await .change_context(ForexCacheError::KmsDecryptionFailed)?; #[cfg(not(feature = "kms"))] - let fallback_forex_api_key = state.conf.forex_api.fallback_api_key.peek(); + let fallback_forex_api_key = fallback_api_key.peek(); let fallback_forex_url: String = format!("{}{}", FALLBACK_FOREX_BASE_URL, fallback_forex_api_key,); @@ -609,6 +692,8 @@ pub async fn convert_currency( to_currency: String, from_currency: String, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { let rates = get_forex_rates( &state, @@ -617,6 +702,8 @@ pub async fn convert_currency( state.conf.forex_api.local_fetch_retry_count, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await .change_context(ForexCacheError::ApiError)?; diff --git a/crates/router_env/Cargo.toml b/crates/router_env/Cargo.toml index 8dca7942ab..579f4ef10e 100644 --- a/crates/router_env/Cargo.toml +++ b/crates/router_env/Cargo.toml @@ -22,10 +22,10 @@ serde_path_to_error = "0.1.14" strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.21", default-features = false, features = ["formatting"] } tokio = { version = "1.35.1" } -tracing = { version = "=0.1.36" } +tracing = { version = "0.1.37" } tracing-actix-web = { version = "0.7.8", features = ["opentelemetry_0_19", "uuid_v7"], optional = true } tracing-appender = { version = "0.2.2" } -tracing-attributes = "=0.1.22" +tracing-attributes = "0.1.27" tracing-opentelemetry = { version = "0.19.0" } tracing-subscriber = { version = "0.3.17", default-features = true, features = ["env-filter", "json", "registry"] } vergen = { version = "8.2.1", optional = true, features = ["cargo", "git", "git2", "rustc"] } @@ -43,4 +43,4 @@ actix_web = ["tracing-actix-web"] log_custom_entries_to_extra = [] log_extra_implicit_fields = [] log_active_span_json = [] -payouts = [] \ No newline at end of file +payouts = []