feat(recon): add merchant and profile IDs in auth tokens (#5643)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-09-06 19:55:40 +05:30
committed by GitHub
parent 36cd5c1c41
commit d9485a5f36
30 changed files with 339 additions and 339 deletions

View File

@ -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 = ""
connector_list = ""
[recipient_emails]
recon = "test@example.com"

View File

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

View File

@ -369,4 +369,4 @@ keys = "accept-language,user-agent"
sdk_eligible_payment_methods = "card"
[locker_based_open_banking_connectors]
connector_list = ""
connector_list = ""

View File

@ -382,4 +382,4 @@ keys = "accept-language,user-agent"
sdk_eligible_payment_methods = "card"
[locker_based_open_banking_connectors]
connector_list = ""
connector_list = ""

View File

@ -386,4 +386,4 @@ keys = "accept-language,user-agent"
sdk_eligible_payment_methods = "card"
[locker_based_open_banking_connectors]
connector_list = ""
connector_list = ""

View File

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

View File

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

View File

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

View File

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

View File

@ -2795,6 +2795,7 @@ pub enum PermissionGroup {
MerchantDetailsView,
MerchantDetailsManage,
OrganizationManage,
ReconOps,
}
/// Name of banks supported by Hyperswitch

View File

@ -252,17 +252,15 @@ impl SecretsHandler for settings::Secrets {
secret_management_client: &dyn SecretManagementInterface,
) -> CustomResult<SecretStateContainer<Self, RawSecret>, 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,
}
}

View File

@ -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<S: SecretState> {
pub user_auth_methods: SecretStateContainer<UserAuthMethodSettings, S>,
pub decision: Option<DecisionConfig>,
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<String>,
pub admin_api_key: Secret<String>,
pub recon_admin_api_key: Secret<String>,
pub master_enc_key: Secret<String>,
}
@ -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<K, V>(
value: HashMap<String, String>,
) -> Result<HashMap<K, HashSet<V>>, String>

View File

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

View File

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

View File

@ -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<recon_api::ReconStatusResponse> {
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<recon_api::ReconTokenResponse> {
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<api_types::MerchantAccountResponse> {
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",
})?,
))
}

View File

@ -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<user_api::VerifyTokenResponse> {
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,
}))
}

View File

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

View File

@ -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<AppState>,
req: HttpRequest,
path: web::Path<common_utils::id_type::MerchantId>,
json_payload: web::Json<recon_api::ReconUpdateMerchantRequest>,
) -> 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<AppState>, 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<AppState>, 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<recon_api::ReconStatusResponse> {
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<api_types::MerchantAccountResponse> {
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<recon_api::ReconTokenResponse> {
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<Secret<String>> {
ReconToken::new_token(user.0.user_id.clone(), &state.conf).await
}

View File

@ -575,7 +575,7 @@ pub async fn verify_recon_token(state: web::Data<AppState>, http_req: HttpReques
&http_req,
(),
|state, user, _req, _| user_core::verify_token(state, user),
&auth::ReconJWT,
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await

View File

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

View File

@ -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<Vec<id_type::ProfileId>>,
}
#[derive(Clone, Debug)]
pub struct AuthenticationDataWithUser {
pub merchant_account: domain::MerchantAccount,
pub key_store: domain::MerchantKeyStore,
pub user: storage::User,
pub profile_id: Option<id_type::ProfileId>,
}
#[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<A> AuthenticateAndFetch<(), A> for ReconAdmin
#[async_trait]
impl<A> AuthenticateAndFetch<UserFromTokenWithAuthData, A> 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::<A, AuthToken>(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<ReconUser, SessionState> for ReconJWT {
async fn authenticate_and_fetch(
&self,
request_headers: &HeaderMap,
state: &SessionState,
) -> RouterResult<(ReconUser, AuthenticationType)> {
let payload = parse_jwt_payload::<SessionState, ReconToken>(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))
}
}

View File

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

View File

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

View File

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

View File

@ -24,6 +24,7 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = 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<HashMap<&'static str, RoleInfo>> = 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<HashMap<&'static str, RoleInfo>> = 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(),

View File

@ -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<configs::Settings>,
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 {

View File

@ -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<Secret<String>> {
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))
}
}

View File

@ -1045,6 +1045,7 @@ impl From<info::PermissionModule> 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,
}
}
}

View File

@ -54,6 +54,7 @@ impl From<Permission> for user_role_api::Permission {
Permission::PayoutRead => Self::PayoutRead,
Permission::PayoutWrite => Self::PayoutWrite,
Permission::GenerateReport => Self::GenerateReport,
Permission::ReconAdmin => Self::ReconAdmin,
}
}
}

View File

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