feat(hashicorp): implement hashicorp secrets manager solution (#3297)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Nishant Joshi
2024-01-24 14:06:52 +05:30
committed by GitHub
parent cc7e33a575
commit 629d546aa7
28 changed files with 1094 additions and 84 deletions

View File

@ -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 = [

192
Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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]

View File

@ -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::<Kv2>(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://{}:{}@{}:{}/{}",

View File

@ -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),

View File

@ -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)]

View File

@ -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" }

View File

@ -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<HashiCorpVault> = 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<Box<dyn Future<Output = error_stack::Result<T, HashiCorpError>> + 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::<HashMap<String, String>>(&client.client, mount, path)
.await
.map_err(Into::<Report<_>>::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<Self, HashiCorpError> {
VaultClient::new(
VaultClientSettingsBuilder::default()
.address(&config.url)
.token(&config.token)
.build()
.map_err(Into::<Report<_>>::into)
.change_context(HashiCorpError::ClientCreationFailed)
.attach_printable("Failed while building vault settings")?,
)
.map_err(Into::<Report<_>>::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<En, I>(&self, data: String) -> error_stack::Result<I, HashiCorpError>
where
for<'a> En: Engine<
ReturnType<'a, String> = Pin<
Box<
dyn Future<Output = error_stack::Result<String, HashiCorpError>>
+ 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<Self>` representing the constructed instance if successful, or `None` otherwise.
///
/// # Example
///
/// ```rust
/// # use your_module::{FromEncoded, masking::Secret, Vec};
/// let secret_instance = Secret::<String>::from_encoded("encoded_secret_string".to_string());
/// let vec_instance = Vec::<u8>::from_encoded("68656c6c6f".to_string());
/// ```
fn from_encoded(input: String) -> Option<Self>;
}
impl FromEncoded for masking::Secret<String> {
fn from_encoded(input: String) -> Option<Self> {
Some(input.into())
}
}
impl FromEncoded for Vec<u8> {
fn from_encoded(input: String) -> Option<Self> {
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,
}

View File

@ -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<Self, super::HashiCorpError>` representing the decrypted instance if successful,
/// or an `super::HashiCorpError` with details about the encountered error.
///
async fn fetch_inner<En>(
self,
client: &super::HashiCorpVault,
) -> error_stack::Result<Self, super::HashiCorpError>
where
for<'a> En: super::Engine<
ReturnType<'a, String> = Pin<
Box<
dyn Future<Output = error_stack::Result<String, super::HashiCorpError>>
+ Send
+ 'a,
>,
>,
> + 'a;
}
#[async_trait::async_trait]
impl VaultFetch for masking::Secret<String> {
async fn fetch_inner<En>(
self,
client: &super::HashiCorpVault,
) -> error_stack::Result<Self, super::HashiCorpError>
where
for<'a> En: super::Engine<
ReturnType<'a, String> = Pin<
Box<
dyn Future<Output = error_stack::Result<String, super::HashiCorpError>>
+ Send
+ 'a,
>,
>,
> + 'a,
{
client.fetch::<En, Self>(self.expose()).await
}
}

View File

@ -190,6 +190,44 @@ impl KmsConfig {
#[serde(transparent)]
pub struct KmsValue(Secret<String>);
impl From<String> for KmsValue {
fn from(value: String) -> Self {
Self(Secret::new(value))
}
}
impl From<Secret<String>> for KmsValue {
fn from(value: Secret<String>) -> Self {
Self(value)
}
}
#[cfg(feature = "hashicorp-vault")]
#[async_trait::async_trait]
impl super::hashicorp_vault::decrypt::VaultFetch for KmsValue {
async fn fetch_inner<En>(
self,
client: &super::hashicorp_vault::HashiCorpVault,
) -> error_stack::Result<Self, super::hashicorp_vault::HashiCorpError>
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::<En>(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()

View File

@ -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 {

View File

@ -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"]

View File

@ -1,4 +1,6 @@
mod defaults;
#[cfg(feature = "hashicorp-vault")]
pub mod hc_vault;
#[cfg(feature = "kms")]
pub mod kms;
pub mod settings;

View File

@ -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<En>(
mut self,
client: &HashiCorpVault,
) -> error_stack::Result<Self, HashiCorpError>
where
for<'a> En: Engine<
ReturnType<'a, String> = std::pin::Pin<
Box<
dyn std::future::Future<
Output = error_stack::Result<String, HashiCorpError>,
> + 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::<En>(client)
.await?
.expose(),
masking::Secret::new(self.rust_locker_encryption_key)
.fetch_inner::<En>(client)
.await?
.expose(),
masking::Secret::new(self.vault_private_key)
.fetch_inner::<En>(client)
.await?
.expose(),
masking::Secret::new(self.tunnel_private_key)
.fetch_inner::<En>(client)
.await?
.expose(),
);
Ok(self)
}
}
#[async_trait::async_trait]
impl VaultFetch for settings::Database {
async fn fetch_inner<En>(
mut self,
client: &HashiCorpVault,
) -> error_stack::Result<Self, HashiCorpError>
where
for<'a> En: Engine<
ReturnType<'a, String> = std::pin::Pin<
Box<
dyn std::future::Future<
Output = error_stack::Result<String, HashiCorpError>,
> + Send
+ 'a,
>,
>,
> + 'a,
{
Ok(Self {
host: self.host,
port: self.port,
dbname: self.dbname,
username: self.username,
password: self.password.fetch_inner::<En>(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<En>(
mut self,
client: &HashiCorpVault,
) -> error_stack::Result<Self, HashiCorpError>
where
for<'a> En: Engine<
ReturnType<'a, String> = std::pin::Pin<
Box<
dyn std::future::Future<
Output = error_stack::Result<String, HashiCorpError>,
> + Send
+ 'a,
>,
>,
> + 'a,
{
self.client_id = self.client_id.fetch_inner::<En>(client).await?;
self.client_secret = self.client_secret.fetch_inner::<En>(client).await?;
self.partner_id = self.partner_id.fetch_inner::<En>(client).await?;
Ok(self)
}
}
#[cfg(feature = "olap")]
#[async_trait::async_trait]
impl VaultFetch for settings::ConnectorOnboarding {
async fn fetch_inner<En>(
mut self,
client: &HashiCorpVault,
) -> error_stack::Result<Self, HashiCorpError>
where
for<'a> En: Engine<
ReturnType<'a, String> = std::pin::Pin<
Box<
dyn std::future::Future<
Output = error_stack::Result<String, HashiCorpError>,
> + Send
+ 'a,
>,
>,
> + 'a,
{
self.paypal = self.paypal.fetch_inner::<En>(client).await?;
Ok(self)
}
}

View File

@ -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,

View File

@ -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<StrongSecret<[u8; PlaintextApiKey::HASH_K
pub async fn get_hash_key(
api_key_config: &settings::ApiKeys,
#[cfg(feature = "kms")] kms_client: &kms::KmsClient,
#[cfg(feature = "hashicorp-vault")]
hc_client: &external_services::hashicorp_vault::HashiCorpVault,
) -> errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> {
HASH_KEY
.get_or_try_init(|| async {
let hash_key = {
#[cfg(feature = "kms")]
let hash_key = api_key_config
.kms_encrypted_hash_key
{
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::<external_services::hashicorp_vault::Kv2>(hc_client)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
#[cfg(feature = "kms")]
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<api::CreateApiKeyResponse> {
@ -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();

View File

@ -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)?,

View File

@ -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<F, Req, Op, FData, Ctx>(
state: &AppState,

View File

@ -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<errors::ApiErrorResponse>>((
masking::Secret::new(
state
.conf
.applepay_decrypt_keys
.apple_pay_merchant_cert
.clone(),
)
.fetch_inner::<hashicorp_vault::Kv2>(client)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?
.expose(),
masking::Secret::new(
state
.conf
.applepay_decrypt_keys
.apple_pay_merchant_cert_key
.clone(),
)
.fetch_inner::<hashicorp_vault::Kv2>(client)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?
.expose(),
masking::Secret::new(
state
.conf
.applepay_merchant_configs
.common_merchant_identifier
.clone(),
)
.fetch_inner::<hashicorp_vault::Kv2>(client)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?
.expose(),
))
}
#[cfg(not(feature = "hashicorp-vault"))]
{
Ok::<_, Report<errors::ApiErrorResponse>>((
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,

View File

@ -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<String, errors::ApplePayDecryptionError> {
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::<hashicorp_vault::Kv2>(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<errors::ApplePayDecryptionError>>(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::<hashicorp_vault::Kv2>(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<errors::ApplePayDecryptionError>>(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()

View File

@ -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::<hashicorp_vault::Kv2>(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<ApiErrorResponse>>(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();

View File

@ -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(),
)

View File

@ -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<dyn StorageInterface> = 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::<external_services::hashicorp_vault::Kv2>(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::<external_services::hashicorp_vault::Kv2>(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::<external_services::hashicorp_vault::Kv2>(hc_client)
.await
.expect("Failed to decrypt connector onboarding credentials");
}
#[cfg(feature = "kms")]
#[allow(clippy::expect_used)]
let kms_secrets = settings::ActiveKmsSecrets {

View File

@ -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::<external_services::hashicorp_vault::Kv2>(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::<external_services::hashicorp_vault::Kv2>(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<StoreType> = 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<Vec<u8>> {
let master_enc_key = conf.secrets.master_enc_key.clone();
#[cfg(feature = "hashicorp-vault")]
let master_enc_key = master_enc_key
.fetch_inner::<external_services::hashicorp_vault::Kv2>(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)
}

View File

@ -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<StrongSecret<String>> =
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<String>> {
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::<external_services::hashicorp_vault::Kv2>(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?;

View File

@ -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<FxExchangeRatesCacheEntry, ForexCacheError> {
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<FxExchangeRatesCacheEntry, ForexCacheError> {
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<FxExchangeRatesCacheEntry, ForexCacheError> {
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<FxExchangeRatesCacheEntry>,
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
#[cfg(feature = "hashicorp-vault")]
hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig,
) -> CustomResult<FxExchangeRatesCacheEntry, ForexCacheError> {
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<FxExchangeRatesCacheEntry, ForexCacheError> {
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<FxExchangeRatesCacheEntry, ForexCacheError> {
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<FxExchangeRatesCacheEntry, error_stack::Report<ForexCacheError>> {
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::<hashicorp_vault::Kv2>(client)
.await
.change_context(ForexCacheError::KmsDecryptionFailed)?;
Ok::<_, error_stack::Report<ForexCacheError>>(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<FxExchangeRatesCacheEntry, ForexCacheError> {
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::<hashicorp_vault::Kv2>(client)
.await
.change_context(ForexCacheError::KmsDecryptionFailed)?;
Ok::<_, error_stack::Report<ForexCacheError>>(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<api_models::currency::CurrencyConversionResponse, ForexCacheError> {
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)?;

View File

@ -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"] }