feat(roles): Add caching for custom roles (#3946)

This commit is contained in:
Mani Chandra
2024-03-05 18:56:09 +05:30
committed by GitHub
parent f6f6a0c0f7
commit 19c502398f
12 changed files with 191 additions and 31 deletions

View File

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

View File

@ -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<bool>,
}
pub type AcceptInvitationResponse = DashboardEntryResponse;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct DeleteUserRoleRequest {
pub email: pii::Email,

View File

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

View File

@ -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<user_api::SwitchMerchantResponse> {
) -> UserResponse<user_api::DashboardEntryResponse> {
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)?,

View File

@ -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<user_role_api::AcceptInvitationResponse> {
) -> UserResponse<user_api::DashboardEntryResponse> {
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)?,
));

View File

@ -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<A: AppStateInfo>(
state: &A,
user_id: &str,

View File

@ -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<A>(
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 {
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<A>(
state: &A,
role_id: &str,
) -> RouterResult<Vec<permissions::Permission>>
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<Permission>")
.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<Vec<permissions::Permission>> {
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<A>(
state: &A,
role_id: &str,
merchant_id: &str,
org_id: &str,
) -> RouterResult<Vec<permissions::Permission>>
where
A: AppStateInfo + Sync,
{
state
.store()
.find_role_by_role_id_in_merchant_scope(
&token.role_id,
&token.merchant_id,
&token.org_id,
)
.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<A>(
state: &A,
role_id: &str,
permissions: &Vec<permissions::Permission>,
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<permissions::Permission> {
@ -62,3 +141,11 @@ pub fn check_authorization(
.into(),
)
}
fn get_redis_connection<A: AppStateInfo>(state: &A) -> RouterResult<Arc<RedisConnectionPool>> {
state
.store()
.get_redis_conn()
.change_context(ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get redis connection")
}

View File

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

View File

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

View File

@ -861,8 +861,11 @@ impl SignInWithSingleRoleStrategy {
async fn get_signin_response(self, state: &AppState) -> UserResult<user_api::SignInResponse> {
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,
))

View File

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

View File

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