diff --git a/config/config.example.toml b/config/config.example.toml index c02402c047..052b10d0b9 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -133,7 +133,6 @@ bg_metrics_collection_interval_in_secs = 15 # Interval for collecting master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long. admin_api_key = "test_admin" # admin API key for admin authentication. jwt_secret = "secret" # JWT secret used for user authentication. -recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication. # Locker settings contain details for accessing a card locker, a # PCI Compliant storage entity which stores payment method information @@ -722,4 +721,7 @@ public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "p encryption_key = "" # Encryption key used for encrypting data in user_authentication_methods table [locker_based_open_banking_connectors] -connector_list = "" \ No newline at end of file +connector_list = "" + +[recipient_emails] +recon = "test@example.com" diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index a7bd116a32..6fe08509b7 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -256,7 +256,6 @@ url = "http://localhost:5000" # URL of the encryption service master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long. admin_api_key = "test_admin" # admin API key for admin authentication. jwt_secret = "secret" # JWT secret used for user authentication. -recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication. # Server configuration [server] @@ -300,3 +299,6 @@ public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "p [user_auth_methods] encryption_key = "user_auth_table_encryption_key" # Encryption key used for encrypting data in user_authentication_methods table + +[recipient_emails] +recon = "recon@example.com" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index eeec8f31bf..1680329589 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -369,4 +369,4 @@ keys = "accept-language,user-agent" sdk_eligible_payment_methods = "card" [locker_based_open_banking_connectors] -connector_list = "" +connector_list = "" \ No newline at end of file diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 3f744891b0..cc16d88c16 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -382,4 +382,4 @@ keys = "accept-language,user-agent" sdk_eligible_payment_methods = "card" [locker_based_open_banking_connectors] -connector_list = "" +connector_list = "" \ No newline at end of file diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 31514982c2..8a8030db05 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -386,4 +386,4 @@ keys = "accept-language,user-agent" sdk_eligible_payment_methods = "card" [locker_based_open_banking_connectors] -connector_list = "" +connector_list = "" \ No newline at end of file diff --git a/config/development.toml b/config/development.toml index 5c849a284c..27ac5bb3f5 100644 --- a/config/development.toml +++ b/config/development.toml @@ -61,7 +61,6 @@ request_body_limit = 32768 admin_api_key = "test_admin" master_enc_key = "73ad7bbbbc640c845a150f67d058b279849370cd2c1f3c67c4dd6c869213e13a" jwt_secret = "secret" -recon_admin_api_key = "recon_test_admin" [applepay_merchant_configs] merchant_cert_key = "MERCHANT CERTIFICATE KEY" @@ -727,3 +726,6 @@ encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC69 [locker_based_open_banking_connectors] connector_list = "" + +[recipient_emails] +recon = "recon@example.com" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 421984a774..98da1c3098 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -50,7 +50,6 @@ pool_size = 5 admin_api_key = "test_admin" jwt_secret = "secret" master_enc_key = "73ad7bbbbc640c845a150f67d058b279849370cd2c1f3c67c4dd6c869213e13a" -recon_admin_api_key = "recon_test_admin" [user] password_validity_in_days = 90 @@ -586,3 +585,6 @@ ach = { country = "US", currency = "USD" } [locker_based_open_banking_connectors] connector_list = "" + +[recipient_emails] +recon = "recon@example.com" diff --git a/crates/api_models/src/recon.rs b/crates/api_models/src/recon.rs index 3aafcc413d..afee0fb562 100644 --- a/crates/api_models/src/recon.rs +++ b/crates/api_models/src/recon.rs @@ -5,7 +5,6 @@ use crate::enums; #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct ReconUpdateMerchantRequest { - pub merchant_id: common_utils::id_type::MerchantId, pub recon_status: enums::ReconStatus, pub user_email: pii::Email, } diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 05087d09e8..8e1a0483c0 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -37,6 +37,7 @@ pub enum Permission { PayoutRead, WebhookEventWrite, GenerateReport, + ReconAdmin, } #[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash)] @@ -50,6 +51,7 @@ pub enum ParentGroup { Merchant, #[serde(rename = "OrganizationAccess")] Organization, + Recon, } #[derive(Debug, serde::Serialize)] @@ -67,6 +69,7 @@ pub enum PermissionModule { SurchargeDecisionManager, AccountCreate, Payouts, + Recon, } #[derive(Debug, serde::Serialize)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index db32cd2285..eef23ff5b8 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2795,6 +2795,7 @@ pub enum PermissionGroup { MerchantDetailsView, MerchantDetailsManage, OrganizationManage, + ReconOps, } /// Name of banks supported by Hyperswitch diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index 56a1cd4511..4a39f58ebe 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -252,17 +252,15 @@ impl SecretsHandler for settings::Secrets { secret_management_client: &dyn SecretManagementInterface, ) -> CustomResult, SecretsManagementError> { let secrets = value.get_inner(); - let (jwt_secret, admin_api_key, recon_admin_api_key, master_enc_key) = tokio::try_join!( + let (jwt_secret, admin_api_key, master_enc_key) = tokio::try_join!( secret_management_client.get_secret(secrets.jwt_secret.clone()), secret_management_client.get_secret(secrets.admin_api_key.clone()), - secret_management_client.get_secret(secrets.recon_admin_api_key.clone()), secret_management_client.get_secret(secrets.master_enc_key.clone()) )?; Ok(value.transition_state(|_| Self { jwt_secret, admin_api_key, - recon_admin_api_key, master_enc_key, })) } @@ -454,5 +452,6 @@ pub(crate) async fn fetch_raw_secrets( user_auth_methods, decision: conf.decision, locker_based_open_banking_connectors: conf.locker_based_open_banking_connectors, + recipient_emails: conf.recipient_emails, } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 79be0bf21d..2c27b0cfc7 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -6,7 +6,7 @@ use std::{ #[cfg(feature = "olap")] use analytics::{opensearch::OpenSearchConfig, ReportConfig}; use api_models::{enums, payment_methods::RequiredFieldInfo}; -use common_utils::ext_traits::ConfigExt; +use common_utils::{ext_traits::ConfigExt, pii::Email}; use config::{Environment, File}; use error_stack::ResultExt; #[cfg(feature = "email")] @@ -120,6 +120,7 @@ pub struct Settings { pub user_auth_methods: SecretStateContainer, pub decision: Option, pub locker_based_open_banking_connectors: LockerBasedRecipientConnectorList, + pub recipient_emails: RecipientMails, } #[derive(Debug, Deserialize, Clone, Default)] @@ -513,7 +514,6 @@ pub struct RequiredFieldFinal { pub struct Secrets { pub jwt_secret: Secret, pub admin_api_key: Secret, - pub recon_admin_api_key: Secret, pub master_enc_key: Secret, } @@ -900,6 +900,11 @@ pub struct ServerTls { pub certificate: PathBuf, } +#[derive(Debug, Deserialize, Clone, Default)] +pub struct RecipientMails { + pub recon: Email, +} + fn deserialize_hashmap_inner( value: HashMap, ) -> Result>, String> diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 3f31536021..70cfb188ea 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -136,3 +136,6 @@ pub const MAX_ALLOWED_AMOUNT: i64 = 999999999; //payment attempt default unified error code and unified error message pub const DEFAULT_UNIFIED_ERROR_CODE: &str = "UE_000"; pub const DEFAULT_UNIFIED_ERROR_MESSAGE: &str = "Something went wrong"; + +// Recon's feature tag +pub const RECON_FEATURE_TAG: &str = "RECONCILIATION AND SETTLEMENT"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 5dc4e29591..39e707cff7 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -33,6 +33,8 @@ pub mod payout_link; pub mod payouts; pub mod pm_auth; pub mod poll; +#[cfg(feature = "recon")] +pub mod recon; pub mod refunds; pub mod routing; pub mod surcharge_decision_config; diff --git a/crates/router/src/core/recon.rs b/crates/router/src/core/recon.rs new file mode 100644 index 0000000000..fa9944ee8e --- /dev/null +++ b/crates/router/src/core/recon.rs @@ -0,0 +1,172 @@ +use api_models::recon as recon_api; +use common_utils::ext_traits::AsyncExt; +use error_stack::ResultExt; +use masking::{ExposeInterface, PeekInterface, Secret}; + +use crate::{ + consts, + core::errors::{self, RouterResponse, UserErrors}, + services::{api as service_api, authentication, email::types as email_types}, + types::{ + api::{self as api_types, enums}, + domain, storage, + transformers::ForeignTryFrom, + }, + SessionState, +}; + +pub async fn send_recon_request( + state: SessionState, + user_with_auth_data: authentication::UserFromTokenWithAuthData, +) -> RouterResponse { + let user = user_with_auth_data.0; + let user_in_db = &user_with_auth_data.1.user; + let merchant_id = user.merchant_id; + + let user_email = user_in_db.email.clone(); + let email_contents = email_types::ProFeatureRequest { + feature_name: consts::RECON_FEATURE_TAG.to_string(), + merchant_id: merchant_id.clone(), + user_name: domain::UserName::new(user_in_db.name.clone()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to form username")?, + user_email: domain::UserEmail::from_pii_email(user_email.clone()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert recipient's email to UserEmail")?, + recipient_email: domain::UserEmail::from_pii_email( + state.conf.recipient_emails.recon.clone(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert recipient's email to UserEmail")?, + settings: state.conf.clone(), + subject: format!( + "Dashboard Pro Feature Request by {}", + user_email.expose().peek() + ), + }; + + state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to compose and send email for ProFeatureRequest [Recon]") + .async_and_then(|_| async { + let auth = user_with_auth_data.1; + let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate { + recon_status: enums::ReconStatus::Requested, + }; + let db = &*state.store; + let key_manager_state = &(&state).into(); + + let response = db + .update_merchant( + key_manager_state, + auth.merchant_account, + updated_merchant_account, + &auth.key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!("Failed while updating merchant's recon status: {merchant_id:?}") + })?; + + Ok(service_api::ApplicationResponse::Json( + recon_api::ReconStatusResponse { + recon_status: response.recon_status, + }, + )) + }) + .await +} + +pub async fn generate_recon_token( + state: SessionState, + user: authentication::UserFromToken, +) -> RouterResponse { + let token = authentication::AuthToken::new_token( + user.user_id.clone(), + user.merchant_id.clone(), + user.role_id.clone(), + &state.conf, + user.org_id.clone(), + user.profile_id.clone(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "Failed to create recon token for params [user_id, org_id, mid, pid] [{}, {:?}, {:?}, {:?}]", + user.user_id, user.org_id, user.merchant_id, user.profile_id, + ) + })?; + + Ok(service_api::ApplicationResponse::Json( + recon_api::ReconTokenResponse { + token: token.into(), + }, + )) +} + +pub async fn recon_merchant_account_update( + state: SessionState, + auth: authentication::AuthenticationData, + req: recon_api::ReconUpdateMerchantRequest, +) -> RouterResponse { + let db = &*state.store; + let key_manager_state = &(&state).into(); + + let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate { + recon_status: req.recon_status, + }; + let merchant_id = auth.merchant_account.get_id().clone(); + + let updated_merchant_account = db + .update_merchant( + key_manager_state, + auth.merchant_account, + updated_merchant_account, + &auth.key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!("Failed while updating merchant's recon status: {merchant_id:?}") + })?; + + let user_email = &req.user_email.clone(); + let email_contents = email_types::ReconActivation { + recipient_email: domain::UserEmail::from_pii_email(user_email.clone()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert recipient's email to UserEmail from pii::Email")?, + user_name: domain::UserName::new(Secret::new("HyperSwitch User".to_string())) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to form username")?, + settings: state.conf.clone(), + subject: "Approval of Recon Request - Access Granted to Recon Dashboard", + }; + + if req.recon_status == enums::ReconStatus::Active { + let _ = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to compose and send email for ReconActivation") + .is_ok(); + } + + Ok(service_api::ApplicationResponse::Json( + api_types::MerchantAccountResponse::foreign_try_from(updated_merchant_account) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_account", + })?, + )) +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index dac6027aa1..31ab0e8ff2 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1821,30 +1821,23 @@ pub async fn send_verification_mail( #[cfg(feature = "recon")] pub async fn verify_token( state: SessionState, - req: auth::ReconUser, + user: auth::UserFromToken, ) -> UserResponse { - let user = state + let user_in_db = state .global_store - .find_user_by_id(&req.user_id) + .find_user_by_id(&user.user_id) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(UserErrors::UserNotFound) - } else { - e.change_context(UserErrors::InternalServerError) - } + .change_context(UserErrors::InternalServerError) + .attach_printable_lazy(|| { + format!( + "Failed to fetch the user from DB for user_id - {}", + user.user_id + ) })?; - let merchant_id = state - .store - .find_user_role_by_user_id(&req.user_id, UserRoleVersion::V1) - .await - .change_context(UserErrors::InternalServerError)? - .merchant_id - .ok_or(UserErrors::InternalServerError)?; Ok(ApplicationResponse::Json(user_api::VerifyTokenResponse { - merchant_id: merchant_id.to_owned(), - user_email: user.email, + merchant_id: user.merchant_id.to_owned(), + user_email: user_in_db.email, })) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f697f704c1..4f8e269dff 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1130,7 +1130,7 @@ impl Recon { web::scope("/recon") .app_data(web::Data::new(state)) .service( - web::resource("/update_merchant") + web::resource("/{merchant_id}/update") .route(web::post().to(recon_routes::update_merchant)), ) .service(web::resource("/token").route(web::get().to(recon_routes::get_recon_token))) diff --git a/crates/router/src/routes/recon.rs b/crates/router/src/routes/recon.rs index 96a935aef5..1ec571ff7c 100644 --- a/crates/router/src/routes/recon.rs +++ b/crates/router/src/routes/recon.rs @@ -1,43 +1,29 @@ use actix_web::{web, HttpRequest, HttpResponse}; -use api_models::recon as recon_api; -use diesel_models::enums::UserRoleVersion; -use error_stack::ResultExt; -use masking::{ExposeInterface, PeekInterface, Secret}; +use api_models::{enums::EntityType, recon as recon_api}; use router_env::Flow; -use super::{AppState, SessionState}; +use super::AppState; use crate::{ - core::{ - api_locking, - errors::{self, RouterResponse, RouterResult, StorageErrorExt, UserErrors}, - }, - services::{ - api as service_api, api, - authentication::{self as auth, ReconUser, UserFromToken}, - email::types as email_types, - recon::ReconToken, - }, - types::{ - api::{self as api_types, enums}, - domain::{UserEmail, UserFromStorage, UserName}, - storage, - transformers::ForeignTryFrom, - }, + core::{api_locking, recon}, + services::{api, authentication, authorization::permissions::Permission}, }; pub async fn update_merchant( state: web::Data, req: HttpRequest, + path: web::Path, json_payload: web::Json, ) -> HttpResponse { let flow = Flow::ReconMerchantUpdate; + let merchant_id = path.into_inner(); + Box::pin(api::server_wrap( flow, state, &req, json_payload.into_inner(), - |state, _user, req, _| recon_merchant_account_update(state, req), - &auth::ReconAdmin, + |state, auth, req, _| recon::recon_merchant_account_update(state, auth, req), + &authentication::AdminApiAuthWithMerchantIdFromRoute(merchant_id), api_locking::LockAction::NotApplicable, )) .await @@ -50,8 +36,11 @@ pub async fn request_for_recon(state: web::Data, http_req: HttpRequest state, &http_req, (), - |state, user: UserFromToken, _req, _| send_recon_request(state, user), - &auth::DashboardNoPermissionAuth, + |state, user, _, _| recon::send_recon_request(state, user), + &authentication::JWTAuth { + permission: Permission::ReconAdmin, + minimum_entity_level: EntityType::Merchant, + }, api_locking::LockAction::NotApplicable, )) .await @@ -64,203 +53,12 @@ pub async fn get_recon_token(state: web::Data, req: HttpRequest) -> Ht state, &req, (), - |state, user: ReconUser, _, _| generate_recon_token(state, user), - &auth::ReconJWT, + |state, user, _, _| recon::generate_recon_token(state, user), + &authentication::JWTAuth { + permission: Permission::ReconAdmin, + minimum_entity_level: EntityType::Merchant, + }, api_locking::LockAction::NotApplicable, )) .await } - -pub async fn send_recon_request( - state: SessionState, - user: UserFromToken, -) -> RouterResponse { - let global_db = &*state.global_store; - let db = &*state.store; - let user_from_db = global_db - .find_user_by_id(&user.user_id) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; - let merchant_id = db - .find_user_role_by_user_id(&user.user_id, UserRoleVersion::V1) - .await - .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)? - .merchant_id - .ok_or(errors::ApiErrorResponse::InternalServerError)?; - let key_manager_state = &(&state).into(); - let key_store = db - .get_merchant_key_store_by_merchant_id( - key_manager_state, - &merchant_id, - &db.get_master_key().to_vec().into(), - ) - .await - .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - let merchant_account = db - .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) - .await - .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - - let email_contents = email_types::ProFeatureRequest { - feature_name: "RECONCILIATION & SETTLEMENT".to_string(), - merchant_id: merchant_id.clone(), - user_name: UserName::new(user_from_db.name) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to form username")?, - recipient_email: UserEmail::new(Secret::new("biz@hyperswitch.io".to_string())) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to convert recipient's email to UserEmail")?, - settings: state.conf.clone(), - subject: format!( - "Dashboard Pro Feature Request by {}", - user_from_db.email.expose().peek() - ), - }; - - let is_email_sent = state - .email_client - .compose_and_send_email( - Box::new(email_contents), - state.conf.proxy.https_url.as_ref(), - ) - .await - .change_context(UserErrors::InternalServerError) - .attach_printable("Failed to compose and send email for ProFeatureRequest") - .is_ok(); - - if is_email_sent { - let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate { - recon_status: enums::ReconStatus::Requested, - }; - - let response = db - .update_merchant( - key_manager_state, - merchant_account, - updated_merchant_account, - &key_store, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| { - format!("Failed while updating merchant's recon status: {merchant_id:?}") - })?; - - Ok(service_api::ApplicationResponse::Json( - recon_api::ReconStatusResponse { - recon_status: response.recon_status, - }, - )) - } else { - Ok(service_api::ApplicationResponse::Json( - recon_api::ReconStatusResponse { - recon_status: enums::ReconStatus::NotRequested, - }, - )) - } -} - -pub async fn recon_merchant_account_update( - state: SessionState, - req: recon_api::ReconUpdateMerchantRequest, -) -> RouterResponse { - let merchant_id = &req.merchant_id.clone(); - let user_email = &req.user_email.clone(); - - let db = &*state.store; - - let key_store = db - .get_merchant_key_store_by_merchant_id( - &(&state).into(), - &req.merchant_id, - &db.get_master_key().to_vec().into(), - ) - .await - .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - - let merchant_account = db - .find_merchant_account_by_merchant_id(&(&state).into(), merchant_id, &key_store) - .await - .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - - let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate { - recon_status: req.recon_status, - }; - - let response = db - .update_merchant( - &(&state).into(), - merchant_account, - updated_merchant_account, - &key_store, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| { - format!("Failed while updating merchant's recon status: {merchant_id:?}") - })?; - - let email_contents = email_types::ReconActivation { - recipient_email: UserEmail::from_pii_email(user_email.clone()) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to convert recipient's email to UserEmail from pii::Email")?, - user_name: UserName::new(Secret::new("HyperSwitch User".to_string())) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to form username")?, - settings: state.conf.clone(), - subject: "Approval of Recon Request - Access Granted to Recon Dashboard", - }; - - if req.recon_status == enums::ReconStatus::Active { - let _is_email_sent = state - .email_client - .compose_and_send_email( - Box::new(email_contents), - state.conf.proxy.https_url.as_ref(), - ) - .await - .change_context(UserErrors::InternalServerError) - .attach_printable("Failed to compose and send email for ReconActivation") - .is_ok(); - } - - Ok(service_api::ApplicationResponse::Json( - api_types::MerchantAccountResponse::foreign_try_from(response).change_context( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "merchant_account", - }, - )?, - )) -} - -pub async fn generate_recon_token( - state: SessionState, - req: ReconUser, -) -> RouterResponse { - let db = &*state.global_store; - let user = db - .find_user_by_id(&req.user_id) - .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::InvalidJwtToken) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - } - })? - .into(); - - let token = Box::pin(get_recon_auth_token(user, state)) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; - Ok(service_api::ApplicationResponse::Json( - recon_api::ReconTokenResponse { token }, - )) -} - -pub async fn get_recon_auth_token( - user: UserFromStorage, - state: SessionState, -) -> RouterResult> { - ReconToken::new_token(user.0.user_id.clone(), &state.conf).await -} diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 85850c39e3..34097b6bac 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -575,7 +575,7 @@ pub async fn verify_recon_token(state: web::Data, http_req: HttpReques &http_req, (), |state, user, _req, _| user_core::verify_token(state, user), - &auth::ReconJWT, + &auth::DashboardNoPermissionAuth, api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 8792f0c8d8..18bc1eb41c 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -11,8 +11,6 @@ pub mod jwt; pub mod kafka; pub mod logger; pub mod pm_auth; -#[cfg(feature = "recon")] -pub mod recon; #[cfg(feature = "olap")] pub mod openidconnect; diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 6d100ab357..5f9fc798d8 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -24,8 +24,6 @@ use self::detached::{ExtractedPayload, GetAuthType}; use super::authorization::{self, permissions::Permission}; #[cfg(feature = "olap")] use super::jwt; -#[cfg(feature = "recon")] -use super::recon::ReconToken; #[cfg(feature = "olap")] use crate::configs::Settings; #[cfg(feature = "olap")] @@ -34,8 +32,6 @@ use crate::consts; use crate::core::errors::UserResult; #[cfg(feature = "partial-auth")] use crate::core::metrics; -#[cfg(feature = "recon")] -use crate::routes::SessionState; use crate::{ core::{ api_keys, @@ -44,7 +40,7 @@ use crate::{ headers, routes::app::SessionStateInfo, services::api, - types::domain, + types::{domain, storage}, utils::OptionExt, }; @@ -69,6 +65,14 @@ pub struct AuthenticationDataWithMultipleProfiles { pub profile_id_list: Option>, } +#[derive(Clone, Debug)] +pub struct AuthenticationDataWithUser { + pub merchant_account: domain::MerchantAccount, + pub key_store: domain::MerchantKeyStore, + pub user: storage::User, + pub profile_id: Option, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde( tag = "api_auth_type", @@ -1980,12 +1984,13 @@ where default_auth } +#[derive(Clone)] #[cfg(feature = "recon")] -pub struct ReconAdmin; +pub struct UserFromTokenWithAuthData(pub UserFromToken, pub AuthenticationDataWithUser); -#[async_trait] #[cfg(feature = "recon")] -impl AuthenticateAndFetch<(), A> for ReconAdmin +#[async_trait] +impl AuthenticateAndFetch for JWTAuth where A: SessionStateInfo + Sync, { @@ -1993,49 +1998,68 @@ where &self, request_headers: &HeaderMap, state: &A, - ) -> RouterResult<((), AuthenticationType)> { - let request_admin_api_key = - get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; - let conf = state.conf(); - - let admin_api_key = conf.secrets.get_inner().recon_admin_api_key.peek(); - - if request_admin_api_key != admin_api_key { - Err(report!(errors::ApiErrorResponse::Unauthorized) - .attach_printable("Recon Admin Authentication Failure"))?; + ) -> RouterResult<(UserFromTokenWithAuthData, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.permission, &role_info)?; + authorization::check_entity(self.minimum_entity_level, &role_info)?; - Ok(((), AuthenticationType::NoAuth)) - } -} - -#[cfg(feature = "recon")] -pub struct ReconJWT; -#[cfg(feature = "recon")] -pub struct ReconUser { - pub user_id: String, -} -#[cfg(feature = "recon")] -impl AuthInfo for ReconUser { - fn get_merchant_id(&self) -> Option<&id_type::MerchantId> { - None - } -} -#[cfg(all(feature = "olap", feature = "recon"))] -#[async_trait] -impl AuthenticateAndFetch for ReconJWT { - async fn authenticate_and_fetch( - &self, - request_headers: &HeaderMap, - state: &SessionState, - ) -> RouterResult<(ReconUser, AuthenticationType)> { - let payload = parse_jwt_payload::(request_headers, state).await?; - - Ok(( - ReconUser { - user_id: payload.user_id, - }, - AuthenticationType::NoAuth, - )) + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + let user_id = payload.user_id; + + let user = state + .session_state() + .global_store + .find_user_by_id(&user_id) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch user for the user id")?; + + let auth = AuthenticationDataWithUser { + merchant_account: merchant, + key_store, + profile_id: payload.profile_id.clone(), + user, + }; + + let auth_type = AuthenticationType::MerchantJwt { + merchant_id: auth.merchant_account.get_id().clone(), + user_id: Some(user_id.clone()), + }; + + let user = UserFromToken { + user_id, + merchant_id: payload.merchant_id.clone(), + org_id: payload.org_id, + role_id: payload.role_id, + profile_id: payload.profile_id, + }; + + Ok((UserFromTokenWithAuthData(user, auth), auth_type)) } } diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs index ca5a668939..822035c792 100644 --- a/crates/router/src/services/authorization/info.rs +++ b/crates/router/src/services/authorization/info.rs @@ -42,6 +42,7 @@ pub enum PermissionModule { SurchargeDecisionManager, AccountCreate, Payouts, + Recon, } impl PermissionModule { @@ -59,7 +60,8 @@ impl PermissionModule { Self::ThreeDsDecisionManager => "View and configure 3DS decision rules configured for a merchant", Self::SurchargeDecisionManager =>"View and configure surcharge decision rules configured for a merchant", Self::AccountCreate => "Create new account within your organization", - Self::Payouts => "Everything related to payouts - like creating and viewing payout related information are within this module" + Self::Payouts => "Everything related to payouts - like creating and viewing payout related information are within this module", + Self::Recon => "Everything related to recon - raise requests for activating recon and generate recon auth tokens", } } } @@ -178,6 +180,11 @@ impl ModuleInfo { Permission::PayoutWrite, ]), }, + PermissionModule::Recon => Self { + module: module_name, + description, + permissions: get_permission_info_from_permissions(&[Permission::ReconAdmin]), + }, } } } @@ -215,6 +222,7 @@ fn get_group_description(group: PermissionGroup) -> &'static str { PermissionGroup::MerchantDetailsView => "View Merchant Details", PermissionGroup::MerchantDetailsManage => "Create, modify and delete Merchant Details like api keys, webhooks, etc", PermissionGroup::OrganizationManage => "Manage organization level tasks like create new Merchant accounts, Organization level roles, etc", + PermissionGroup::ReconOps => "View and manage reconciliation reports", } } @@ -233,6 +241,7 @@ pub fn get_parent_name(group: PermissionGroup) -> ParentGroup { ParentGroup::Merchant } PermissionGroup::OrganizationManage => ParentGroup::Organization, + PermissionGroup::ReconOps => ParentGroup::Recon, } } @@ -245,5 +254,6 @@ pub fn get_parent_group_description(group: ParentGroup) -> &'static str { ParentGroup::Users => "Manage and invite Users to the Team", ParentGroup::Merchant => "Create, modify and delete Merchant Details like api keys, webhooks, etc", ParentGroup::Organization =>"Manage organization level tasks like create new Merchant accounts, Organization level roles, etc", + ParentGroup::Recon => "View and manage reconciliation reports", } } diff --git a/crates/router/src/services/authorization/permission_groups.rs b/crates/router/src/services/authorization/permission_groups.rs index c31e3d32b1..aafc9cee94 100644 --- a/crates/router/src/services/authorization/permission_groups.rs +++ b/crates/router/src/services/authorization/permission_groups.rs @@ -16,6 +16,7 @@ pub fn get_permissions_vec(permission_group: &PermissionGroup) -> &[Permission] PermissionGroup::MerchantDetailsView => &MERCHANT_DETAILS_VIEW, PermissionGroup::MerchantDetailsManage => &MERCHANT_DETAILS_MANAGE, PermissionGroup::OrganizationManage => &ORGANIZATION_MANAGE, + PermissionGroup::ReconOps => &RECON, } } @@ -92,3 +93,5 @@ pub static ORGANIZATION_MANAGE: [Permission; 2] = [ Permission::MerchantAccountCreate, Permission::MerchantAccountRead, ]; + +pub static RECON: [Permission; 1] = [Permission::ReconAdmin]; diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs index 36ed89f4a4..2f0617557c 100644 --- a/crates/router/src/services/authorization/permissions.rs +++ b/crates/router/src/services/authorization/permissions.rs @@ -35,6 +35,7 @@ pub enum Permission { PayoutRead, PayoutWrite, GenerateReport, + ReconAdmin, } impl Permission { @@ -77,6 +78,7 @@ impl Permission { Self::PayoutRead => "View all payouts", Self::PayoutWrite => "Create payout, download payout data", Self::GenerateReport => "Generate reports for payments, refunds and disputes", + Self::ReconAdmin => "View and manage reconciliation reports", } } } diff --git a/crates/router/src/services/authorization/roles/predefined_roles.rs b/crates/router/src/services/authorization/roles/predefined_roles.rs index 9d55c5b2d1..8bac297e7c 100644 --- a/crates/router/src/services/authorization/roles/predefined_roles.rs +++ b/crates/router/src/services/authorization/roles/predefined_roles.rs @@ -24,6 +24,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsView, PermissionGroup::MerchantDetailsManage, PermissionGroup::OrganizationManage, + PermissionGroup::ReconOps, ], role_id: common_utils::consts::ROLE_ID_INTERNAL_ADMIN.to_string(), role_name: "internal_admin".to_string(), @@ -73,6 +74,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsView, PermissionGroup::MerchantDetailsManage, PermissionGroup::OrganizationManage, + PermissionGroup::ReconOps, ], role_id: common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), role_name: "organization_admin".to_string(), @@ -101,6 +103,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::UsersManage, PermissionGroup::MerchantDetailsView, PermissionGroup::MerchantDetailsManage, + PermissionGroup::ReconOps, ], role_id: consts::user_role::ROLE_ID_MERCHANT_ADMIN.to_string(), role_name: "admin".to_string(), diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 6ed03e1ea0..35bc9f06d9 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -6,7 +6,7 @@ use common_utils::{ }; use error_stack::ResultExt; use external_services::email::{EmailContents, EmailData, EmailError}; -use masking::{ExposeInterface, PeekInterface, Secret}; +use masking::{ExposeInterface, Secret}; use crate::{configs, consts, routes::SessionState}; #[cfg(feature = "olap")] @@ -454,6 +454,7 @@ pub struct ProFeatureRequest { pub feature_name: String, pub merchant_id: common_utils::id_type::MerchantId, pub user_name: domain::UserName, + pub user_email: domain::UserEmail, pub settings: std::sync::Arc, pub subject: String, } @@ -467,7 +468,7 @@ impl EmailData for ProFeatureRequest { user_name: self.user_name.clone().get_secret().expose(), feature_name: self.feature_name.clone(), merchant_id: self.merchant_id.clone(), - user_email: recipient.peek().to_string(), + user_email: self.user_email.clone().get_secret().expose(), }); Ok(EmailContents { diff --git a/crates/router/src/services/recon.rs b/crates/router/src/services/recon.rs deleted file mode 100644 index cc10fb7c7f..0000000000 --- a/crates/router/src/services/recon.rs +++ /dev/null @@ -1,29 +0,0 @@ -use error_stack::ResultExt; -use masking::Secret; - -use super::jwt; -use crate::{ - configs::Settings, - consts, - core::{self, errors::RouterResult}, -}; - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct ReconToken { - pub user_id: String, - pub exp: u64, -} - -impl ReconToken { - pub async fn new_token(user_id: String, settings: &Settings) -> RouterResult> { - let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); - let exp = jwt::generate_exp(exp_duration) - .change_context(core::errors::ApiErrorResponse::InternalServerError)? - .as_secs(); - let token_payload = Self { user_id, exp }; - let token = jwt::generate_jwt(&token_payload, settings) - .await - .change_context(core::errors::ApiErrorResponse::InternalServerError)?; - Ok(Secret::new(token)) - } -} diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 2a8e3563fb..8d6fb1a8ff 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -1045,6 +1045,7 @@ impl From for user_role_api::PermissionModule { info::PermissionModule::SurchargeDecisionManager => Self::SurchargeDecisionManager, info::PermissionModule::AccountCreate => Self::AccountCreate, info::PermissionModule::Payouts => Self::Payouts, + info::PermissionModule::Recon => Self::Recon, } } } diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index cf02d7766b..68bee0f022 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -54,6 +54,7 @@ impl From for user_role_api::Permission { Permission::PayoutRead => Self::PayoutRead, Permission::PayoutWrite => Self::PayoutWrite, Permission::GenerateReport => Self::GenerateReport, + Permission::ReconAdmin => Self::ReconAdmin, } } } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 6dd25f8fbd..11a404e7c2 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -364,3 +364,6 @@ global_tenant = { schema = "public", redis_key_prefix = "" } [multitenancy.tenants] public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"} + +[recipient_emails] +recon = "recon@example.com"