mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 10:06:32 +08:00 
			
		
		
		
	feat(authz): Add custom role checks in authorization (#3719)
Co-authored-by: Apoorv Dixit <apoorv.dixit@juspay.in> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
		| @ -1,3 +1,4 @@ | ||||
| use common_enums::RoleScope; | ||||
| use common_utils::pii; | ||||
|  | ||||
| use crate::user::DashboardEntryResponse; | ||||
| @ -7,9 +8,10 @@ pub struct ListRolesResponse(pub Vec<RoleInfoResponse>); | ||||
|  | ||||
| #[derive(Debug, serde::Serialize)] | ||||
| pub struct RoleInfoResponse { | ||||
|     pub role_id: &'static str, | ||||
|     pub role_id: String, | ||||
|     pub permissions: Vec<Permission>, | ||||
|     pub role_name: &'static str, | ||||
|     pub role_name: String, | ||||
|     pub role_scope: RoleScope, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, serde::Deserialize, serde::Serialize)] | ||||
|  | ||||
| @ -497,7 +497,7 @@ pub mod routes { | ||||
|                 .await | ||||
|                 .map(ApplicationResponse::Json) | ||||
|             }, | ||||
|             &auth::JWTAuth(Permission::Analytics), | ||||
|             &auth::JWTAuth(Permission::PaymentWrite), | ||||
|             api_locking::LockAction::NotApplicable, | ||||
|         )) | ||||
|         .await | ||||
|  | ||||
| @ -18,10 +18,8 @@ use crate::services::email::types as email_types; | ||||
| use crate::{ | ||||
|     consts, | ||||
|     routes::AppState, | ||||
|     services::{ | ||||
|         authentication as auth, authorization::predefined_permissions, ApplicationResponse, | ||||
|     }, | ||||
|     types::domain, | ||||
|     services::{authentication as auth, authorization::roles, ApplicationResponse}, | ||||
|     types::{domain, transformers::ForeignInto}, | ||||
|     utils, | ||||
| }; | ||||
| pub mod dashboard_metadata; | ||||
| @ -444,7 +442,16 @@ pub async fn invite_user( | ||||
|         .into()); | ||||
|     } | ||||
|  | ||||
|     if !predefined_permissions::is_role_invitable(request.role_id.as_str())? { | ||||
|     let role_info = roles::get_role_info_from_role_id( | ||||
|         &state, | ||||
|         &request.role_id, | ||||
|         &user_from_token.merchant_id, | ||||
|         &user_from_token.org_id, | ||||
|     ) | ||||
|     .await | ||||
|     .to_not_found_response(UserErrors::InvalidRoleId)?; | ||||
|  | ||||
|     if !role_info.is_invitable() { | ||||
|         return Err(UserErrors::InvalidRoleId.into()) | ||||
|             .attach_printable(format!("role_id = {} is not invitable", request.role_id)); | ||||
|     } | ||||
| @ -652,7 +659,16 @@ async fn handle_invitation( | ||||
|         .into()); | ||||
|     } | ||||
|  | ||||
|     if !predefined_permissions::is_role_invitable(request.role_id.as_str())? { | ||||
|     let role_info = roles::get_role_info_from_role_id( | ||||
|         state, | ||||
|         &request.role_id, | ||||
|         &user_from_token.merchant_id, | ||||
|         &user_from_token.org_id, | ||||
|     ) | ||||
|     .await | ||||
|     .to_not_found_response(UserErrors::InvalidRoleId)?; | ||||
|  | ||||
|     if !role_info.is_invitable() { | ||||
|         return Err(UserErrors::InvalidRoleId.into()) | ||||
|             .attach_printable(format!("role_id = {} is not invitable", request.role_id)); | ||||
|     } | ||||
| @ -1030,20 +1046,18 @@ pub async fn switch_merchant_id( | ||||
|         .into()); | ||||
|     } | ||||
|  | ||||
|     let user_roles = state | ||||
|         .store | ||||
|         .list_user_roles_by_user_id(&user_from_token.user_id) | ||||
|         .await | ||||
|         .change_context(UserErrors::InternalServerError)?; | ||||
|  | ||||
|     let active_user_roles = user_roles | ||||
|         .into_iter() | ||||
|         .filter(|role| role.status == UserStatus::Active) | ||||
|         .collect::<Vec<_>>(); | ||||
|  | ||||
|     let user = user_from_token.get_user_from_db(&state).await?; | ||||
|  | ||||
|     let (token, role_id) = if utils::user_role::is_internal_role(&user_from_token.role_id) { | ||||
|     let role_info = roles::get_role_info_from_role_id( | ||||
|         &state, | ||||
|         &user_from_token.role_id, | ||||
|         &user_from_token.merchant_id, | ||||
|         &user_from_token.org_id, | ||||
|     ) | ||||
|     .await | ||||
|     .to_not_found_response(UserErrors::InternalServerError)?; | ||||
|  | ||||
|     let (token, role_id) = if role_info.is_internal() { | ||||
|         let key_store = state | ||||
|             .store | ||||
|             .get_merchant_key_store_by_merchant_id( | ||||
| @ -1082,6 +1096,17 @@ pub async fn switch_merchant_id( | ||||
|         .await?; | ||||
|         (token, user_from_token.role_id) | ||||
|     } else { | ||||
|         let user_roles = state | ||||
|             .store | ||||
|             .list_user_roles_by_user_id(&user_from_token.user_id) | ||||
|             .await | ||||
|             .change_context(UserErrors::InternalServerError)?; | ||||
|  | ||||
|         let active_user_roles = user_roles | ||||
|             .into_iter() | ||||
|             .filter(|role| role.status == UserStatus::Active) | ||||
|             .collect::<Vec<_>>(); | ||||
|  | ||||
|         let user_role = active_user_roles | ||||
|             .iter() | ||||
|             .find(|role| role.merchant_id == request.merchant_id) | ||||
| @ -1166,17 +1191,47 @@ pub async fn get_users_for_merchant_account( | ||||
|     state: AppState, | ||||
|     user_from_token: auth::UserFromToken, | ||||
| ) -> UserResponse<user_api::GetUsersResponse> { | ||||
|     let users = state | ||||
|     let users_and_user_roles = state | ||||
|         .store | ||||
|         .find_users_and_roles_by_merchant_id(user_from_token.merchant_id.as_str()) | ||||
|         .await | ||||
|         .change_context(UserErrors::InternalServerError) | ||||
|         .attach_printable("No users for given merchant id")? | ||||
|         .attach_printable("No users for given merchant id")?; | ||||
|  | ||||
|     let users_user_roles_and_roles = | ||||
|         futures::future::try_join_all(users_and_user_roles.into_iter().map( | ||||
|             |(user, user_role)| async { | ||||
|                 roles::get_role_info_from_role_id( | ||||
|                     &state, | ||||
|                     &user_role.role_id, | ||||
|                     &user_role.merchant_id, | ||||
|                     &user_role.org_id, | ||||
|                 ) | ||||
|                 .await | ||||
|                 .map(|role_info| (user, user_role, role_info)) | ||||
|                 .to_not_found_response(UserErrors::InternalServerError) | ||||
|             }, | ||||
|         )) | ||||
|         .await?; | ||||
|  | ||||
|     let user_details_vec = users_user_roles_and_roles | ||||
|         .into_iter() | ||||
|         .filter_map(|(user, role)| domain::UserAndRoleJoined(user, role).try_into().ok()) | ||||
|         .map(|(user, user_role, role_info)| { | ||||
|             let user = domain::UserFromStorage::from(user); | ||||
|             user_api::UserDetails { | ||||
|                 email: user.get_email(), | ||||
|                 name: user.get_name(), | ||||
|                 role_id: user_role.role_id, | ||||
|                 role_name: role_info.get_role_name().to_string(), | ||||
|                 status: user_role.status.foreign_into(), | ||||
|                 last_modified_at: user_role.last_modified, | ||||
|             } | ||||
|         }) | ||||
|         .collect(); | ||||
|  | ||||
|     Ok(ApplicationResponse::Json(user_api::GetUsersResponse(users))) | ||||
|     Ok(ApplicationResponse::Json(user_api::GetUsersResponse( | ||||
|         user_details_vec, | ||||
|     ))) | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "email")] | ||||
|  | ||||
| @ -10,7 +10,7 @@ use crate::{ | ||||
|     routes::AppState, | ||||
|     services::{ | ||||
|         authentication::{self as auth}, | ||||
|         authorization::{info, predefined_permissions}, | ||||
|         authorization::{info, roles}, | ||||
|         ApplicationResponse, | ||||
|     }, | ||||
|     types::domain, | ||||
| @ -30,58 +30,96 @@ pub async fn get_authorization_info( | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub async fn list_roles(_state: AppState) -> UserResponse<user_role_api::ListRolesResponse> { | ||||
| pub async fn list_invitable_roles( | ||||
|     state: AppState, | ||||
|     user_from_token: auth::UserFromToken, | ||||
| ) -> UserResponse<user_role_api::ListRolesResponse> { | ||||
|     let predefined_roles_map = roles::predefined_roles::PREDEFINED_ROLES | ||||
|         .iter() | ||||
|         .filter(|(_, role_info)| role_info.is_invitable()) | ||||
|         .map(|(role_id, role_info)| user_role_api::RoleInfoResponse { | ||||
|             permissions: role_info | ||||
|                 .get_permissions_set() | ||||
|                 .into_iter() | ||||
|                 .map(Into::into) | ||||
|                 .collect(), | ||||
|             role_id: role_id.to_string(), | ||||
|             role_name: role_info.get_role_name().to_string(), | ||||
|             role_scope: role_info.get_scope(), | ||||
|         }); | ||||
|  | ||||
|     let custom_roles_map = state | ||||
|         .store | ||||
|         .list_all_roles(&user_from_token.merchant_id, &user_from_token.org_id) | ||||
|         .await | ||||
|         .change_context(UserErrors::InternalServerError)? | ||||
|         .into_iter() | ||||
|         .map(roles::RoleInfo::from) | ||||
|         .filter(|role_info| role_info.is_invitable()) | ||||
|         .map(|role_info| user_role_api::RoleInfoResponse { | ||||
|             permissions: role_info | ||||
|                 .get_permissions_set() | ||||
|                 .into_iter() | ||||
|                 .map(Into::into) | ||||
|                 .collect(), | ||||
|             role_id: role_info.get_role_id().to_string(), | ||||
|             role_name: role_info.get_role_name().to_string(), | ||||
|             role_scope: role_info.get_scope(), | ||||
|         }); | ||||
|  | ||||
|     Ok(ApplicationResponse::Json(user_role_api::ListRolesResponse( | ||||
|         predefined_permissions::PREDEFINED_PERMISSIONS | ||||
|             .iter() | ||||
|             .filter(|(_, role_info)| role_info.is_invitable()) | ||||
|             .filter_map(|(role_id, role_info)| { | ||||
|                 utils::user_role::get_role_name_and_permission_response(role_info).map( | ||||
|                     |(permissions, role_name)| user_role_api::RoleInfoResponse { | ||||
|                         permissions, | ||||
|                         role_id, | ||||
|                         role_name, | ||||
|                     }, | ||||
|                 ) | ||||
|             }) | ||||
|             .collect(), | ||||
|         predefined_roles_map.chain(custom_roles_map).collect(), | ||||
|     ))) | ||||
| } | ||||
|  | ||||
| pub async fn get_role( | ||||
|     _state: AppState, | ||||
|     state: AppState, | ||||
|     user_from_token: auth::UserFromToken, | ||||
|     role: user_role_api::GetRoleRequest, | ||||
| ) -> UserResponse<user_role_api::RoleInfoResponse> { | ||||
|     let info = predefined_permissions::PREDEFINED_PERMISSIONS | ||||
|         .get_key_value(role.role_id.as_str()) | ||||
|         .and_then(|(role_id, role_info)| { | ||||
|             utils::user_role::get_role_name_and_permission_response(role_info).map( | ||||
|                 |(permissions, role_name)| user_role_api::RoleInfoResponse { | ||||
|                     permissions, | ||||
|                     role_id, | ||||
|                     role_name, | ||||
|                 }, | ||||
|             ) | ||||
|         }) | ||||
|         .ok_or(UserErrors::InvalidRoleId)?; | ||||
|     let role_info = roles::get_role_info_from_role_id( | ||||
|         &state, | ||||
|         &role.role_id, | ||||
|         &user_from_token.merchant_id, | ||||
|         &user_from_token.org_id, | ||||
|     ) | ||||
|     .await | ||||
|     .to_not_found_response(UserErrors::InvalidRoleId)?; | ||||
|  | ||||
|     Ok(ApplicationResponse::Json(info)) | ||||
|     if role_info.is_internal() { | ||||
|         return Err(UserErrors::InvalidRoleId.into()); | ||||
|     } | ||||
|  | ||||
|     let permissions = role_info | ||||
|         .get_permissions_set() | ||||
|         .into_iter() | ||||
|         .map(Into::into) | ||||
|         .collect(); | ||||
|  | ||||
|     Ok(ApplicationResponse::Json(user_role_api::RoleInfoResponse { | ||||
|         permissions, | ||||
|         role_id: role.role_id, | ||||
|         role_name: role_info.get_role_name().to_string(), | ||||
|         role_scope: role_info.get_scope(), | ||||
|     })) | ||||
| } | ||||
|  | ||||
| pub async fn get_role_from_token( | ||||
|     _state: AppState, | ||||
|     user: auth::UserFromToken, | ||||
|     state: AppState, | ||||
|     user_from_token: auth::UserFromToken, | ||||
| ) -> UserResponse<Vec<user_role_api::Permission>> { | ||||
|     Ok(ApplicationResponse::Json( | ||||
|         predefined_permissions::PREDEFINED_PERMISSIONS | ||||
|             .get(user.role_id.as_str()) | ||||
|             .ok_or(UserErrors::InternalServerError.into()) | ||||
|             .attach_printable("Invalid Role Id in JWT")? | ||||
|             .get_permissions() | ||||
|             .iter() | ||||
|             .map(|&per| per.into()) | ||||
|             .collect(), | ||||
|     )) | ||||
|     let role_info = user_from_token | ||||
|         .get_role_info_from_db(&state) | ||||
|         .await | ||||
|         .attach_printable("Invalid role_id in JWT")?; | ||||
|  | ||||
|     let permissions = role_info | ||||
|         .get_permissions_set() | ||||
|         .into_iter() | ||||
|         .map(Into::into) | ||||
|         .collect(); | ||||
|  | ||||
|     Ok(ApplicationResponse::Json(permissions)) | ||||
| } | ||||
|  | ||||
| pub async fn update_user_role( | ||||
| @ -89,7 +127,16 @@ pub async fn update_user_role( | ||||
|     user_from_token: auth::UserFromToken, | ||||
|     req: user_role_api::UpdateUserRoleRequest, | ||||
| ) -> UserResponse<()> { | ||||
|     if !predefined_permissions::is_role_updatable(&req.role_id)? { | ||||
|     let role_info = roles::get_role_info_from_role_id( | ||||
|         &state, | ||||
|         &req.role_id, | ||||
|         &user_from_token.merchant_id, | ||||
|         &user_from_token.org_id, | ||||
|     ) | ||||
|     .await | ||||
|     .to_not_found_response(UserErrors::InvalidRoleId)?; | ||||
|  | ||||
|     if !role_info.is_updatable() { | ||||
|         return Err(UserErrors::InvalidRoleOperation.into()) | ||||
|             .attach_printable(format!("User role cannot be updated to {}", req.role_id)); | ||||
|     } | ||||
| @ -110,10 +157,19 @@ pub async fn update_user_role( | ||||
|         .await | ||||
|         .to_not_found_response(UserErrors::InvalidRoleOperation)?; | ||||
|  | ||||
|     if !predefined_permissions::is_role_updatable(&user_role_to_be_updated.role_id)? { | ||||
|     let role_to_be_updated = roles::get_role_info_from_role_id( | ||||
|         &state, | ||||
|         &user_role_to_be_updated.role_id, | ||||
|         &user_from_token.merchant_id, | ||||
|         &user_from_token.org_id, | ||||
|     ) | ||||
|     .await | ||||
|     .change_context(UserErrors::InternalServerError)?; | ||||
|  | ||||
|     if !role_to_be_updated.is_updatable() { | ||||
|         return Err(UserErrors::InvalidRoleOperation.into()).attach_printable(format!( | ||||
|             "User role cannot be updated from {}", | ||||
|             user_role_to_be_updated.role_id | ||||
|             role_to_be_updated.get_role_id() | ||||
|         )); | ||||
|     } | ||||
|  | ||||
| @ -270,7 +326,15 @@ pub async fn delete_user_role( | ||||
|         .find(|&role| role.merchant_id == user_from_token.merchant_id.as_str()) | ||||
|     { | ||||
|         Some(user_role) => { | ||||
|             if !predefined_permissions::is_role_deletable(&user_role.role_id)? { | ||||
|             let role_info = roles::get_role_info_from_role_id( | ||||
|                 &state, | ||||
|                 &user_role.role_id, | ||||
|                 &user_from_token.merchant_id, | ||||
|                 &user_from_token.org_id, | ||||
|             ) | ||||
|             .await | ||||
|             .change_context(UserErrors::InternalServerError)?; | ||||
|             if !role_info.is_deletable() { | ||||
|                 return Err(UserErrors::InvalidDeleteOperation.into()) | ||||
|                     .attach_printable(format!("role_id = {} is not deletable", user_role.role_id)); | ||||
|             } | ||||
|  | ||||
| @ -291,7 +291,7 @@ pub async fn delete_sample_data( | ||||
|         &http_req, | ||||
|         payload.into_inner(), | ||||
|         sample_data::delete_sample_data_for_user, | ||||
|         &auth::JWTAuth(Permission::PaymentWrite), | ||||
|         &auth::JWTAuth(Permission::MerchantAccountWrite), | ||||
|         api_locking::LockAction::NotApplicable, | ||||
|     )) | ||||
|     .await | ||||
|  | ||||
| @ -36,7 +36,7 @@ pub async fn list_all_roles(state: web::Data<AppState>, req: HttpRequest) -> Htt | ||||
|         state.clone(), | ||||
|         &req, | ||||
|         (), | ||||
|         |state, _: (), _| user_role_core::list_roles(state), | ||||
|         |state, user, _| user_role_core::list_invitable_roles(state, user), | ||||
|         &auth::JWTAuth(Permission::UsersRead), | ||||
|         api_locking::LockAction::NotApplicable, | ||||
|     )) | ||||
| @ -57,7 +57,7 @@ pub async fn get_role( | ||||
|         state.clone(), | ||||
|         &req, | ||||
|         request_payload, | ||||
|         |state, _: (), req| user_role_core::get_role(state, req), | ||||
|         user_role_core::get_role, | ||||
|         &auth::JWTAuth(Permission::UsersRead), | ||||
|         api_locking::LockAction::NotApplicable, | ||||
|     )) | ||||
|  | ||||
| @ -503,8 +503,8 @@ where | ||||
|             return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); | ||||
|         } | ||||
|  | ||||
|         let permissions = authorization::get_permissions(&payload.role_id)?; | ||||
|         authorization::check_authorization(&self.0, permissions)?; | ||||
|         let permissions = authorization::get_permissions(state, &payload).await?; | ||||
|         authorization::check_authorization(&self.0, &permissions)?; | ||||
|  | ||||
|         Ok(( | ||||
|             (), | ||||
| @ -532,8 +532,8 @@ where | ||||
|             return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); | ||||
|         } | ||||
|  | ||||
|         let permissions = authorization::get_permissions(&payload.role_id)?; | ||||
|         authorization::check_authorization(&self.0, permissions)?; | ||||
|         let permissions = authorization::get_permissions(state, &payload).await?; | ||||
|         authorization::check_authorization(&self.0, &permissions)?; | ||||
|  | ||||
|         Ok(( | ||||
|             UserFromToken { | ||||
| @ -570,8 +570,8 @@ where | ||||
|             return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); | ||||
|         } | ||||
|  | ||||
|         let permissions = authorization::get_permissions(&payload.role_id)?; | ||||
|         authorization::check_authorization(&self.required_permission, permissions)?; | ||||
|         let permissions = authorization::get_permissions(state, &payload).await?; | ||||
|         authorization::check_authorization(&self.required_permission, &permissions)?; | ||||
|  | ||||
|         // Check if token has access to MerchantId that has been requested through query param | ||||
|         if payload.merchant_id != self.merchant_id { | ||||
| @ -613,8 +613,8 @@ where | ||||
|             return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); | ||||
|         } | ||||
|  | ||||
|         let permissions = authorization::get_permissions(&payload.role_id)?; | ||||
|         authorization::check_authorization(&self.0, permissions)?; | ||||
|         let permissions = authorization::get_permissions(state, &payload).await?; | ||||
|         authorization::check_authorization(&self.0, &permissions)?; | ||||
|  | ||||
|         let key_store = state | ||||
|             .store() | ||||
| @ -663,8 +663,8 @@ where | ||||
|             return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); | ||||
|         } | ||||
|  | ||||
|         let permissions = authorization::get_permissions(&payload.role_id)?; | ||||
|         authorization::check_authorization(&self.0, permissions)?; | ||||
|         let permissions = authorization::get_permissions(state, &payload).await?; | ||||
|         authorization::check_authorization(&self.0, &permissions)?; | ||||
|  | ||||
|         let key_store = state | ||||
|             .store() | ||||
|  | ||||
| @ -1,14 +1,50 @@ | ||||
| use crate::core::errors::{ApiErrorResponse, RouterResult}; | ||||
| use common_enums::PermissionGroup; | ||||
|  | ||||
| use super::authentication::AuthToken; | ||||
| use crate::{ | ||||
|     core::errors::{ApiErrorResponse, RouterResult, StorageErrorExt}, | ||||
|     routes::app::AppStateInfo, | ||||
| }; | ||||
|  | ||||
| pub mod info; | ||||
| pub mod permission_groups; | ||||
| pub mod permissions; | ||||
| pub mod predefined_permissions; | ||||
| pub mod roles; | ||||
|  | ||||
| pub fn get_permissions(role: &str) -> RouterResult<&Vec<permissions::Permission>> { | ||||
|     predefined_permissions::PREDEFINED_PERMISSIONS | ||||
|         .get(role) | ||||
|         .map(|role_info| role_info.get_permissions()) | ||||
|         .ok_or(ApiErrorResponse::InvalidJwtToken.into()) | ||||
| pub async fn get_permissions<A>( | ||||
|     state: &A, | ||||
|     token: &AuthToken, | ||||
| ) -> RouterResult<Vec<permissions::Permission>> | ||||
| 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) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn get_permissions_from_groups(groups: &[PermissionGroup]) -> Vec<permissions::Permission> { | ||||
|     groups | ||||
|         .iter() | ||||
|         .flat_map(|group| { | ||||
|             permission_groups::get_permissions_vec(group) | ||||
|                 .iter() | ||||
|                 .cloned() | ||||
|         }) | ||||
|         .collect() | ||||
| } | ||||
|  | ||||
| pub fn check_authorization( | ||||
|  | ||||
| @ -0,0 +1,86 @@ | ||||
| use common_enums::PermissionGroup; | ||||
|  | ||||
| use super::permissions::Permission; | ||||
|  | ||||
| pub fn get_permissions_vec(permission_group: &PermissionGroup) -> &[Permission] { | ||||
|     match permission_group { | ||||
|         PermissionGroup::OperationsView => &OPERATIONS_VIEW, | ||||
|         PermissionGroup::OperationsManage => &OPERATIONS_MANAGE, | ||||
|         PermissionGroup::ConnectorsView => &CONNECTORS_VIEW, | ||||
|         PermissionGroup::ConnectorsManage => &CONNECTORS_MANAGE, | ||||
|         PermissionGroup::WorkflowsView => &WORKFLOWS_VIEW, | ||||
|         PermissionGroup::WorkflowsManage => &WORKFLOWS_MANAGE, | ||||
|         PermissionGroup::AnalyticsView => &ANALYTICS_VIEW, | ||||
|         PermissionGroup::UsersView => &USERS_VIEW, | ||||
|         PermissionGroup::UsersManage => &USERS_MANAGE, | ||||
|         PermissionGroup::MerchantDetailsView => &MERCHANT_DETAILS_VIEW, | ||||
|         PermissionGroup::MerchantDetailsManage => &MERCHANT_DETAILS_MANAGE, | ||||
|         PermissionGroup::OrganizationManage => &ORGANIZATION_MANAGE, | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub static OPERATIONS_VIEW: [Permission; 6] = [ | ||||
|     Permission::PaymentRead, | ||||
|     Permission::RefundRead, | ||||
|     Permission::MandateRead, | ||||
|     Permission::DisputeRead, | ||||
|     Permission::CustomerRead, | ||||
|     Permission::MerchantAccountRead, | ||||
| ]; | ||||
|  | ||||
| pub static OPERATIONS_MANAGE: [Permission; 6] = [ | ||||
|     Permission::PaymentWrite, | ||||
|     Permission::RefundWrite, | ||||
|     Permission::MandateWrite, | ||||
|     Permission::DisputeWrite, | ||||
|     Permission::CustomerWrite, | ||||
|     Permission::MerchantAccountRead, | ||||
| ]; | ||||
|  | ||||
| pub static CONNECTORS_VIEW: [Permission; 2] = [ | ||||
|     Permission::MerchantConnectorAccountRead, | ||||
|     Permission::MerchantAccountRead, | ||||
| ]; | ||||
|  | ||||
| pub static CONNECTORS_MANAGE: [Permission; 2] = [ | ||||
|     Permission::MerchantConnectorAccountWrite, | ||||
|     Permission::MerchantAccountRead, | ||||
| ]; | ||||
|  | ||||
| pub static WORKFLOWS_VIEW: [Permission; 5] = [ | ||||
|     Permission::RoutingRead, | ||||
|     Permission::ThreeDsDecisionManagerRead, | ||||
|     Permission::SurchargeDecisionManagerRead, | ||||
|     Permission::MerchantConnectorAccountRead, | ||||
|     Permission::MerchantAccountRead, | ||||
| ]; | ||||
|  | ||||
| pub static WORKFLOWS_MANAGE: [Permission; 5] = [ | ||||
|     Permission::RoutingWrite, | ||||
|     Permission::ThreeDsDecisionManagerWrite, | ||||
|     Permission::SurchargeDecisionManagerWrite, | ||||
|     Permission::MerchantConnectorAccountRead, | ||||
|     Permission::MerchantAccountRead, | ||||
| ]; | ||||
|  | ||||
| pub static ANALYTICS_VIEW: [Permission; 2] = | ||||
|     [Permission::Analytics, Permission::MerchantAccountRead]; | ||||
|  | ||||
| pub static USERS_VIEW: [Permission; 2] = [Permission::UsersRead, Permission::MerchantAccountRead]; | ||||
|  | ||||
| pub static USERS_MANAGE: [Permission; 2] = | ||||
|     [Permission::UsersWrite, Permission::MerchantAccountRead]; | ||||
|  | ||||
| pub static MERCHANT_DETAILS_VIEW: [Permission; 1] = [Permission::MerchantAccountRead]; | ||||
|  | ||||
| pub static MERCHANT_DETAILS_MANAGE: [Permission; 4] = [ | ||||
|     Permission::MerchantAccountWrite, | ||||
|     Permission::ApiKeyRead, | ||||
|     Permission::ApiKeyWrite, | ||||
|     Permission::MerchantAccountRead, | ||||
| ]; | ||||
|  | ||||
| pub static ORGANIZATION_MANAGE: [Permission; 2] = [ | ||||
|     Permission::MerchantAccountCreate, | ||||
|     Permission::MerchantAccountRead, | ||||
| ]; | ||||
| @ -1,6 +1,6 @@ | ||||
| use strum::Display; | ||||
|  | ||||
| #[derive(PartialEq, Display, Clone, Debug, Copy)] | ||||
| #[derive(PartialEq, Display, Clone, Debug, Copy, Eq, Hash)] | ||||
| pub enum Permission { | ||||
|     PaymentRead, | ||||
|     PaymentWrite, | ||||
|  | ||||
							
								
								
									
										100
									
								
								crates/router/src/services/authorization/roles.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								crates/router/src/services/authorization/roles.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| use std::collections::HashSet; | ||||
|  | ||||
| use common_enums::{PermissionGroup, RoleScope}; | ||||
| use common_utils::errors::CustomResult; | ||||
|  | ||||
| use super::{permission_groups::get_permissions_vec, permissions::Permission}; | ||||
| use crate::{core::errors, routes::AppState}; | ||||
|  | ||||
| pub mod predefined_roles; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| pub struct RoleInfo { | ||||
|     role_id: String, | ||||
|     role_name: String, | ||||
|     groups: Vec<PermissionGroup>, | ||||
|     scope: RoleScope, | ||||
|     is_invitable: bool, | ||||
|     is_deletable: bool, | ||||
|     is_updatable: bool, | ||||
|     is_internal: bool, | ||||
| } | ||||
|  | ||||
| impl RoleInfo { | ||||
|     pub fn get_role_id(&self) -> &str { | ||||
|         &self.role_id | ||||
|     } | ||||
|  | ||||
|     pub fn get_role_name(&self) -> &str { | ||||
|         &self.role_name | ||||
|     } | ||||
|  | ||||
|     pub fn get_permission_groups(&self) -> &Vec<PermissionGroup> { | ||||
|         &self.groups | ||||
|     } | ||||
|  | ||||
|     pub fn get_scope(&self) -> RoleScope { | ||||
|         self.scope | ||||
|     } | ||||
|  | ||||
|     pub fn is_invitable(&self) -> bool { | ||||
|         self.is_invitable | ||||
|     } | ||||
|  | ||||
|     pub fn is_deletable(&self) -> bool { | ||||
|         self.is_deletable | ||||
|     } | ||||
|  | ||||
|     pub fn is_internal(&self) -> bool { | ||||
|         self.is_internal | ||||
|     } | ||||
|  | ||||
|     pub fn is_updatable(&self) -> bool { | ||||
|         self.is_updatable | ||||
|     } | ||||
|  | ||||
|     pub fn get_permissions_set(&self) -> HashSet<Permission> { | ||||
|         self.groups | ||||
|             .iter() | ||||
|             .flat_map(|group| get_permissions_vec(group).iter().copied()) | ||||
|             .collect() | ||||
|     } | ||||
|  | ||||
|     pub fn check_permission_exists(&self, required_permission: &Permission) -> bool { | ||||
|         self.groups | ||||
|             .iter() | ||||
|             .any(|group| get_permissions_vec(group).contains(required_permission)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn get_role_info_from_role_id( | ||||
|     state: &AppState, | ||||
|     role_id: &str, | ||||
|     merchant_id: &str, | ||||
|     org_id: &str, | ||||
| ) -> CustomResult<RoleInfo, errors::StorageError> { | ||||
|     if let Some(role) = predefined_roles::PREDEFINED_ROLES.get(role_id) { | ||||
|         Ok(role.clone()) | ||||
|     } else { | ||||
|         state | ||||
|             .store | ||||
|             .find_role_by_role_id_in_merchant_scope(role_id, merchant_id, org_id) | ||||
|             .await | ||||
|             .map(RoleInfo::from) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<diesel_models::role::Role> for RoleInfo { | ||||
|     fn from(role: diesel_models::role::Role) -> Self { | ||||
|         Self { | ||||
|             role_id: role.role_id, | ||||
|             role_name: role.role_name, | ||||
|             groups: role.groups.into_iter().map(Into::into).collect(), | ||||
|             scope: role.scope, | ||||
|             is_invitable: true, | ||||
|             is_deletable: true, | ||||
|             is_updatable: true, | ||||
|             is_internal: false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,210 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use common_enums::{PermissionGroup, RoleScope}; | ||||
| use once_cell::sync::Lazy; | ||||
|  | ||||
| use super::RoleInfo; | ||||
| use crate::consts; | ||||
|  | ||||
| pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|| { | ||||
|     let mut roles = HashMap::new(); | ||||
|     roles.insert( | ||||
|         consts::user_role::ROLE_ID_INTERNAL_ADMIN, | ||||
|         RoleInfo { | ||||
|             groups: vec![ | ||||
|                 PermissionGroup::OperationsView, | ||||
|                 PermissionGroup::OperationsManage, | ||||
|                 PermissionGroup::ConnectorsView, | ||||
|                 PermissionGroup::ConnectorsManage, | ||||
|                 PermissionGroup::WorkflowsView, | ||||
|                 PermissionGroup::WorkflowsManage, | ||||
|                 PermissionGroup::AnalyticsView, | ||||
|                 PermissionGroup::UsersView, | ||||
|                 PermissionGroup::UsersManage, | ||||
|                 PermissionGroup::MerchantDetailsView, | ||||
|                 PermissionGroup::MerchantDetailsManage, | ||||
|                 PermissionGroup::OrganizationManage, | ||||
|             ], | ||||
|             role_id: consts::user_role::ROLE_ID_INTERNAL_ADMIN.to_string(), | ||||
|             role_name: "Internal Admin".to_string(), | ||||
|             scope: RoleScope::Organization, | ||||
|             is_invitable: false, | ||||
|             is_deletable: false, | ||||
|             is_updatable: false, | ||||
|             is_internal: true, | ||||
|         }, | ||||
|     ); | ||||
|     roles.insert( | ||||
|         consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER, | ||||
|         RoleInfo { | ||||
|             groups: vec![ | ||||
|                 PermissionGroup::OperationsView, | ||||
|                 PermissionGroup::ConnectorsView, | ||||
|                 PermissionGroup::WorkflowsView, | ||||
|                 PermissionGroup::AnalyticsView, | ||||
|                 PermissionGroup::UsersView, | ||||
|                 PermissionGroup::MerchantDetailsView, | ||||
|             ], | ||||
|             role_id: consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), | ||||
|             role_name: "Internal View Only".to_string(), | ||||
|             scope: RoleScope::Organization, | ||||
|             is_invitable: false, | ||||
|             is_deletable: false, | ||||
|             is_updatable: false, | ||||
|             is_internal: true, | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     roles.insert( | ||||
|         consts::user_role::ROLE_ID_ORGANIZATION_ADMIN, | ||||
|         RoleInfo { | ||||
|             groups: vec![ | ||||
|                 PermissionGroup::OperationsView, | ||||
|                 PermissionGroup::OperationsManage, | ||||
|                 PermissionGroup::ConnectorsView, | ||||
|                 PermissionGroup::ConnectorsManage, | ||||
|                 PermissionGroup::WorkflowsView, | ||||
|                 PermissionGroup::WorkflowsManage, | ||||
|                 PermissionGroup::AnalyticsView, | ||||
|                 PermissionGroup::UsersView, | ||||
|                 PermissionGroup::UsersManage, | ||||
|                 PermissionGroup::MerchantDetailsView, | ||||
|                 PermissionGroup::MerchantDetailsManage, | ||||
|                 PermissionGroup::OrganizationManage, | ||||
|             ], | ||||
|             role_id: consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), | ||||
|             role_name: "Organization Admin".to_string(), | ||||
|             scope: RoleScope::Organization, | ||||
|             is_invitable: false, | ||||
|             is_deletable: false, | ||||
|             is_updatable: false, | ||||
|             is_internal: false, | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     // MERCHANT ROLES | ||||
|     roles.insert( | ||||
|         consts::user_role::ROLE_ID_MERCHANT_ADMIN, | ||||
|         RoleInfo { | ||||
|             groups: vec![ | ||||
|                 PermissionGroup::OperationsView, | ||||
|                 PermissionGroup::OperationsManage, | ||||
|                 PermissionGroup::ConnectorsView, | ||||
|                 PermissionGroup::ConnectorsManage, | ||||
|                 PermissionGroup::WorkflowsView, | ||||
|                 PermissionGroup::WorkflowsManage, | ||||
|                 PermissionGroup::AnalyticsView, | ||||
|                 PermissionGroup::UsersView, | ||||
|                 PermissionGroup::UsersManage, | ||||
|                 PermissionGroup::MerchantDetailsView, | ||||
|                 PermissionGroup::MerchantDetailsManage, | ||||
|             ], | ||||
|             role_id: consts::user_role::ROLE_ID_MERCHANT_ADMIN.to_string(), | ||||
|             role_name: "Admin".to_string(), | ||||
|             scope: RoleScope::Organization, | ||||
|             is_invitable: true, | ||||
|             is_deletable: true, | ||||
|             is_updatable: true, | ||||
|             is_internal: false, | ||||
|         }, | ||||
|     ); | ||||
|     roles.insert( | ||||
|         consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY, | ||||
|         RoleInfo { | ||||
|             groups: vec![ | ||||
|                 PermissionGroup::OperationsView, | ||||
|                 PermissionGroup::ConnectorsView, | ||||
|                 PermissionGroup::WorkflowsView, | ||||
|                 PermissionGroup::AnalyticsView, | ||||
|                 PermissionGroup::UsersView, | ||||
|                 PermissionGroup::MerchantDetailsView, | ||||
|             ], | ||||
|             role_id: consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY.to_string(), | ||||
|             role_name: "View Only".to_string(), | ||||
|             scope: RoleScope::Organization, | ||||
|             is_invitable: true, | ||||
|             is_deletable: true, | ||||
|             is_updatable: true, | ||||
|             is_internal: false, | ||||
|         }, | ||||
|     ); | ||||
|     roles.insert( | ||||
|         consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN, | ||||
|         RoleInfo { | ||||
|             groups: vec![ | ||||
|                 PermissionGroup::OperationsView, | ||||
|                 PermissionGroup::AnalyticsView, | ||||
|                 PermissionGroup::UsersView, | ||||
|                 PermissionGroup::UsersManage, | ||||
|                 PermissionGroup::MerchantDetailsView, | ||||
|             ], | ||||
|             role_id: consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN.to_string(), | ||||
|             role_name: "IAM".to_string(), | ||||
|             scope: RoleScope::Organization, | ||||
|             is_invitable: true, | ||||
|             is_deletable: true, | ||||
|             is_updatable: true, | ||||
|             is_internal: false, | ||||
|         }, | ||||
|     ); | ||||
|     roles.insert( | ||||
|         consts::user_role::ROLE_ID_MERCHANT_DEVELOPER, | ||||
|         RoleInfo { | ||||
|             groups: vec![ | ||||
|                 PermissionGroup::OperationsView, | ||||
|                 PermissionGroup::ConnectorsView, | ||||
|                 PermissionGroup::AnalyticsView, | ||||
|                 PermissionGroup::UsersView, | ||||
|                 PermissionGroup::MerchantDetailsView, | ||||
|                 PermissionGroup::MerchantDetailsManage, | ||||
|             ], | ||||
|             role_id: consts::user_role::ROLE_ID_MERCHANT_DEVELOPER.to_string(), | ||||
|             role_name: "Developer".to_string(), | ||||
|             scope: RoleScope::Organization, | ||||
|             is_invitable: true, | ||||
|             is_deletable: true, | ||||
|             is_updatable: true, | ||||
|             is_internal: false, | ||||
|         }, | ||||
|     ); | ||||
|     roles.insert( | ||||
|         consts::user_role::ROLE_ID_MERCHANT_OPERATOR, | ||||
|         RoleInfo { | ||||
|             groups: vec![ | ||||
|                 PermissionGroup::OperationsView, | ||||
|                 PermissionGroup::OperationsManage, | ||||
|                 PermissionGroup::ConnectorsView, | ||||
|                 PermissionGroup::WorkflowsView, | ||||
|                 PermissionGroup::AnalyticsView, | ||||
|                 PermissionGroup::UsersView, | ||||
|                 PermissionGroup::MerchantDetailsView, | ||||
|             ], | ||||
|             role_id: consts::user_role::ROLE_ID_MERCHANT_OPERATOR.to_string(), | ||||
|             role_name: "Operator".to_string(), | ||||
|             scope: RoleScope::Organization, | ||||
|             is_invitable: true, | ||||
|             is_deletable: true, | ||||
|             is_updatable: true, | ||||
|             is_internal: false, | ||||
|         }, | ||||
|     ); | ||||
|     roles.insert( | ||||
|         consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT, | ||||
|         RoleInfo { | ||||
|             groups: vec![ | ||||
|                 PermissionGroup::OperationsView, | ||||
|                 PermissionGroup::AnalyticsView, | ||||
|                 PermissionGroup::UsersView, | ||||
|                 PermissionGroup::MerchantDetailsView, | ||||
|             ], | ||||
|             role_id: consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT.to_string(), | ||||
|             role_name: "Customer Support".to_string(), | ||||
|             scope: RoleScope::Organization, | ||||
|             is_invitable: true, | ||||
|             is_deletable: true, | ||||
|             is_updatable: true, | ||||
|             is_internal: false, | ||||
|         }, | ||||
|     ); | ||||
|     roles | ||||
| }); | ||||
| @ -25,11 +25,7 @@ use crate::{ | ||||
|     }, | ||||
|     db::StorageInterface, | ||||
|     routes::AppState, | ||||
|     services::{ | ||||
|         authentication as auth, | ||||
|         authentication::UserFromToken, | ||||
|         authorization::{info, predefined_permissions}, | ||||
|     }, | ||||
|     services::{authentication as auth, authentication::UserFromToken, authorization::info}, | ||||
|     types::transformers::ForeignFrom, | ||||
|     utils::{self, user::password}, | ||||
| }; | ||||
| @ -824,32 +820,6 @@ impl From<info::PermissionInfo> for user_role_api::PermissionInfo { | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct UserAndRoleJoined(pub storage_user::User, pub UserRole); | ||||
|  | ||||
| impl TryFrom<UserAndRoleJoined> for user_api::UserDetails { | ||||
|     type Error = (); | ||||
|     fn try_from(user_and_role: UserAndRoleJoined) -> Result<Self, Self::Error> { | ||||
|         let status = match user_and_role.1.status { | ||||
|             UserStatus::Active => user_role_api::UserStatus::Active, | ||||
|             UserStatus::InvitationSent => user_role_api::UserStatus::InvitationSent, | ||||
|         }; | ||||
|  | ||||
|         let role_id = user_and_role.1.role_id; | ||||
|         let role_name = predefined_permissions::get_role_name_from_id(role_id.as_str()) | ||||
|             .ok_or(())? | ||||
|             .to_string(); | ||||
|  | ||||
|         Ok(Self { | ||||
|             email: user_and_role.0.email, | ||||
|             name: user_and_role.0.name, | ||||
|             role_id, | ||||
|             status, | ||||
|             role_name, | ||||
|             last_modified_at: user_and_role.0.last_modified_at, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub enum SignInWithRoleStrategyType { | ||||
|     SingleRole(SignInWithSingleRoleStrategy), | ||||
|     MultipleRoles(SignInWithMultipleRolesStrategy), | ||||
| @ -947,3 +917,12 @@ impl SignInWithMultipleRolesStrategy { | ||||
|         )) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ForeignFrom<UserStatus> for user_role_api::UserStatus { | ||||
|     fn foreign_from(value: UserStatus) -> Self { | ||||
|         match value { | ||||
|             UserStatus::Active => Self::Active, | ||||
|             UserStatus::InvitationSent => Self::InvitationSent, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -9,7 +9,10 @@ use masking::{ExposeInterface, Secret}; | ||||
| use crate::{ | ||||
|     core::errors::{StorageError, UserErrors, UserResult}, | ||||
|     routes::AppState, | ||||
|     services::authentication::{AuthToken, UserFromToken}, | ||||
|     services::{ | ||||
|         authentication::{AuthToken, UserFromToken}, | ||||
|         authorization::roles::{self, RoleInfo}, | ||||
|     }, | ||||
|     types::domain::{self, MerchantAccount, UserFromStorage}, | ||||
| }; | ||||
|  | ||||
| @ -19,7 +22,10 @@ pub mod password; | ||||
| pub mod sample_data; | ||||
|  | ||||
| impl UserFromToken { | ||||
|     pub async fn get_merchant_account(&self, state: AppState) -> UserResult<MerchantAccount> { | ||||
|     pub async fn get_merchant_account_from_db( | ||||
|         &self, | ||||
|         state: AppState, | ||||
|     ) -> UserResult<MerchantAccount> { | ||||
|         let key_store = state | ||||
|             .store | ||||
|             .get_merchant_key_store_by_merchant_id( | ||||
| @ -56,6 +62,12 @@ impl UserFromToken { | ||||
|             .change_context(UserErrors::InternalServerError)?; | ||||
|         Ok(user.into()) | ||||
|     } | ||||
|  | ||||
|     pub async fn get_role_info_from_db(&self, state: &AppState) -> UserResult<RoleInfo> { | ||||
|         roles::get_role_info_from_role_id(state, &self.role_id, &self.merchant_id, &self.org_id) | ||||
|             .await | ||||
|             .change_context(UserErrors::InternalServerError) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn generate_jwt_auth_token( | ||||
|  | ||||
| @ -1,29 +1,6 @@ | ||||
| use api_models::user_role as user_role_api; | ||||
|  | ||||
| use crate::{ | ||||
|     consts, | ||||
|     services::authorization::{permissions::Permission, predefined_permissions::RoleInfo}, | ||||
| }; | ||||
|  | ||||
| pub fn is_internal_role(role_id: &str) -> bool { | ||||
|     role_id == consts::user_role::ROLE_ID_INTERNAL_ADMIN | ||||
|         || role_id == consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER | ||||
| } | ||||
|  | ||||
| pub fn get_role_name_and_permission_response( | ||||
|     role_info: &RoleInfo, | ||||
| ) -> Option<(Vec<user_role_api::Permission>, &'static str)> { | ||||
|     role_info.get_name().map(|name| { | ||||
|         ( | ||||
|             role_info | ||||
|                 .get_permissions() | ||||
|                 .iter() | ||||
|                 .map(|&per| per.into()) | ||||
|                 .collect::<Vec<user_role_api::Permission>>(), | ||||
|             name, | ||||
|         ) | ||||
|     }) | ||||
| } | ||||
| use crate::services::authorization::permissions::Permission; | ||||
|  | ||||
| impl From<Permission> for user_role_api::Permission { | ||||
|     fn from(value: Permission) -> Self { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Mani Chandra
					Mani Chandra