diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index c00afac646..4af3f855d7 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -446,3 +446,20 @@ pub enum StripeChargeType { pub fn convert_frm_connector(connector_name: &str) -> Option { FrmConnectors::from_str(connector_name).ok() } + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)] +pub enum ReconPermissionScope { + #[serde(rename = "R")] + Read = 0, + #[serde(rename = "RW")] + Write = 1, +} + +impl From for ReconPermissionScope { + fn from(scope: PermissionScope) -> Self { + match scope { + PermissionScope::Read => Self::Read, + PermissionScope::Write => Self::Write, + } + } +} diff --git a/crates/api_models/src/events/recon.rs b/crates/api_models/src/events/recon.rs index aed648f4c8..596b054128 100644 --- a/crates/api_models/src/events/recon.rs +++ b/crates/api_models/src/events/recon.rs @@ -1,6 +1,9 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; +use masking::PeekInterface; -use crate::recon::{ReconStatusResponse, ReconTokenResponse, ReconUpdateMerchantRequest}; +use crate::recon::{ + ReconStatusResponse, ReconTokenResponse, ReconUpdateMerchantRequest, VerifyTokenResponse, +}; impl ApiEventMetric for ReconUpdateMerchantRequest { fn get_api_event_type(&self) -> Option { @@ -19,3 +22,11 @@ impl ApiEventMetric for ReconStatusResponse { Some(ApiEventsType::Recon) } } + +impl ApiEventMetric for VerifyTokenResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::User { + user_id: self.user_email.peek().to_string(), + }) + } +} diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 4dc2a1a301..baac14e8af 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,11 +1,7 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; -#[cfg(feature = "recon")] -use masking::PeekInterface; #[cfg(feature = "dummy_connector")] use crate::user::sample_data::SampleDataRequest; -#[cfg(feature = "recon")] -use crate::user::VerifyTokenResponse; use crate::user::{ dashboard_metadata::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, @@ -23,15 +19,6 @@ use crate::user::{ VerifyTotpRequest, }; -#[cfg(feature = "recon")] -impl ApiEventMetric for VerifyTokenResponse { - fn get_api_event_type(&self) -> Option { - Some(ApiEventsType::User { - user_id: self.user_email.peek().to_string(), - }) - } -} - common_utils::impl_api_event_type!( Miscellaneous, ( diff --git a/crates/api_models/src/recon.rs b/crates/api_models/src/recon.rs index afee0fb562..f73bcc5ae1 100644 --- a/crates/api_models/src/recon.rs +++ b/crates/api_models/src/recon.rs @@ -1,4 +1,4 @@ -use common_utils::pii; +use common_utils::{id_type, pii}; use masking::Secret; use crate::enums; @@ -18,3 +18,11 @@ pub struct ReconTokenResponse { pub struct ReconStatusResponse { pub recon_status: enums::ReconStatus, } + +#[derive(serde::Serialize, Debug)] +pub struct VerifyTokenResponse { + pub merchant_id: id_type::MerchantId, + pub user_email: pii::Email, + #[serde(skip_serializing_if = "Option::is_none")] + pub acl: Option, +} diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 089426c68b..9c70ea895a 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -167,13 +167,6 @@ pub struct SendVerifyEmailRequest { pub email: pii::Email, } -#[cfg(feature = "recon")] -#[derive(serde::Serialize, Debug)] -pub struct VerifyTokenResponse { - pub merchant_id: id_type::MerchantId, - pub user_email: pii::Email, -} - #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct UpdateUserAccountDetailsRequest { pub name: Option>, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 781b5e3710..cb4281ee45 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2819,9 +2819,15 @@ pub enum PermissionGroup { MerchantDetailsManage, // TODO: To be deprecated, make sure DB is migrated before removing OrganizationManage, - ReconOps, AccountView, AccountManage, + ReconReportsView, + ReconReportsManage, + ReconOpsView, + // Alias is added for backward compatibility with database + // TODO: Remove alias post migration + #[serde(alias = "recon_ops")] + ReconOpsManage, } #[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash, strum::EnumIter)] @@ -2831,7 +2837,8 @@ pub enum ParentGroup { Workflows, Analytics, Users, - Recon, + ReconOps, + ReconReports, Account, } @@ -2854,7 +2861,13 @@ pub enum Resource { WebhookEvent, Payout, Report, - Recon, + ReconToken, + ReconFiles, + ReconAndSettlementAnalytics, + ReconUpload, + ReconReports, + RunRecon, + ReconConfig, } #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)] diff --git a/crates/router/src/core/recon.rs b/crates/router/src/core/recon.rs index 521c978e3c..13cf4c488e 100644 --- a/crates/router/src/core/recon.rs +++ b/crates/router/src/core/recon.rs @@ -1,100 +1,113 @@ use api_models::recon as recon_api; +#[cfg(feature = "email")] use common_utils::ext_traits::AsyncExt; use error_stack::ResultExt; +#[cfg(feature = "email")] use masking::{ExposeInterface, PeekInterface, Secret}; +#[cfg(feature = "email")] +use crate::{consts, services::email::types as email_types, types::domain}; use crate::{ - consts, - core::errors::{self, RouterResponse, UserErrors}, - services::{api as service_api, authentication, email::types as email_types}, + core::errors::{self, RouterResponse, UserErrors, UserResponse}, + services::{api as service_api, authentication}, types::{ api::{self as api_types, enums}, - domain, storage, + storage, transformers::ForeignTryFrom, }, SessionState, }; +#[allow(unused_variables)] pub async fn send_recon_request( state: SessionState, auth_data: authentication::AuthenticationDataWithUser, ) -> RouterResponse { - let user_in_db = &auth_data.user; - let merchant_id = auth_data.merchant_account.get_id().clone(); + #[cfg(not(feature = "email"))] + return Ok(service_api::ApplicationResponse::Json( + recon_api::ReconStatusResponse { + recon_status: enums::ReconStatus::NotRequested, + }, + )); - 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()) + #[cfg(feature = "email")] + { + let user_in_db = &auth_data.user; + let merchant_id = auth_data.merchant_account.get_id().clone(); + + 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.email.recon_recipient_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.email.recon_recipient_email.clone(), - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to convert recipient's email to UserEmail")?, - settings: state.conf.clone(), - subject: format!( - "{} {}", - consts::EMAIL_SUBJECT_DASHBOARD_FEATURE_REQUEST, - user_email.expose().peek() - ), - }; + subject: format!( + "{} {}", + consts::EMAIL_SUBJECT_DASHBOARD_FEATURE_REQUEST, + 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 updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate { + recon_status: enums::ReconStatus::Requested, + }; + let db = &*state.store; + let key_manager_state = &(&state).into(); - 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 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_data.merchant_account, + updated_merchant_account, + &auth_data.key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!("Failed while updating merchant's recon status: {merchant_id:?}") + })?; - let response = db - .update_merchant( - key_manager_state, - auth_data.merchant_account, - updated_merchant_account, - &auth_data.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 + 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, + user_with_role: authentication::UserFromTokenWithRoleInfo, ) -> RouterResponse { - let token = authentication::AuthToken::new_token( + let user = user_with_role.user; + let token = authentication::ReconToken::new_token( user.user_id.clone(), user.merchant_id.clone(), - user.role_id.clone(), &state.conf, user.org_id.clone(), user.profile_id.clone(), user.tenant_id, + user_with_role.role_info, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -138,29 +151,37 @@ pub async fn recon_merchant_account_update( 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: consts::EMAIL_SUBJECT_APPROVAL_RECON_REQUEST, - }; - - 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(); + #[cfg(feature = "email")] + { + 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")?, + subject: consts::EMAIL_SUBJECT_APPROVAL_RECON_REQUEST, + }; + 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 + .inspect_err(|err| { + router_env::logger::error!( + "Failed to compose and send email notifying them of recon activation: {}", + err + ) + }) + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to compose and send email for ReconActivation"); + } } Ok(service_api::ApplicationResponse::Json( @@ -170,3 +191,34 @@ pub async fn recon_merchant_account_update( })?, )) } + +pub async fn verify_recon_token( + state: SessionState, + user_with_role: authentication::UserFromTokenWithRoleInfo, +) -> UserResponse { + let user = user_with_role.user; + let user_in_db = user + .get_user_from_db(&state) + .await + .attach_printable_lazy(|| { + format!( + "Failed to fetch the user from DB for user_id - {}", + user.user_id + ) + })?; + + let acl = user_with_role.role_info.get_recon_acl(); + let optional_acl_str = serde_json::to_string(&acl) + .inspect_err(|err| router_env::logger::error!("Failed to serialize acl to string: {}", err)) + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to serialize acl to string. Using empty ACL") + .ok(); + + Ok(service_api::ApplicationResponse::Json( + recon_api::VerifyTokenResponse { + merchant_id: user.merchant_id.to_owned(), + user_email: user_in_db.0.email, + acl: optional_acl_str, + }, + )) +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 039e891e42..7ca4a127e8 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1593,27 +1593,6 @@ pub async fn send_verification_mail( Ok(ApplicationResponse::StatusOk) } -#[cfg(feature = "recon")] -pub async fn verify_token( - state: SessionState, - user: auth::UserFromToken, -) -> UserResponse { - let user_in_db = user - .get_user_from_db(&state) - .await - .attach_printable_lazy(|| { - format!( - "Failed to fetch the user from DB for user_id - {}", - user.user_id - ) - })?; - - Ok(ApplicationResponse::Json(user_api::VerifyTokenResponse { - merchant_id: user.merchant_id.to_owned(), - user_email: user_in_db.0.email, - })) -} - pub async fn update_user_details( state: SessionState, user_token: auth::UserFromToken, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index bb8d0d4f2b..3d1474ee81 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1216,7 +1216,10 @@ impl Recon { .service( web::resource("/request").route(web::post().to(recon_routes::request_for_recon)), ) - .service(web::resource("/verify_token").route(web::get().to(user::verify_recon_token))) + .service( + web::resource("/verify_token") + .route(web::get().to(recon_routes::verify_recon_token)), + ) } } diff --git a/crates/router/src/routes/recon.rs b/crates/router/src/routes/recon.rs index cdc2ae758e..cfd076c710 100644 --- a/crates/router/src/routes/recon.rs +++ b/crates/router/src/routes/recon.rs @@ -38,7 +38,7 @@ pub async fn request_for_recon(state: web::Data, http_req: HttpRequest (), |state, user, _, _| recon::send_recon_request(state, user), &authentication::JWTAuth { - permission: Permission::MerchantReconWrite, + permission: Permission::MerchantAccountWrite, }, api_locking::LockAction::NotApplicable, )) @@ -54,7 +54,24 @@ pub async fn get_recon_token(state: web::Data, req: HttpRequest) -> Ht (), |state, user, _, _| recon::generate_recon_token(state, user), &authentication::JWTAuth { - permission: Permission::MerchantReconWrite, + permission: Permission::MerchantReconTokenRead, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "recon")] +pub async fn verify_recon_token(state: web::Data, http_req: HttpRequest) -> HttpResponse { + let flow = Flow::ReconVerifyToken; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + (), + |state, user, _req, _| recon::verify_recon_token(state, user), + &authentication::JWTAuth { + permission: Permission::MerchantReconTokenRead, }, api_locking::LockAction::NotApplicable, )) diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 8fc0dad452..af55f7f305 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -487,23 +487,6 @@ pub async fn verify_email_request( .await } -#[cfg(feature = "recon")] -pub async fn verify_recon_token(state: web::Data, http_req: HttpRequest) -> HttpResponse { - let flow = Flow::ReconVerifyToken; - Box::pin(api::server_wrap( - flow, - state.clone(), - &http_req, - (), - |state, user, _req, _| user_core::verify_token(state, user), - &auth::JWTAuth { - permission: Permission::MerchantReconWrite, - }, - api_locking::LockAction::NotApplicable, - )) - .await -} - pub async fn update_user_account_details( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 1967eafd18..58253684a2 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -90,6 +90,12 @@ pub struct AuthenticationDataWithUser { pub profile_id: id_type::ProfileId, } +#[derive(Clone)] +pub struct UserFromTokenWithRoleInfo { + pub user: UserFromToken, + pub role_info: authorization::roles::RoleInfo, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde( tag = "api_auth_type", @@ -3228,3 +3234,91 @@ where Ok((auth, auth_type)) } } + +#[cfg(feature = "recon")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserFromTokenWithRoleInfo, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.permission, &role_info)?; + + let user = UserFromToken { + user_id: payload.user_id.clone(), + merchant_id: payload.merchant_id.clone(), + org_id: payload.org_id, + role_id: payload.role_id, + profile_id: payload.profile_id, + tenant_id: payload.tenant_id, + }; + + Ok(( + UserFromTokenWithRoleInfo { user, role_info }, + AuthenticationType::MerchantJwt { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + +#[cfg(feature = "recon")] +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ReconToken { + pub user_id: String, + pub merchant_id: id_type::MerchantId, + pub role_id: String, + pub exp: u64, + pub org_id: id_type::OrganizationId, + pub profile_id: id_type::ProfileId, + pub tenant_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub acl: Option, +} + +#[cfg(all(feature = "olap", feature = "recon"))] +impl ReconToken { + pub async fn new_token( + user_id: String, + merchant_id: id_type::MerchantId, + settings: &Settings, + org_id: id_type::OrganizationId, + profile_id: id_type::ProfileId, + tenant_id: Option, + role_info: authorization::roles::RoleInfo, + ) -> UserResult { + let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); + let exp = jwt::generate_exp(exp_duration)?.as_secs(); + let acl = role_info.get_recon_acl(); + let optional_acl_str = serde_json::to_string(&acl) + .inspect_err(|err| logger::error!("Failed to serialize acl to string: {}", err)) + .change_context(errors::UserErrors::InternalServerError) + .attach_printable("Failed to serialize acl to string. Using empty ACL") + .ok(); + let token_payload = Self { + user_id, + merchant_id, + role_id: role_info.get_role_id().to_string(), + exp, + org_id, + profile_id, + tenant_id, + acl: optional_acl_str, + }; + jwt::generate_jwt(&token_payload, settings).await + } +} diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs index bd987e2fe9..2d808a4377 100644 --- a/crates/router/src/services/authorization/info.rs +++ b/crates/router/src/services/authorization/info.rs @@ -40,7 +40,10 @@ fn get_group_description(group: PermissionGroup) -> &'static str { PermissionGroup::MerchantDetailsView | PermissionGroup::AccountView => "View Merchant Details", PermissionGroup::MerchantDetailsManage | PermissionGroup::AccountManage => "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", + PermissionGroup::ReconReportsView => "View and access reconciliation reports and analytics", + PermissionGroup::ReconReportsManage => "Manage reconciliation reports", + PermissionGroup::ReconOpsView => "View and access reconciliation operations", + PermissionGroup::ReconOpsManage => "Manage reconciliation operations", } } @@ -52,6 +55,7 @@ pub fn get_parent_group_description(group: ParentGroup) -> &'static str { ParentGroup::Analytics => "View Analytics", ParentGroup::Users => "Manage and invite Users to the Team", ParentGroup::Account => "Create, modify and delete Merchant Details like api keys, webhooks, etc", - ParentGroup::Recon => "View and manage reconciliation reports", + ParentGroup::ReconOps => "View, manage reconciliation operations like upload and process files, run reconciliation etc", + ParentGroup::ReconReports => "View, manage reconciliation reports and analytics", } } diff --git a/crates/router/src/services/authorization/permission_groups.rs b/crates/router/src/services/authorization/permission_groups.rs index 57c385565a..7ca8442c1c 100644 --- a/crates/router/src/services/authorization/permission_groups.rs +++ b/crates/router/src/services/authorization/permission_groups.rs @@ -21,7 +21,9 @@ impl PermissionGroupExt for PermissionGroup { | Self::AnalyticsView | Self::UsersView | Self::MerchantDetailsView - | Self::AccountView => PermissionScope::Read, + | Self::AccountView + | Self::ReconOpsView + | Self::ReconReportsView => PermissionScope::Read, Self::OperationsManage | Self::ConnectorsManage @@ -29,8 +31,9 @@ impl PermissionGroupExt for PermissionGroup { | Self::UsersManage | Self::MerchantDetailsManage | Self::OrganizationManage - | Self::ReconOps - | Self::AccountManage => PermissionScope::Write, + | Self::AccountManage + | Self::ReconOpsManage + | Self::ReconReportsManage => PermissionScope::Write, } } @@ -41,12 +44,13 @@ impl PermissionGroupExt for PermissionGroup { Self::WorkflowsView | Self::WorkflowsManage => ParentGroup::Workflows, Self::AnalyticsView => ParentGroup::Analytics, Self::UsersView | Self::UsersManage => ParentGroup::Users, - Self::ReconOps => ParentGroup::Recon, Self::MerchantDetailsView | Self::OrganizationManage | Self::MerchantDetailsManage | Self::AccountView | Self::AccountManage => ParentGroup::Account, + Self::ReconOpsView | Self::ReconOpsManage => ParentGroup::ReconOps, + Self::ReconReportsView | Self::ReconReportsManage => ParentGroup::ReconReports, } } @@ -76,7 +80,11 @@ impl PermissionGroupExt for PermissionGroup { vec![Self::UsersView, Self::UsersManage] } - Self::ReconOps => vec![Self::ReconOps], + Self::ReconOpsView => vec![Self::ReconOpsView], + Self::ReconOpsManage => vec![Self::ReconOpsView, Self::ReconOpsManage], + + Self::ReconReportsView => vec![Self::ReconReportsView], + Self::ReconReportsManage => vec![Self::ReconReportsView, Self::ReconReportsManage], Self::MerchantDetailsView => vec![Self::MerchantDetailsView], Self::MerchantDetailsManage => { @@ -108,7 +116,8 @@ impl ParentGroupExt for ParentGroup { Self::Analytics => ANALYTICS.to_vec(), Self::Users => USERS.to_vec(), Self::Account => ACCOUNT.to_vec(), - Self::Recon => RECON.to_vec(), + Self::ReconOps => RECON_OPS.to_vec(), + Self::ReconReports => RECON_REPORTS.to_vec(), } } @@ -167,4 +176,18 @@ pub static USERS: [Resource; 2] = [Resource::User, Resource::Account]; pub static ACCOUNT: [Resource; 3] = [Resource::Account, Resource::ApiKey, Resource::WebhookEvent]; -pub static RECON: [Resource; 1] = [Resource::Recon]; +pub static RECON_OPS: [Resource; 7] = [ + Resource::ReconToken, + Resource::ReconFiles, + Resource::ReconUpload, + Resource::RunRecon, + Resource::ReconConfig, + Resource::ReconAndSettlementAnalytics, + Resource::ReconReports, +]; + +pub static RECON_REPORTS: [Resource; 3] = [ + Resource::ReconToken, + Resource::ReconAndSettlementAnalytics, + Resource::ReconReports, +]; diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs index 6e472d5562..6f61200742 100644 --- a/crates/router/src/services/authorization/permissions.rs +++ b/crates/router/src/services/authorization/permissions.rs @@ -67,8 +67,32 @@ generate_permissions! { scopes: [Read, Write], entities: [Merchant] }, - Recon: { - scopes: [Write], + ReconToken: { + scopes: [Read], + entities: [Merchant] + }, + ReconFiles: { + scopes: [Read, Write], + entities: [Merchant] + }, + ReconAndSettlementAnalytics: { + scopes: [Read], + entities: [Merchant] + }, + ReconUpload: { + scopes: [Read, Write], + entities: [Merchant] + }, + ReconReports: { + scopes: [Read, Write], + entities: [Merchant] + }, + RunRecon: { + scopes: [Read, Write], + entities: [Merchant] + }, + ReconConfig: { + scopes: [Read, Write], entities: [Merchant] }, ] @@ -91,7 +115,13 @@ pub fn get_resource_name(resource: &Resource, entity_type: &EntityType) -> &'sta (Resource::Report, _) => "Operation Reports", (Resource::User, _) => "Users", (Resource::WebhookEvent, _) => "Webhook Events", - (Resource::Recon, _) => "Reconciliation Reports", + (Resource::ReconUpload, _) => "Reconciliation File Upload", + (Resource::RunRecon, _) => "Run Reconciliation Process", + (Resource::ReconConfig, _) => "Reconciliation Configurations", + (Resource::ReconToken, _) => "Generate & Verify Reconciliation Token", + (Resource::ReconFiles, _) => "Reconciliation Process Manager", + (Resource::ReconReports, _) => "Reconciliation Reports", + (Resource::ReconAndSettlementAnalytics, _) => "Reconciliation Analytics", (Resource::Account, EntityType::Profile) => "Business Profile Account", (Resource::Account, EntityType::Merchant) => "Merchant Account", (Resource::Account, EntityType::Organization) => "Organization Account", diff --git a/crates/router/src/services/authorization/roles.rs b/crates/router/src/services/authorization/roles.rs index bf66eb9246..f6c4f4b9ef 100644 --- a/crates/router/src/services/authorization/roles.rs +++ b/crates/router/src/services/authorization/roles.rs @@ -1,8 +1,14 @@ +#[cfg(feature = "recon")] +use std::collections::HashMap; use std::collections::HashSet; +#[cfg(feature = "recon")] +use api_models::enums::ReconPermissionScope; use common_enums::{EntityType, PermissionGroup, Resource, RoleScope}; use common_utils::{errors::CustomResult, id_type}; +#[cfg(feature = "recon")] +use super::permission_groups::{RECON_OPS, RECON_REPORTS}; use super::{permission_groups::PermissionGroupExt, permissions::Permission}; use crate::{core::errors, routes::SessionState}; @@ -78,6 +84,38 @@ impl RoleInfo { }) } + #[cfg(feature = "recon")] + pub fn get_recon_acl(&self) -> HashMap { + let mut acl: HashMap = HashMap::new(); + let mut recon_resources = RECON_OPS.to_vec(); + recon_resources.extend(RECON_REPORTS); + let recon_internal_resources = [Resource::ReconToken]; + self.get_permission_groups() + .iter() + .for_each(|permission_group| { + permission_group.resources().iter().for_each(|resource| { + if recon_resources.contains(resource) + && !recon_internal_resources.contains(resource) + { + let scope = match resource { + Resource::ReconAndSettlementAnalytics => ReconPermissionScope::Read, + _ => ReconPermissionScope::from(permission_group.scope()), + }; + acl.entry(*resource) + .and_modify(|curr_scope| { + *curr_scope = if (*curr_scope) < scope { + scope + } else { + *curr_scope + } + }) + .or_insert(scope); + } + }) + }); + acl + } + pub async fn from_role_id_in_merchant_scope( state: &SessionState, role_id: &str, diff --git a/crates/router/src/services/authorization/roles/predefined_roles.rs b/crates/router/src/services/authorization/roles/predefined_roles.rs index 39f6d47f82..9c67c12f52 100644 --- a/crates/router/src/services/authorization/roles/predefined_roles.rs +++ b/crates/router/src/services/authorization/roles/predefined_roles.rs @@ -28,7 +28,10 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, PermissionGroup::OrganizationManage, - PermissionGroup::ReconOps, + PermissionGroup::ReconOpsView, + PermissionGroup::ReconOpsManage, + PermissionGroup::ReconReportsView, + PermissionGroup::ReconReportsManage, ], role_id: common_utils::consts::ROLE_ID_INTERNAL_ADMIN.to_string(), role_name: "internal_admin".to_string(), @@ -51,6 +54,8 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::UsersView, PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, + PermissionGroup::ReconOpsView, + PermissionGroup::ReconReportsView, ], role_id: common_utils::consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), role_name: "internal_view_only".to_string(), @@ -82,7 +87,10 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, PermissionGroup::OrganizationManage, - PermissionGroup::ReconOps, + PermissionGroup::ReconOpsView, + PermissionGroup::ReconOpsManage, + PermissionGroup::ReconReportsView, + PermissionGroup::ReconReportsManage, ], role_id: common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), role_name: "organization_admin".to_string(), @@ -113,7 +121,10 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::AccountView, PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, - PermissionGroup::ReconOps, + PermissionGroup::ReconOpsView, + PermissionGroup::ReconOpsManage, + PermissionGroup::ReconReportsView, + PermissionGroup::ReconReportsManage, ], role_id: consts::user_role::ROLE_ID_MERCHANT_ADMIN.to_string(), role_name: "merchant_admin".to_string(), @@ -136,6 +147,8 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::UsersView, PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, + PermissionGroup::ReconOpsView, + PermissionGroup::ReconReportsView, ], role_id: consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY.to_string(), role_name: "merchant_view_only".to_string(), @@ -180,6 +193,8 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::AccountView, PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, + PermissionGroup::ReconOpsView, + PermissionGroup::ReconReportsView, ], role_id: consts::user_role::ROLE_ID_MERCHANT_DEVELOPER.to_string(), role_name: "merchant_developer".to_string(), @@ -203,6 +218,9 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::UsersView, PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, + PermissionGroup::ReconOpsView, + PermissionGroup::ReconOpsManage, + PermissionGroup::ReconReportsView, ], role_id: consts::user_role::ROLE_ID_MERCHANT_OPERATOR.to_string(), role_name: "merchant_operator".to_string(), @@ -223,6 +241,8 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::UsersView, PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, + PermissionGroup::ReconOpsView, + PermissionGroup::ReconReportsView, ], role_id: consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT.to_string(), role_name: "customer_support".to_string(), diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 8c966d79d8..66e730f082 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -383,7 +383,6 @@ impl EmailData for InviteUser { pub struct ReconActivation { pub recipient_email: domain::UserEmail, pub user_name: domain::UserName, - pub settings: std::sync::Arc, pub subject: &'static str, } @@ -458,7 +457,6 @@ pub struct ProFeatureRequest { 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, }