mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
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:
@ -446,3 +446,20 @@ pub enum StripeChargeType {
|
||||
pub fn convert_frm_connector(connector_name: &str) -> Option<FrmConnectors> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ApiEventsType> {
|
||||
@ -19,3 +22,11 @@ impl ApiEventMetric for ReconStatusResponse {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ApiEventsType> {
|
||||
Some(ApiEventsType::User {
|
||||
user_id: self.user_email.peek().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
common_utils::impl_api_event_type!(
|
||||
Miscellaneous,
|
||||
(
|
||||
|
||||
@ -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<String>,
|
||||
}
|
||||
|
||||
@ -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<Secret<String>>,
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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<recon_api::ReconStatusResponse> {
|
||||
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<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.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<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,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@ -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<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(
|
||||
state: SessionState,
|
||||
user_token: auth::UserFromToken,
|
||||
|
||||
@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
&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<AppState>, 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<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,
|
||||
))
|
||||
|
||||
@ -487,23 +487,6 @@ pub async fn verify_email_request(
|
||||
.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(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
|
||||
@ -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<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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
];
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<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(
|
||||
state: &SessionState,
|
||||
role_id: &str,
|
||||
|
||||
@ -28,7 +28,10 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = 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<HashMap<&'static str, RoleInfo>> = 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<HashMap<&'static str, RoleInfo>> = 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<HashMap<&'static str, RoleInfo>> = 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<HashMap<&'static str, RoleInfo>> = 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<HashMap<&'static str, RoleInfo>> = 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<HashMap<&'static str, RoleInfo>> = 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<HashMap<&'static str, RoleInfo>> = 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(),
|
||||
|
||||
@ -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<configs::Settings>,
|
||||
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<configs::Settings>,
|
||||
pub subject: String,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user