refactor: extract kms module to external_services crate (#793)

This commit is contained in:
Sanchith Hegde
2023-03-24 14:31:59 +05:30
committed by GitHub
parent 346bd95445
commit 029e3894fe
15 changed files with 162 additions and 67 deletions

19
Cargo.lock generated
View File

@ -1618,6 +1618,22 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "external_services"
version = "0.1.0"
dependencies = [
"aws-config",
"aws-sdk-kms",
"base64 0.21.0",
"common_utils",
"error-stack",
"once_cell",
"router_env",
"serde",
"thiserror",
"tokio",
]
[[package]] [[package]]
name = "fake" name = "fake"
version = "2.5.0" version = "2.5.0"
@ -3433,8 +3449,6 @@ dependencies = [
"async-bb8-diesel", "async-bb8-diesel",
"async-trait", "async-trait",
"awc", "awc",
"aws-config",
"aws-sdk-kms",
"base64 0.21.0", "base64 0.21.0",
"bb8", "bb8",
"blake3", "blake3",
@ -3448,6 +3462,7 @@ dependencies = [
"dyn-clone", "dyn-clone",
"encoding_rs", "encoding_rs",
"error-stack", "error-stack",
"external_services",
"frunk", "frunk",
"frunk_core", "frunk_core",
"futures", "futures",

View File

@ -18,6 +18,7 @@ pub mod logger {
vec![ vec![
"drainer", "drainer",
"common_utils", "common_utils",
"external_services",
"redis_interface", "redis_interface",
"router_env", "router_env",
"storage_models", "storage_models",

View File

@ -0,0 +1,25 @@
[package]
name = "external_services"
description = "Interactions of the router with external systems"
version = "0.1.0"
edition = "2021"
rust-version = "1.65"
readme = "README.md"
license = "Apache-2.0"
[features]
kms = ["dep:aws-config", "dep:aws-sdk-kms"]
[dependencies]
aws-config = { version = "0.54.1", optional = true }
aws-sdk-kms = { version = "0.24.0", optional = true }
base64 = "0.21.0"
error-stack = "0.3.1"
once_cell = "1.17.1"
serde = { version = "1.0.155", features = ["derive"] }
thiserror = "1.0.39"
tokio = "1.26.0"
# First party crates
common_utils = { version = "0.1.0", path = "../common_utils" }
router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] }

View File

@ -0,0 +1,4 @@
# External Services
This crate includes interactions with external systems, which may be shared
across crates within this workspace.

View File

@ -1,23 +1,35 @@
//! Interactions with the AWS KMS SDK
use aws_config::meta::region::RegionProviderChain; use aws_config::meta::region::RegionProviderChain;
use aws_sdk_kms::{types::Blob, Client, Region}; use aws_sdk_kms::{types::Blob, Client, Region};
use base64::Engine; use base64::Engine;
use common_utils::errors::CustomResult;
use error_stack::{IntoReport, ResultExt}; use error_stack::{IntoReport, ResultExt};
use router_env::logger;
use crate::{ use crate::{consts, metrics};
configs::settings,
consts,
core::errors::{self, CustomResult},
logger,
routes::metrics,
};
static KMS_CLIENT: tokio::sync::OnceCell<KmsClient> = tokio::sync::OnceCell::const_new(); static KMS_CLIENT: tokio::sync::OnceCell<KmsClient> = tokio::sync::OnceCell::const_new();
/// Returns a shared KMS client, or initializes a new one if not previously initialized.
#[inline] #[inline]
pub async fn get_kms_client(config: &settings::Kms) -> &KmsClient { pub async fn get_kms_client(config: &KmsConfig) -> &KmsClient {
KMS_CLIENT.get_or_init(|| KmsClient::new(config)).await KMS_CLIENT.get_or_init(|| KmsClient::new(config)).await
} }
/// Configuration parameters required for constructing a [`KmsClient`].
#[derive(Clone, Debug, Default, serde::Deserialize)]
#[serde(default)]
pub struct KmsConfig {
/// The AWS key identifier of the KMS key used to encrypt or decrypt data.
pub key_id: String,
/// The AWS region to send KMS requests to.
pub region: String,
}
/// Client for KMS operations.
#[derive(Debug)]
pub struct KmsClient { pub struct KmsClient {
inner_client: Client, inner_client: Client,
key_id: String, key_id: String,
@ -25,7 +37,7 @@ pub struct KmsClient {
impl KmsClient { impl KmsClient {
/// Constructs a new KMS client. /// Constructs a new KMS client.
pub async fn new(config: &settings::Kms) -> Self { pub async fn new(config: &KmsConfig) -> Self {
let region_provider = RegionProviderChain::first_try(Region::new(config.region.clone())); let region_provider = RegionProviderChain::first_try(Region::new(config.region.clone()));
let sdk_config = aws_config::from_env().region(region_provider).load().await; let sdk_config = aws_config::from_env().region(region_provider).load().await;
@ -39,11 +51,11 @@ impl KmsClient {
/// the SDK has the values required to interact with the AWS KMS APIs (`AWS_ACCESS_KEY_ID` and /// the SDK has the values required to interact with the AWS KMS APIs (`AWS_ACCESS_KEY_ID` and
/// `AWS_SECRET_ACCESS_KEY`) either set in environment variables, or that the SDK is running in /// `AWS_SECRET_ACCESS_KEY`) either set in environment variables, or that the SDK is running in
/// a machine that is able to assume an IAM role. /// a machine that is able to assume an IAM role.
pub async fn decrypt(&self, data: impl AsRef<[u8]>) -> CustomResult<String, errors::KmsError> { pub async fn decrypt(&self, data: impl AsRef<[u8]>) -> CustomResult<String, KmsError> {
let data = consts::BASE64_ENGINE let data = consts::BASE64_ENGINE
.decode(data) .decode(data)
.into_report() .into_report()
.change_context(errors::KmsError::Base64DecodingFailed)?; .change_context(KmsError::Base64DecodingFailed)?;
let ciphertext_blob = Blob::new(data); let ciphertext_blob = Blob::new(data);
let decrypt_output = self let decrypt_output = self
@ -61,16 +73,51 @@ impl KmsClient {
error error
}) })
.into_report() .into_report()
.change_context(errors::KmsError::DecryptionFailed)?; .change_context(KmsError::DecryptionFailed)?;
decrypt_output decrypt_output
.plaintext .plaintext
.ok_or(errors::KmsError::MissingPlaintextDecryptionOutput) .ok_or(KmsError::MissingPlaintextDecryptionOutput)
.into_report() .into_report()
.and_then(|blob| { .and_then(|blob| {
String::from_utf8(blob.into_inner()) String::from_utf8(blob.into_inner())
.into_report() .into_report()
.change_context(errors::KmsError::Utf8DecodingFailed) .change_context(KmsError::Utf8DecodingFailed)
}) })
} }
} }
/// Errors that could occur during KMS operations.
#[derive(Debug, thiserror::Error)]
pub enum KmsError {
/// 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,
}
impl KmsConfig {
/// Verifies that the [`KmsClient`] configuration is usable.
pub fn validate(&self) -> Result<(), &'static str> {
use common_utils::{ext_traits::ConfigExt, fp_utils::when};
when(self.key_id.is_default_or_empty(), || {
Err("KMS AWS key ID must not be empty")
})?;
when(self.region.is_default_or_empty(), || {
Err("KMS AWS region must not be empty")
})
}
}

View File

@ -0,0 +1,27 @@
//! Interactions with external systems.
#![forbid(unsafe_code)]
#![warn(missing_docs, missing_debug_implementations)]
#[cfg(feature = "kms")]
pub mod kms;
/// Crate specific constants
#[cfg(feature = "kms")]
pub mod consts {
/// General purpose base64 engine
pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose =
base64::engine::general_purpose::STANDARD;
}
/// Metrics for interactions with external systems.
#[cfg(feature = "kms")]
pub mod metrics {
use router_env::{counter_metric, global_meter, metrics_context};
metrics_context!(CONTEXT);
global_meter!(GLOBAL_METER, "EXTERNAL_SERVICES");
#[cfg(feature = "kms")]
counter_metric!(AWS_KMS_FAILURES, GLOBAL_METER); // No. of AWS KMS API failures
}

View File

@ -11,7 +11,7 @@ build = "src/build.rs"
[features] [features]
default = ["kv_store", "stripe", "oltp", "olap", "accounts_cache"] default = ["kv_store", "stripe", "oltp", "olap", "accounts_cache"]
kms = ["aws-config", "aws-sdk-kms"] kms = ["external_services/kms"]
basilisk = ["kms"] basilisk = ["kms"]
stripe = ["dep:serde_qs"] stripe = ["dep:serde_qs"]
sandbox = ["kms", "stripe", "basilisk"] sandbox = ["kms", "stripe", "basilisk"]
@ -31,8 +31,6 @@ actix-rt = "2.8.0"
actix-web = "4.3.1" actix-web = "4.3.1"
async-bb8-diesel = { git = "https://github.com/juspay/async-bb8-diesel", rev = "9a71d142726dbc33f41c1fd935ddaa79841c7be5" } async-bb8-diesel = { git = "https://github.com/juspay/async-bb8-diesel", rev = "9a71d142726dbc33f41c1fd935ddaa79841c7be5" }
async-trait = "0.1.66" async-trait = "0.1.66"
aws-config = { version = "0.54.1", optional = true }
aws-sdk-kms = { version = "0.24.0", optional = true }
base64 = "0.21.0" base64 = "0.21.0"
bb8 = "0.8" bb8 = "0.8"
blake3 = "1.3.3" blake3 = "1.3.3"
@ -83,6 +81,7 @@ uuid = { version = "1.3.0", features = ["serde", "v4"] }
# First party crates # First party crates
api_models = { version = "0.1.0", path = "../api_models" } api_models = { version = "0.1.0", path = "../api_models" }
common_utils = { version = "0.1.0", path = "../common_utils" } common_utils = { version = "0.1.0", path = "../common_utils" }
external_services = { version = "0.1.0", path = "../external_services" }
masking = { version = "0.1.0", path = "../masking" } masking = { version = "0.1.0", path = "../masking" }
redis_interface = { version = "0.1.0", path = "../redis_interface" } redis_interface = { version = "0.1.0", path = "../redis_interface" }
router_derive = { version = "0.1.0", path = "../router_derive" } router_derive = { version = "0.1.0", path = "../router_derive" }

View File

@ -6,6 +6,8 @@ use std::{
use common_utils::ext_traits::ConfigExt; use common_utils::ext_traits::ConfigExt;
use config::{Environment, File}; use config::{Environment, File};
#[cfg(feature = "kms")]
use external_services::kms;
use redis_interface::RedisSettings; use redis_interface::RedisSettings;
pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
@ -59,7 +61,7 @@ pub struct Settings {
pub bank_config: BankRedirectConfig, pub bank_config: BankRedirectConfig,
pub api_keys: ApiKeys, pub api_keys: ApiKeys,
#[cfg(feature = "kms")] #[cfg(feature = "kms")]
pub kms: Kms, pub kms: kms::KmsConfig,
} }
#[derive(Debug, Deserialize, Clone, Default)] #[derive(Debug, Deserialize, Clone, Default)]
@ -337,14 +339,6 @@ pub struct ApiKeys {
pub hash_key: String, pub hash_key: String,
} }
#[cfg(feature = "kms")]
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(default)]
pub struct Kms {
pub key_id: String,
pub region: String,
}
impl Settings { impl Settings {
pub fn new() -> ApplicationResult<Self> { pub fn new() -> ApplicationResult<Self> {
Self::with_config_path(None) Self::with_config_path(None)
@ -420,7 +414,9 @@ impl Settings {
self.drainer.validate()?; self.drainer.validate()?;
self.api_keys.validate()?; self.api_keys.validate()?;
#[cfg(feature = "kms")] #[cfg(feature = "kms")]
self.kms.validate()?; self.kms
.validate()
.map_err(|error| ApplicationError::InvalidConfigurationValueError(error.into()))?;
Ok(()) Ok(())
} }

View File

@ -184,22 +184,3 @@ impl super::settings::ApiKeys {
}) })
} }
} }
#[cfg(feature = "kms")]
impl super::settings::Kms {
pub fn validate(&self) -> Result<(), ApplicationError> {
use common_utils::fp_utils::when;
when(self.key_id.is_default_or_empty(), || {
Err(ApplicationError::InvalidConfigurationValueError(
"KMS AWS key ID must not be empty".into(),
))
})?;
when(self.region.is_default_or_empty(), || {
Err(ApplicationError::InvalidConfigurationValueError(
"KMS AWS region must not be empty".into(),
))
})
}
}

View File

@ -1,10 +1,10 @@
use common_utils::date_time; use common_utils::date_time;
use error_stack::{report, IntoReport, ResultExt}; use error_stack::{report, IntoReport, ResultExt};
#[cfg(feature = "kms")]
use external_services::kms;
use masking::{PeekInterface, StrongSecret}; use masking::{PeekInterface, StrongSecret};
use router_env::{instrument, tracing}; use router_env::{instrument, tracing};
#[cfg(feature = "kms")]
use crate::services::kms;
use crate::{ use crate::{
configs::settings, configs::settings,
consts, consts,
@ -21,7 +21,7 @@ static HASH_KEY: tokio::sync::OnceCell<StrongSecret<[u8; PlaintextApiKey::HASH_K
pub async fn get_hash_key( pub async fn get_hash_key(
api_key_config: &settings::ApiKeys, api_key_config: &settings::ApiKeys,
#[cfg(feature = "kms")] kms_config: &settings::Kms, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
) -> errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> { ) -> errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> {
HASH_KEY HASH_KEY
.get_or_try_init(|| async { .get_or_try_init(|| async {
@ -119,7 +119,7 @@ impl PlaintextApiKey {
pub async fn create_api_key( pub async fn create_api_key(
store: &dyn StorageInterface, store: &dyn StorageInterface,
api_key_config: &settings::ApiKeys, api_key_config: &settings::ApiKeys,
#[cfg(feature = "kms")] kms_config: &settings::Kms, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
api_key: api::CreateApiKeyRequest, api_key: api::CreateApiKeyRequest,
merchant_id: String, merchant_id: String,
) -> RouterResponse<api::CreateApiKeyResponse> { ) -> RouterResponse<api::CreateApiKeyResponse> {

View File

@ -1,10 +1,10 @@
use common_utils::ext_traits::StringExt; use common_utils::ext_traits::StringExt;
use error_stack::ResultExt; use error_stack::ResultExt;
#[cfg(feature = "kms")]
use external_services::kms;
use josekit::jwe; use josekit::jwe;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "kms")]
use crate::services::kms;
use crate::{ use crate::{
configs::settings, configs::settings,
core::errors::{self, CustomResult}, core::errors::{self, CustomResult},
@ -149,7 +149,7 @@ pub fn get_dotted_jws(jws: encryption::JwsBody) -> String {
pub async fn get_decrypted_response_payload( pub async fn get_decrypted_response_payload(
jwekey: &settings::Jwekey, jwekey: &settings::Jwekey,
jwe_body: encryption::JweBody, jwe_body: encryption::JweBody,
#[cfg(feature = "kms")] kms_config: &settings::Kms, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
) -> CustomResult<String, errors::VaultError> { ) -> CustomResult<String, errors::VaultError> {
#[cfg(feature = "kms")] #[cfg(feature = "kms")]
let public_key = kms::get_kms_client(kms_config) let public_key = kms::get_kms_client(kms_config)
@ -192,7 +192,7 @@ pub async fn get_decrypted_response_payload(
pub async fn mk_basilisk_req( pub async fn mk_basilisk_req(
jwekey: &settings::Jwekey, jwekey: &settings::Jwekey,
jws: &str, jws: &str,
#[cfg(feature = "kms")] kms_config: &settings::Kms, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
) -> CustomResult<encryption::JweBody, errors::VaultError> { ) -> CustomResult<encryption::JweBody, errors::VaultError> {
let jws_payload: Vec<&str> = jws.split('.').collect(); let jws_payload: Vec<&str> = jws.split('.').collect();
@ -247,7 +247,7 @@ pub async fn mk_add_card_request_hs(
card: &api::CardDetail, card: &api::CardDetail,
customer_id: &str, customer_id: &str,
merchant_id: &str, merchant_id: &str,
#[cfg(feature = "kms")] kms_config: &settings::Kms, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
) -> CustomResult<services::Request, errors::VaultError> { ) -> CustomResult<services::Request, errors::VaultError> {
let merchant_customer_id = if cfg!(feature = "sandbox") { let merchant_customer_id = if cfg!(feature = "sandbox") {
format!("{customer_id}::{merchant_id}") format!("{customer_id}::{merchant_id}")
@ -409,7 +409,7 @@ pub async fn mk_get_card_request_hs(
customer_id: &str, customer_id: &str,
merchant_id: &str, merchant_id: &str,
card_reference: &str, card_reference: &str,
#[cfg(feature = "kms")] kms_config: &settings::Kms, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
) -> CustomResult<services::Request, errors::VaultError> { ) -> CustomResult<services::Request, errors::VaultError> {
let merchant_customer_id = if cfg!(feature = "sandbox") { let merchant_customer_id = if cfg!(feature = "sandbox") {
format!("{customer_id}::{merchant_id}") format!("{customer_id}::{merchant_id}")
@ -501,7 +501,7 @@ pub async fn mk_delete_card_request_hs(
customer_id: &str, customer_id: &str,
merchant_id: &str, merchant_id: &str,
card_reference: &str, card_reference: &str,
#[cfg(feature = "kms")] kms_config: &settings::Kms, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
) -> CustomResult<services::Request, errors::VaultError> { ) -> CustomResult<services::Request, errors::VaultError> {
let merchant_customer_id = if cfg!(feature = "sandbox") { let merchant_customer_id = if cfg!(feature = "sandbox") {
format!("{customer_id}::{merchant_id}") format!("{customer_id}::{merchant_id}")

View File

@ -1,6 +1,8 @@
use common_utils::generate_id_with_default_len; use common_utils::generate_id_with_default_len;
use error_stack::{IntoReport, ResultExt}; use error_stack::{IntoReport, ResultExt};
#[cfg(feature = "basilisk")] #[cfg(feature = "basilisk")]
use external_services::kms;
#[cfg(feature = "basilisk")]
use josekit::jwe; use josekit::jwe;
use masking::PeekInterface; use masking::PeekInterface;
use router_env::{instrument, tracing}; use router_env::{instrument, tracing};
@ -18,11 +20,7 @@ use crate::{
utils::{self, StringExt}, utils::{self, StringExt},
}; };
#[cfg(feature = "basilisk")] #[cfg(feature = "basilisk")]
use crate::{ use crate::{core::payment_methods::transformers as payment_methods, services, utils::BytesExt};
core::payment_methods::transformers as payment_methods,
services::{self, kms},
utils::BytesExt,
};
#[cfg(feature = "basilisk")] #[cfg(feature = "basilisk")]
use crate::{ use crate::{
db, db,
@ -407,7 +405,7 @@ pub fn get_key_id(keys: &settings::Jwekey) -> &str {
#[cfg(feature = "basilisk")] #[cfg(feature = "basilisk")]
async fn get_locker_jwe_keys( async fn get_locker_jwe_keys(
keys: &settings::Jwekey, keys: &settings::Jwekey,
kms_config: &settings::Kms, kms_config: &kms::KmsConfig,
) -> CustomResult<(String, String), errors::EncryptionError> { ) -> CustomResult<(String, String), errors::EncryptionError> {
let key_id = get_key_id(keys); let key_id = get_key_id(keys);
let (encryption_key, decryption_key) = if key_id == keys.locker_key_identifier1 { let (encryption_key, decryption_key) = if key_id == keys.locker_key_identifier1 {

View File

@ -20,6 +20,7 @@ pub mod logger {
"actix_server", "actix_server",
"api_models", "api_models",
"common_utils", "common_utils",
"external_services",
"masking", "masking",
"redis_interface", "redis_interface",
"router_derive", "router_derive",

View File

@ -1,8 +1,6 @@
pub mod api; pub mod api;
pub mod authentication; pub mod authentication;
pub mod encryption; pub mod encryption;
#[cfg(feature = "kms")]
pub mod kms;
pub mod logger; pub mod logger;
use std::sync::{atomic, Arc}; use std::sync::{atomic, Arc};

View File

@ -1,5 +1,7 @@
#[cfg(feature = "vergen")]
use router_env as env; use router_env as env;
#[cfg(feature = "vergen")]
#[tokio::test] #[tokio::test]
async fn basic() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn basic() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("CARGO_PKG_VERSION : {:?}", env!("CARGO_PKG_VERSION")); println!("CARGO_PKG_VERSION : {:?}", env!("CARGO_PKG_VERSION"));
@ -19,6 +21,7 @@ async fn basic() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(()) Ok(())
} }
#[cfg(feature = "vergen")]
#[tokio::test] #[tokio::test]
async fn env_macro() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn env_macro() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("version : {:?}", env::version!()); println!("version : {:?}", env::version!());