diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 8f77f72aad..1641c0ae53 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -128,8 +128,6 @@ pub struct SwitchMerchantIdRequest { pub merchant_id: String, } -pub type SwitchMerchantResponse = DashboardEntryResponse; - #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct CreateInternalUserRequest { pub name: Secret, diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 80d3861969..411bf823a2 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -1,8 +1,6 @@ use common_enums::PermissionGroup; use common_utils::pii; -use crate::user::DashboardEntryResponse; - pub mod role; #[derive(Debug, serde::Serialize)] @@ -99,8 +97,6 @@ pub struct AcceptInvitationRequest { pub need_dashboard_entry_response: Option, } -pub type AcceptInvitationResponse = DashboardEntryResponse; - #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct DeleteUserRoleRequest { pub email: pii::Email, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 72f160990e..87a16b73a8 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -78,6 +78,8 @@ pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day #[cfg(feature = "email")] pub const EMAIL_TOKEN_BLACKLIST_PREFIX: &str = "BET_"; +pub const ROLE_CACHE_PREFIX: &str = "CR_"; + #[cfg(feature = "olap")] pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; #[cfg(feature = "olap")] diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index b8b8cbd24c..fceee52805 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -94,6 +94,7 @@ pub async fn signup( ) .await?; let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; Ok(ApplicationResponse::Json( utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, @@ -121,6 +122,7 @@ pub async fn signin_without_invite_checks( let user_role = user_from_db.get_role_from_db(state.clone()).await?; let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; Ok(ApplicationResponse::Json( utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, @@ -966,6 +968,8 @@ pub async fn accept_invite_from_email( let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &update_status_result).await?; + utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &update_status_result) + .await; Ok(ApplicationResponse::Json( utils::user::get_dashboard_entry_response( @@ -1044,7 +1048,7 @@ pub async fn switch_merchant_id( state: AppState, request: user_api::SwitchMerchantIdRequest, user_from_token: auth::UserFromToken, -) -> UserResponse { +) -> UserResponse { if user_from_token.merchant_id == request.merchant_id { return Err(UserErrors::InvalidRoleOperationWithMessage( "User switching to same merchant id".to_string(), @@ -1093,13 +1097,14 @@ pub async fn switch_merchant_id( .organization_id; let token = utils::user::generate_jwt_auth_token_with_custom_role_attributes( - state, + &state, &user, request.merchant_id.clone(), - org_id, + org_id.clone(), user_from_token.role_id.clone(), ) .await?; + (token, user_from_token.role_id) } else { let user_roles = state @@ -1120,11 +1125,13 @@ pub async fn switch_merchant_id( .attach_printable("User doesn't have access to switch")?; let token = utils::user::generate_jwt_auth_token(&state, &user, user_role).await?; + utils::user_role::set_role_permissions_in_cache_by_user_role(&state, user_role).await; + (token, user_role.role_id.clone()) }; Ok(ApplicationResponse::Json( - user_api::SwitchMerchantResponse { + user_api::DashboardEntryResponse { token, name: user.get_name(), email: user.get_email(), @@ -1266,6 +1273,7 @@ pub async fn verify_email_without_invite_checks( .await .map_err(|e| logger::error!(?e)); let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; Ok(ApplicationResponse::Json( utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 9dca9b43d9..b81b980c07 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -159,6 +159,7 @@ pub async fn transfer_org_ownership( .to_not_found_response(UserErrors::InvalidRoleOperation)?; let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; Ok(ApplicationResponse::Json( utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, @@ -169,7 +170,7 @@ pub async fn accept_invitation( state: AppState, user_token: auth::UserWithoutMerchantFromToken, req: user_role_api::AcceptInvitationRequest, -) -> UserResponse { +) -> UserResponse { let user_role = futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async { state .store @@ -202,6 +203,8 @@ pub async fn accept_invitation( .into(); let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; + return Ok(ApplicationResponse::Json( utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, )); diff --git a/crates/router/src/services/authentication/blacklist.rs b/crates/router/src/services/authentication/blacklist.rs index 346e563ee3..2ee8302be6 100644 --- a/crates/router/src/services/authentication/blacklist.rs +++ b/crates/router/src/services/authentication/blacklist.rs @@ -17,6 +17,7 @@ use crate::{ use crate::{ core::errors::{UserErrors, UserResult}, routes::AppState, + services::authorization as authz, }; #[cfg(feature = "olap")] @@ -47,10 +48,23 @@ pub async fn insert_role_in_blacklist(state: &AppState, role_id: &str) -> UserRe date_time::now_unix_timestamp(), expiry, ) + .await + .change_context(UserErrors::InternalServerError)?; + invalidate_role_cache(state, role_id) .await .change_context(UserErrors::InternalServerError) } +#[cfg(feature = "olap")] +async fn invalidate_role_cache(state: &AppState, role_id: &str) -> RouterResult<()> { + let redis_conn = get_redis_connection(state)?; + redis_conn + .delete_key(authz::get_cache_key_from_role_id(role_id).as_str()) + .await + .map(|_| ()) + .change_context(ApiErrorResponse::InternalServerError) +} + pub async fn check_user_in_blacklist( state: &A, user_id: &str, diff --git a/crates/router/src/services/authorization.rs b/crates/router/src/services/authorization.rs index 2a516cc82d..dc6d82d4e9 100644 --- a/crates/router/src/services/authorization.rs +++ b/crates/router/src/services/authorization.rs @@ -1,7 +1,13 @@ +use std::sync::Arc; + use common_enums::PermissionGroup; +use error_stack::{IntoReport, ResultExt}; +use redis_interface::RedisConnectionPool; +use router_env::logger; use super::authentication::AuthToken; use crate::{ + consts, core::errors::{ApiErrorResponse, RouterResult, StorageErrorExt}, routes::app::AppStateInfo, }; @@ -19,22 +25,95 @@ pub async fn get_permissions( where A: AppStateInfo + Sync, { - if let Some(role_info) = roles::predefined_roles::PREDEFINED_ROLES.get(token.role_id.as_str()) { - Ok(get_permissions_from_groups( - role_info.get_permission_groups(), - )) - } else { - state - .store() - .find_role_by_role_id_in_merchant_scope( - &token.role_id, - &token.merchant_id, - &token.org_id, - ) - .await - .map(|role| get_permissions_from_groups(&role.groups)) - .to_not_found_response(ApiErrorResponse::InvalidJwtToken) + if let Some(permissions) = get_permissions_from_predefined_roles(&token.role_id) { + return Ok(permissions); } + + if let Ok(permissions) = get_permissions_from_cache(state, &token.role_id) + .await + .map_err(|e| logger::error!("Failed to get permissions from cache {}", e)) + { + return Ok(permissions); + } + + let permissions = + get_permissions_from_db(state, &token.role_id, &token.merchant_id, &token.org_id).await?; + + let token_expiry: i64 = token + .exp + .try_into() + .into_report() + .change_context(ApiErrorResponse::InternalServerError)?; + let cache_ttl = token_expiry - common_utils::date_time::now_unix_timestamp(); + + set_permissions_in_cache(state, &token.role_id, &permissions, cache_ttl) + .await + .map_err(|e| logger::error!("Failed to set permissions in cache {}", e)) + .ok(); + Ok(permissions) +} + +async fn get_permissions_from_cache( + state: &A, + role_id: &str, +) -> RouterResult> +where + A: AppStateInfo + Sync, +{ + let redis_conn = get_redis_connection(state)?; + + redis_conn + .get_and_deserialize_key(&get_cache_key_from_role_id(role_id), "Vec") + .await + .change_context(ApiErrorResponse::InternalServerError) +} + +pub fn get_cache_key_from_role_id(role_id: &str) -> String { + format!("{}{}", consts::ROLE_CACHE_PREFIX, role_id) +} + +fn get_permissions_from_predefined_roles(role_id: &str) -> Option> { + roles::predefined_roles::PREDEFINED_ROLES + .get(role_id) + .map(|role_info| get_permissions_from_groups(role_info.get_permission_groups())) +} + +async fn get_permissions_from_db( + state: &A, + role_id: &str, + merchant_id: &str, + org_id: &str, +) -> RouterResult> +where + A: AppStateInfo + Sync, +{ + state + .store() + .find_role_by_role_id_in_merchant_scope(role_id, merchant_id, org_id) + .await + .map(|role| get_permissions_from_groups(&role.groups)) + .to_not_found_response(ApiErrorResponse::InvalidJwtToken) +} + +pub async fn set_permissions_in_cache( + state: &A, + role_id: &str, + permissions: &Vec, + expiry: i64, +) -> RouterResult<()> +where + A: AppStateInfo + Sync, +{ + let redis_conn = get_redis_connection(state)?; + + redis_conn + .serialize_and_set_key_with_expiry( + &get_cache_key_from_role_id(role_id), + permissions, + expiry, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) } pub fn get_permissions_from_groups(groups: &[PermissionGroup]) -> Vec { @@ -62,3 +141,11 @@ pub fn check_authorization( .into(), ) } + +fn get_redis_connection(state: &A) -> RouterResult> { + state + .store() + .get_redis_conn() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection") +} diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs index d6a20b7f8e..7b82e84d64 100644 --- a/crates/router/src/services/authorization/info.rs +++ b/crates/router/src/services/authorization/info.rs @@ -187,7 +187,7 @@ fn get_group_description(group: PermissionGroup) -> &'static str { "View Payments, Refunds, Mandates, Disputes and Customers" } PermissionGroup::OperationsManage => { - "Create,modify and delete Payments, Refunds, Mandates, Disputes and Customers" + "Create, modify and delete Payments, Refunds, Mandates, Disputes and Customers" } PermissionGroup::ConnectorsView => { "View connected Payment Processors, Payout Processors and Fraud & Risk Manager details" diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs index 8d436618b3..edf9a40915 100644 --- a/crates/router/src/services/authorization/permissions.rs +++ b/crates/router/src/services/authorization/permissions.rs @@ -1,6 +1,8 @@ use strum::Display; -#[derive(PartialEq, Display, Clone, Debug, Copy, Eq, Hash)] +#[derive( + PartialEq, Display, Clone, Debug, Copy, Eq, Hash, serde::Deserialize, serde::Serialize, +)] pub enum Permission { PaymentRead, PaymentWrite, diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index d6e878456e..1850eb4b81 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -861,8 +861,11 @@ impl SignInWithSingleRoleStrategy { async fn get_signin_response(self, state: &AppState) -> UserResult { let token = utils::user::generate_jwt_auth_token(state, &self.user, &self.user_role).await?; + utils::user_role::set_role_permissions_in_cache_by_user_role(state, &self.user_role).await; + let dashboard_entry_response = utils::user::get_dashboard_entry_response(state, self.user, self.user_role, token)?; + Ok(user_api::SignInResponse::DashboardEntry( dashboard_entry_response, )) diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index c3b795d1a5..cfab331f14 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -87,7 +87,7 @@ pub async fn generate_jwt_auth_token( } pub async fn generate_jwt_auth_token_with_custom_role_attributes( - state: AppState, + state: &AppState, user: &UserFromStorage, merchant_id: String, org_id: String, diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index bb83b82f2d..a0ac140058 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -1,11 +1,14 @@ use api_models::user_role as user_role_api; use common_enums::PermissionGroup; -use error_stack::ResultExt; +use diesel_models::user_role::UserRole; +use error_stack::{IntoReport, ResultExt}; +use router_env::logger; use crate::{ + consts, core::errors::{UserErrors, UserResult}, routes::AppState, - services::authorization::{permissions::Permission, roles}, + services::authorization::{self as authz, permissions::Permission, roles}, types::domain, }; @@ -83,3 +86,47 @@ pub async fn validate_role_name( Ok(()) } + +pub async fn set_role_permissions_in_cache_by_user_role( + state: &AppState, + user_role: &UserRole, +) -> bool { + set_role_permissions_in_cache_if_required( + state, + user_role.role_id.as_str(), + user_role.merchant_id.as_str(), + user_role.org_id.as_str(), + ) + .await + .map_err(|e| logger::error!("Error setting permissions in cache {:?}", e)) + .is_ok() +} + +pub async fn set_role_permissions_in_cache_if_required( + state: &AppState, + role_id: &str, + merchant_id: &str, + org_id: &str, +) -> UserResult<()> { + if roles::predefined_roles::PREDEFINED_ROLES.contains_key(role_id) { + return Ok(()); + } + + let role_info = roles::RoleInfo::from_role_id(state, role_id, merchant_id, org_id) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error getting role_info from role_id")?; + + authz::set_permissions_in_cache( + state, + role_id, + &role_info.get_permissions_set().into_iter().collect(), + consts::JWT_TOKEN_TIME_IN_SECS + .try_into() + .into_report() + .change_context(UserErrors::InternalServerError)?, + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error setting permissions in redis") +}