feat: add resources and granular permission groups for reconciliation (#6591)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-12-04 14:58:10 +05:30
committed by GitHub
parent 35f963c2e8
commit fa21ef892d
18 changed files with 440 additions and 170 deletions

View File

@ -446,3 +446,20 @@ pub enum StripeChargeType {
pub fn convert_frm_connector(connector_name: &str) -> Option<FrmConnectors> { pub fn convert_frm_connector(connector_name: &str) -> Option<FrmConnectors> {
FrmConnectors::from_str(connector_name).ok() 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<PermissionScope> for ReconPermissionScope {
fn from(scope: PermissionScope) -> Self {
match scope {
PermissionScope::Read => Self::Read,
PermissionScope::Write => Self::Write,
}
}
}

View File

@ -1,6 +1,9 @@
use common_utils::events::{ApiEventMetric, ApiEventsType}; 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 { impl ApiEventMetric for ReconUpdateMerchantRequest {
fn get_api_event_type(&self) -> Option<ApiEventsType> { fn get_api_event_type(&self) -> Option<ApiEventsType> {
@ -19,3 +22,11 @@ impl ApiEventMetric for ReconStatusResponse {
Some(ApiEventsType::Recon) Some(ApiEventsType::Recon)
} }
} }
impl ApiEventMetric for VerifyTokenResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::User {
user_id: self.user_email.peek().to_string(),
})
}
}

View File

@ -1,11 +1,7 @@
use common_utils::events::{ApiEventMetric, ApiEventsType}; use common_utils::events::{ApiEventMetric, ApiEventsType};
#[cfg(feature = "recon")]
use masking::PeekInterface;
#[cfg(feature = "dummy_connector")] #[cfg(feature = "dummy_connector")]
use crate::user::sample_data::SampleDataRequest; use crate::user::sample_data::SampleDataRequest;
#[cfg(feature = "recon")]
use crate::user::VerifyTokenResponse;
use crate::user::{ use crate::user::{
dashboard_metadata::{ dashboard_metadata::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
@ -23,15 +19,6 @@ use crate::user::{
VerifyTotpRequest, VerifyTotpRequest,
}; };
#[cfg(feature = "recon")]
impl ApiEventMetric for VerifyTokenResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::User {
user_id: self.user_email.peek().to_string(),
})
}
}
common_utils::impl_api_event_type!( common_utils::impl_api_event_type!(
Miscellaneous, Miscellaneous,
( (

View File

@ -1,4 +1,4 @@
use common_utils::pii; use common_utils::{id_type, pii};
use masking::Secret; use masking::Secret;
use crate::enums; use crate::enums;
@ -18,3 +18,11 @@ pub struct ReconTokenResponse {
pub struct ReconStatusResponse { pub struct ReconStatusResponse {
pub recon_status: enums::ReconStatus, 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<String>,
}

View File

@ -167,13 +167,6 @@ pub struct SendVerifyEmailRequest {
pub email: pii::Email, 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)] #[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct UpdateUserAccountDetailsRequest { pub struct UpdateUserAccountDetailsRequest {
pub name: Option<Secret<String>>, pub name: Option<Secret<String>>,

View File

@ -2819,9 +2819,15 @@ pub enum PermissionGroup {
MerchantDetailsManage, MerchantDetailsManage,
// TODO: To be deprecated, make sure DB is migrated before removing // TODO: To be deprecated, make sure DB is migrated before removing
OrganizationManage, OrganizationManage,
ReconOps,
AccountView, AccountView,
AccountManage, 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)] #[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash, strum::EnumIter)]
@ -2831,7 +2837,8 @@ pub enum ParentGroup {
Workflows, Workflows,
Analytics, Analytics,
Users, Users,
Recon, ReconOps,
ReconReports,
Account, Account,
} }
@ -2854,7 +2861,13 @@ pub enum Resource {
WebhookEvent, WebhookEvent,
Payout, Payout,
Report, Report,
Recon, ReconToken,
ReconFiles,
ReconAndSettlementAnalytics,
ReconUpload,
ReconReports,
RunRecon,
ReconConfig,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)] #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)]

View File

@ -1,24 +1,37 @@
use api_models::recon as recon_api; use api_models::recon as recon_api;
#[cfg(feature = "email")]
use common_utils::ext_traits::AsyncExt; use common_utils::ext_traits::AsyncExt;
use error_stack::ResultExt; use error_stack::ResultExt;
#[cfg(feature = "email")]
use masking::{ExposeInterface, PeekInterface, Secret}; use masking::{ExposeInterface, PeekInterface, Secret};
#[cfg(feature = "email")]
use crate::{consts, services::email::types as email_types, types::domain};
use crate::{ use crate::{
consts, core::errors::{self, RouterResponse, UserErrors, UserResponse},
core::errors::{self, RouterResponse, UserErrors}, services::{api as service_api, authentication},
services::{api as service_api, authentication, email::types as email_types},
types::{ types::{
api::{self as api_types, enums}, api::{self as api_types, enums},
domain, storage, storage,
transformers::ForeignTryFrom, transformers::ForeignTryFrom,
}, },
SessionState, SessionState,
}; };
#[allow(unused_variables)]
pub async fn send_recon_request( pub async fn send_recon_request(
state: SessionState, state: SessionState,
auth_data: authentication::AuthenticationDataWithUser, auth_data: authentication::AuthenticationDataWithUser,
) -> RouterResponse<recon_api::ReconStatusResponse> { ) -> RouterResponse<recon_api::ReconStatusResponse> {
#[cfg(not(feature = "email"))]
return Ok(service_api::ApplicationResponse::Json(
recon_api::ReconStatusResponse {
recon_status: enums::ReconStatus::NotRequested,
},
));
#[cfg(feature = "email")]
{
let user_in_db = &auth_data.user; let user_in_db = &auth_data.user;
let merchant_id = auth_data.merchant_account.get_id().clone(); let merchant_id = auth_data.merchant_account.get_id().clone();
@ -37,14 +50,12 @@ pub async fn send_recon_request(
) )
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to convert recipient's email to UserEmail")?, .attach_printable("Failed to convert recipient's email to UserEmail")?,
settings: state.conf.clone(),
subject: format!( subject: format!(
"{} {}", "{} {}",
consts::EMAIL_SUBJECT_DASHBOARD_FEATURE_REQUEST, consts::EMAIL_SUBJECT_DASHBOARD_FEATURE_REQUEST,
user_email.expose().peek() user_email.expose().peek()
), ),
}; };
state state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
@ -81,20 +92,22 @@ pub async fn send_recon_request(
)) ))
}) })
.await .await
}
} }
pub async fn generate_recon_token( pub async fn generate_recon_token(
state: SessionState, state: SessionState,
user: authentication::UserFromToken, user_with_role: authentication::UserFromTokenWithRoleInfo,
) -> RouterResponse<recon_api::ReconTokenResponse> { ) -> RouterResponse<recon_api::ReconTokenResponse> {
let token = authentication::AuthToken::new_token( let user = user_with_role.user;
let token = authentication::ReconToken::new_token(
user.user_id.clone(), user.user_id.clone(),
user.merchant_id.clone(), user.merchant_id.clone(),
user.role_id.clone(),
&state.conf, &state.conf,
user.org_id.clone(), user.org_id.clone(),
user.profile_id.clone(), user.profile_id.clone(),
user.tenant_id, user.tenant_id,
user_with_role.role_info,
) )
.await .await
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
@ -138,18 +151,20 @@ pub async fn recon_merchant_account_update(
format!("Failed while updating merchant's recon status: {merchant_id:?}") format!("Failed while updating merchant's recon status: {merchant_id:?}")
})?; })?;
#[cfg(feature = "email")]
{
let user_email = &req.user_email.clone(); let user_email = &req.user_email.clone();
let email_contents = email_types::ReconActivation { let email_contents = email_types::ReconActivation {
recipient_email: domain::UserEmail::from_pii_email(user_email.clone()) recipient_email: domain::UserEmail::from_pii_email(user_email.clone())
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to convert recipient's email to UserEmail from pii::Email")?, .attach_printable(
"Failed to convert recipient's email to UserEmail from pii::Email",
)?,
user_name: domain::UserName::new(Secret::new("HyperSwitch User".to_string())) user_name: domain::UserName::new(Secret::new("HyperSwitch User".to_string()))
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to form username")?, .attach_printable("Failed to form username")?,
settings: state.conf.clone(),
subject: consts::EMAIL_SUBJECT_APPROVAL_RECON_REQUEST, subject: consts::EMAIL_SUBJECT_APPROVAL_RECON_REQUEST,
}; };
if req.recon_status == enums::ReconStatus::Active { if req.recon_status == enums::ReconStatus::Active {
let _ = state let _ = state
.email_client .email_client
@ -158,9 +173,15 @@ pub async fn recon_merchant_account_update(
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )
.await .await
.inspect_err(|err| {
router_env::logger::error!(
"Failed to compose and send email notifying them of recon activation: {}",
err
)
})
.change_context(UserErrors::InternalServerError) .change_context(UserErrors::InternalServerError)
.attach_printable("Failed to compose and send email for ReconActivation") .attach_printable("Failed to compose and send email for ReconActivation");
.is_ok(); }
} }
Ok(service_api::ApplicationResponse::Json( 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<recon_api::VerifyTokenResponse> {
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,
},
))
}

View File

@ -1593,27 +1593,6 @@ pub async fn send_verification_mail(
Ok(ApplicationResponse::StatusOk) Ok(ApplicationResponse::StatusOk)
} }
#[cfg(feature = "recon")]
pub async fn verify_token(
state: SessionState,
user: auth::UserFromToken,
) -> UserResponse<user_api::VerifyTokenResponse> {
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( pub async fn update_user_details(
state: SessionState, state: SessionState,
user_token: auth::UserFromToken, user_token: auth::UserFromToken,

View File

@ -1216,7 +1216,10 @@ impl Recon {
.service( .service(
web::resource("/request").route(web::post().to(recon_routes::request_for_recon)), 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)),
)
} }
} }

View File

@ -38,7 +38,7 @@ pub async fn request_for_recon(state: web::Data<AppState>, http_req: HttpRequest
(), (),
|state, user, _, _| recon::send_recon_request(state, user), |state, user, _, _| recon::send_recon_request(state, user),
&authentication::JWTAuth { &authentication::JWTAuth {
permission: Permission::MerchantReconWrite, permission: Permission::MerchantAccountWrite,
}, },
api_locking::LockAction::NotApplicable, api_locking::LockAction::NotApplicable,
)) ))
@ -54,7 +54,24 @@ pub async fn get_recon_token(state: web::Data<AppState>, req: HttpRequest) -> Ht
(), (),
|state, user, _, _| recon::generate_recon_token(state, user), |state, user, _, _| recon::generate_recon_token(state, user),
&authentication::JWTAuth { &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<AppState>, 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, api_locking::LockAction::NotApplicable,
)) ))

View File

@ -487,23 +487,6 @@ pub async fn verify_email_request(
.await .await
} }
#[cfg(feature = "recon")]
pub async fn verify_recon_token(state: web::Data<AppState>, 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( pub async fn update_user_account_details(
state: web::Data<AppState>, state: web::Data<AppState>,
req: HttpRequest, req: HttpRequest,

View File

@ -90,6 +90,12 @@ pub struct AuthenticationDataWithUser {
pub profile_id: id_type::ProfileId, 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)] #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde( #[serde(
tag = "api_auth_type", tag = "api_auth_type",
@ -3228,3 +3234,91 @@ where
Ok((auth, auth_type)) Ok((auth, auth_type))
} }
} }
#[cfg(feature = "recon")]
#[async_trait]
impl<A> AuthenticateAndFetch<UserFromTokenWithRoleInfo, A> 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::<A, AuthToken>(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<id_type::TenantId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub acl: Option<String>,
}
#[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<id_type::TenantId>,
role_info: authorization::roles::RoleInfo,
) -> UserResult<String> {
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
}
}

View File

@ -40,7 +40,10 @@ fn get_group_description(group: PermissionGroup) -> &'static str {
PermissionGroup::MerchantDetailsView | PermissionGroup::AccountView => "View Merchant Details", PermissionGroup::MerchantDetailsView | PermissionGroup::AccountView => "View Merchant Details",
PermissionGroup::MerchantDetailsManage | PermissionGroup::AccountManage => "Create, modify and delete Merchant Details like api keys, webhooks, etc", 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::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::Analytics => "View Analytics",
ParentGroup::Users => "Manage and invite Users to the Team", ParentGroup::Users => "Manage and invite Users to the Team",
ParentGroup::Account => "Create, modify and delete Merchant Details like api keys, webhooks, etc", 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",
} }
} }

View File

@ -21,7 +21,9 @@ impl PermissionGroupExt for PermissionGroup {
| Self::AnalyticsView | Self::AnalyticsView
| Self::UsersView | Self::UsersView
| Self::MerchantDetailsView | Self::MerchantDetailsView
| Self::AccountView => PermissionScope::Read, | Self::AccountView
| Self::ReconOpsView
| Self::ReconReportsView => PermissionScope::Read,
Self::OperationsManage Self::OperationsManage
| Self::ConnectorsManage | Self::ConnectorsManage
@ -29,8 +31,9 @@ impl PermissionGroupExt for PermissionGroup {
| Self::UsersManage | Self::UsersManage
| Self::MerchantDetailsManage | Self::MerchantDetailsManage
| Self::OrganizationManage | Self::OrganizationManage
| Self::ReconOps | Self::AccountManage
| Self::AccountManage => PermissionScope::Write, | Self::ReconOpsManage
| Self::ReconReportsManage => PermissionScope::Write,
} }
} }
@ -41,12 +44,13 @@ impl PermissionGroupExt for PermissionGroup {
Self::WorkflowsView | Self::WorkflowsManage => ParentGroup::Workflows, Self::WorkflowsView | Self::WorkflowsManage => ParentGroup::Workflows,
Self::AnalyticsView => ParentGroup::Analytics, Self::AnalyticsView => ParentGroup::Analytics,
Self::UsersView | Self::UsersManage => ParentGroup::Users, Self::UsersView | Self::UsersManage => ParentGroup::Users,
Self::ReconOps => ParentGroup::Recon,
Self::MerchantDetailsView Self::MerchantDetailsView
| Self::OrganizationManage | Self::OrganizationManage
| Self::MerchantDetailsManage | Self::MerchantDetailsManage
| Self::AccountView | Self::AccountView
| Self::AccountManage => ParentGroup::Account, | 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] 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::MerchantDetailsView => vec![Self::MerchantDetailsView],
Self::MerchantDetailsManage => { Self::MerchantDetailsManage => {
@ -108,7 +116,8 @@ impl ParentGroupExt for ParentGroup {
Self::Analytics => ANALYTICS.to_vec(), Self::Analytics => ANALYTICS.to_vec(),
Self::Users => USERS.to_vec(), Self::Users => USERS.to_vec(),
Self::Account => ACCOUNT.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 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,
];

View File

@ -67,8 +67,32 @@ generate_permissions! {
scopes: [Read, Write], scopes: [Read, Write],
entities: [Merchant] entities: [Merchant]
}, },
Recon: { ReconToken: {
scopes: [Write], 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] entities: [Merchant]
}, },
] ]
@ -91,7 +115,13 @@ pub fn get_resource_name(resource: &Resource, entity_type: &EntityType) -> &'sta
(Resource::Report, _) => "Operation Reports", (Resource::Report, _) => "Operation Reports",
(Resource::User, _) => "Users", (Resource::User, _) => "Users",
(Resource::WebhookEvent, _) => "Webhook Events", (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::Profile) => "Business Profile Account",
(Resource::Account, EntityType::Merchant) => "Merchant Account", (Resource::Account, EntityType::Merchant) => "Merchant Account",
(Resource::Account, EntityType::Organization) => "Organization Account", (Resource::Account, EntityType::Organization) => "Organization Account",

View File

@ -1,8 +1,14 @@
#[cfg(feature = "recon")]
use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
#[cfg(feature = "recon")]
use api_models::enums::ReconPermissionScope;
use common_enums::{EntityType, PermissionGroup, Resource, RoleScope}; use common_enums::{EntityType, PermissionGroup, Resource, RoleScope};
use common_utils::{errors::CustomResult, id_type}; 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 super::{permission_groups::PermissionGroupExt, permissions::Permission};
use crate::{core::errors, routes::SessionState}; use crate::{core::errors, routes::SessionState};
@ -78,6 +84,38 @@ impl RoleInfo {
}) })
} }
#[cfg(feature = "recon")]
pub fn get_recon_acl(&self) -> HashMap<Resource, ReconPermissionScope> {
let mut acl: HashMap<Resource, ReconPermissionScope> = 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( pub async fn from_role_id_in_merchant_scope(
state: &SessionState, state: &SessionState,
role_id: &str, role_id: &str,

View File

@ -28,7 +28,10 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
PermissionGroup::MerchantDetailsManage, PermissionGroup::MerchantDetailsManage,
PermissionGroup::AccountManage, PermissionGroup::AccountManage,
PermissionGroup::OrganizationManage, PermissionGroup::OrganizationManage,
PermissionGroup::ReconOps, PermissionGroup::ReconOpsView,
PermissionGroup::ReconOpsManage,
PermissionGroup::ReconReportsView,
PermissionGroup::ReconReportsManage,
], ],
role_id: common_utils::consts::ROLE_ID_INTERNAL_ADMIN.to_string(), role_id: common_utils::consts::ROLE_ID_INTERNAL_ADMIN.to_string(),
role_name: "internal_admin".to_string(), role_name: "internal_admin".to_string(),
@ -51,6 +54,8 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
PermissionGroup::UsersView, PermissionGroup::UsersView,
PermissionGroup::MerchantDetailsView, PermissionGroup::MerchantDetailsView,
PermissionGroup::AccountView, PermissionGroup::AccountView,
PermissionGroup::ReconOpsView,
PermissionGroup::ReconReportsView,
], ],
role_id: common_utils::consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), role_id: common_utils::consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(),
role_name: "internal_view_only".to_string(), role_name: "internal_view_only".to_string(),
@ -82,7 +87,10 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
PermissionGroup::MerchantDetailsManage, PermissionGroup::MerchantDetailsManage,
PermissionGroup::AccountManage, PermissionGroup::AccountManage,
PermissionGroup::OrganizationManage, PermissionGroup::OrganizationManage,
PermissionGroup::ReconOps, PermissionGroup::ReconOpsView,
PermissionGroup::ReconOpsManage,
PermissionGroup::ReconReportsView,
PermissionGroup::ReconReportsManage,
], ],
role_id: common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), role_id: common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
role_name: "organization_admin".to_string(), role_name: "organization_admin".to_string(),
@ -113,7 +121,10 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
PermissionGroup::AccountView, PermissionGroup::AccountView,
PermissionGroup::MerchantDetailsManage, PermissionGroup::MerchantDetailsManage,
PermissionGroup::AccountManage, PermissionGroup::AccountManage,
PermissionGroup::ReconOps, PermissionGroup::ReconOpsView,
PermissionGroup::ReconOpsManage,
PermissionGroup::ReconReportsView,
PermissionGroup::ReconReportsManage,
], ],
role_id: consts::user_role::ROLE_ID_MERCHANT_ADMIN.to_string(), role_id: consts::user_role::ROLE_ID_MERCHANT_ADMIN.to_string(),
role_name: "merchant_admin".to_string(), role_name: "merchant_admin".to_string(),
@ -136,6 +147,8 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
PermissionGroup::UsersView, PermissionGroup::UsersView,
PermissionGroup::MerchantDetailsView, PermissionGroup::MerchantDetailsView,
PermissionGroup::AccountView, PermissionGroup::AccountView,
PermissionGroup::ReconOpsView,
PermissionGroup::ReconReportsView,
], ],
role_id: consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY.to_string(), role_id: consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY.to_string(),
role_name: "merchant_view_only".to_string(), role_name: "merchant_view_only".to_string(),
@ -180,6 +193,8 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
PermissionGroup::AccountView, PermissionGroup::AccountView,
PermissionGroup::MerchantDetailsManage, PermissionGroup::MerchantDetailsManage,
PermissionGroup::AccountManage, PermissionGroup::AccountManage,
PermissionGroup::ReconOpsView,
PermissionGroup::ReconReportsView,
], ],
role_id: consts::user_role::ROLE_ID_MERCHANT_DEVELOPER.to_string(), role_id: consts::user_role::ROLE_ID_MERCHANT_DEVELOPER.to_string(),
role_name: "merchant_developer".to_string(), role_name: "merchant_developer".to_string(),
@ -203,6 +218,9 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
PermissionGroup::UsersView, PermissionGroup::UsersView,
PermissionGroup::MerchantDetailsView, PermissionGroup::MerchantDetailsView,
PermissionGroup::AccountView, PermissionGroup::AccountView,
PermissionGroup::ReconOpsView,
PermissionGroup::ReconOpsManage,
PermissionGroup::ReconReportsView,
], ],
role_id: consts::user_role::ROLE_ID_MERCHANT_OPERATOR.to_string(), role_id: consts::user_role::ROLE_ID_MERCHANT_OPERATOR.to_string(),
role_name: "merchant_operator".to_string(), role_name: "merchant_operator".to_string(),
@ -223,6 +241,8 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
PermissionGroup::UsersView, PermissionGroup::UsersView,
PermissionGroup::MerchantDetailsView, PermissionGroup::MerchantDetailsView,
PermissionGroup::AccountView, PermissionGroup::AccountView,
PermissionGroup::ReconOpsView,
PermissionGroup::ReconReportsView,
], ],
role_id: consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT.to_string(), role_id: consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT.to_string(),
role_name: "customer_support".to_string(), role_name: "customer_support".to_string(),

View File

@ -383,7 +383,6 @@ impl EmailData for InviteUser {
pub struct ReconActivation { pub struct ReconActivation {
pub recipient_email: domain::UserEmail, pub recipient_email: domain::UserEmail,
pub user_name: domain::UserName, pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::Settings>,
pub subject: &'static str, pub subject: &'static str,
} }
@ -458,7 +457,6 @@ pub struct ProFeatureRequest {
pub merchant_id: common_utils::id_type::MerchantId, pub merchant_id: common_utils::id_type::MerchantId,
pub user_name: domain::UserName, pub user_name: domain::UserName,
pub user_email: domain::UserEmail, pub user_email: domain::UserEmail,
pub settings: std::sync::Arc<configs::Settings>,
pub subject: String, pub subject: String,
} }