mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
feat(user_role): Add APIs for user roles (#3013)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -7,6 +7,7 @@ pub mod payouts;
|
||||
pub mod refund;
|
||||
pub mod routing;
|
||||
pub mod user;
|
||||
pub mod user_role;
|
||||
|
||||
use common_utils::{
|
||||
events::{ApiEventMetric, ApiEventsType},
|
||||
|
||||
@ -5,6 +5,7 @@ use crate::user::{
|
||||
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
|
||||
},
|
||||
ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse,
|
||||
CreateInternalUserRequest, SwitchMerchantIdRequest, UserMerchantCreate,
|
||||
};
|
||||
|
||||
impl ApiEventMetric for ConnectAccountResponse {
|
||||
@ -23,5 +24,8 @@ common_utils::impl_misc_api_event_type!(
|
||||
GetMultipleMetaDataPayload,
|
||||
GetMetaDataResponse,
|
||||
GetMetaDataRequest,
|
||||
SetMetaDataRequest
|
||||
SetMetaDataRequest,
|
||||
SwitchMerchantIdRequest,
|
||||
CreateInternalUserRequest,
|
||||
UserMerchantCreate
|
||||
);
|
||||
|
||||
14
crates/api_models/src/events/user_role.rs
Normal file
14
crates/api_models/src/events/user_role.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
||||
|
||||
use crate::user_role::{
|
||||
AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, RoleInfoResponse,
|
||||
UpdateUserRoleRequest,
|
||||
};
|
||||
|
||||
common_utils::impl_misc_api_event_type!(
|
||||
ListRolesResponse,
|
||||
RoleInfoResponse,
|
||||
GetRoleRequest,
|
||||
AuthorizationInfoResponse,
|
||||
UpdateUserRoleRequest
|
||||
);
|
||||
@ -26,6 +26,7 @@ pub mod refunds;
|
||||
pub mod routing;
|
||||
pub mod surcharge_decision_configs;
|
||||
pub mod user;
|
||||
pub mod user_role;
|
||||
pub mod verifications;
|
||||
pub mod verify_connector;
|
||||
pub mod webhooks;
|
||||
|
||||
@ -26,3 +26,20 @@ pub struct ChangePasswordRequest {
|
||||
pub new_password: Secret<String>,
|
||||
pub old_password: Secret<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct SwitchMerchantIdRequest {
|
||||
pub merchant_id: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, serde::Serialize)]
|
||||
pub struct CreateInternalUserRequest {
|
||||
pub name: Secret<String>,
|
||||
pub email: pii::Email,
|
||||
pub password: Secret<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct UserMerchantCreate {
|
||||
pub company_name: String,
|
||||
}
|
||||
|
||||
82
crates/api_models/src/user_role.rs
Normal file
82
crates/api_models/src/user_role.rs
Normal file
@ -0,0 +1,82 @@
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct RoleInfoResponse {
|
||||
pub role_id: &'static str,
|
||||
pub permissions: Vec<Permission>,
|
||||
pub role_name: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct GetRoleRequest {
|
||||
pub role_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub enum Permission {
|
||||
PaymentRead,
|
||||
PaymentWrite,
|
||||
RefundRead,
|
||||
RefundWrite,
|
||||
ApiKeyRead,
|
||||
ApiKeyWrite,
|
||||
MerchantAccountRead,
|
||||
MerchantAccountWrite,
|
||||
MerchantConnectorAccountRead,
|
||||
MerchantConnectorAccountWrite,
|
||||
ForexRead,
|
||||
RoutingRead,
|
||||
RoutingWrite,
|
||||
DisputeRead,
|
||||
DisputeWrite,
|
||||
MandateRead,
|
||||
MandateWrite,
|
||||
FileRead,
|
||||
FileWrite,
|
||||
Analytics,
|
||||
ThreeDsDecisionManagerWrite,
|
||||
ThreeDsDecisionManagerRead,
|
||||
SurchargeDecisionManagerWrite,
|
||||
SurchargeDecisionManagerRead,
|
||||
UsersRead,
|
||||
UsersWrite,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub enum PermissionModule {
|
||||
Payments,
|
||||
Refunds,
|
||||
MerchantAccount,
|
||||
Forex,
|
||||
Connectors,
|
||||
Routing,
|
||||
Analytics,
|
||||
Mandates,
|
||||
Disputes,
|
||||
Files,
|
||||
ThreeDsDecisionManager,
|
||||
SurchargeDecisionManager,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct AuthorizationInfoResponse(pub Vec<ModuleInfo>);
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct ModuleInfo {
|
||||
pub module: PermissionModule,
|
||||
pub description: &'static str,
|
||||
pub permissions: Vec<PermissionInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct PermissionInfo {
|
||||
pub enum_name: Permission,
|
||||
pub description: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct UpdateUserRoleRequest {
|
||||
pub user_id: String,
|
||||
pub role_id: String,
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
pub mod user_role;
|
||||
|
||||
// ID generation
|
||||
pub(crate) const ID_LENGTH: usize = 20;
|
||||
@ -64,7 +65,6 @@ pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day
|
||||
pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin";
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify";
|
||||
|
||||
11
crates/router/src/consts/user_role.rs
Normal file
11
crates/router/src/consts/user_role.rs
Normal file
@ -0,0 +1,11 @@
|
||||
// User Roles
|
||||
pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only";
|
||||
pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin";
|
||||
pub const ROLE_ID_MERCHANT_ADMIN: &str = "merchant_admin";
|
||||
pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin";
|
||||
pub const ROLE_ID_MERCHANT_VIEW_ONLY: &str = "merchant_view_only";
|
||||
pub const ROLE_ID_MERCHANT_IAM_ADMIN: &str = "merchant_iam_admin";
|
||||
pub const ROLE_ID_MERCHANT_DEVELOPER: &str = "merchant_developer";
|
||||
pub const ROLE_ID_MERCHANT_OPERATOR: &str = "merchant_operator";
|
||||
pub const ROLE_ID_MERCHANT_CUSTOMER_SUPPORT: &str = "merchant_customer_support";
|
||||
pub const INTERNAL_USER_MERCHANT_ID: &str = "juspay000";
|
||||
@ -25,6 +25,8 @@ pub mod routing;
|
||||
pub mod surcharge_decision_config;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user_role;
|
||||
pub mod utils;
|
||||
#[cfg(all(feature = "olap", feature = "kms"))]
|
||||
pub mod verification;
|
||||
|
||||
@ -27,16 +27,22 @@ pub enum UserErrors {
|
||||
MerchantAccountCreationError(String),
|
||||
#[error("InvalidEmailError")]
|
||||
InvalidEmailError,
|
||||
#[error("DuplicateOrganizationId")]
|
||||
DuplicateOrganizationId,
|
||||
#[error("MerchantIdNotFound")]
|
||||
MerchantIdNotFound,
|
||||
#[error("MetadataAlreadySet")]
|
||||
MetadataAlreadySet,
|
||||
#[error("DuplicateOrganizationId")]
|
||||
DuplicateOrganizationId,
|
||||
#[error("InvalidRoleId")]
|
||||
InvalidRoleId,
|
||||
#[error("InvalidRoleOperation")]
|
||||
InvalidRoleOperation,
|
||||
#[error("IpAddressParsingFailed")]
|
||||
IpAddressParsingFailed,
|
||||
#[error("InvalidMetadataRequest")]
|
||||
InvalidMetadataRequest,
|
||||
#[error("MerchantIdParsingError")]
|
||||
MerchantIdParsingError,
|
||||
}
|
||||
|
||||
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
|
||||
@ -95,6 +101,15 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
||||
"An Organization with the id already exists",
|
||||
None,
|
||||
)),
|
||||
Self::InvalidRoleId => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 22, "Invalid Role ID", None))
|
||||
}
|
||||
Self::InvalidRoleOperation => AER::BadRequest(ApiError::new(
|
||||
sub_code,
|
||||
23,
|
||||
"User Role Operation Not Supported",
|
||||
None,
|
||||
)),
|
||||
Self::IpAddressParsingFailed => {
|
||||
AER::InternalServerError(ApiError::new(sub_code, 24, "Something Went Wrong", None))
|
||||
}
|
||||
@ -104,6 +119,9 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
||||
"Invalid Metadata Request",
|
||||
None,
|
||||
)),
|
||||
Self::MerchantIdParsingError => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 28, "Invalid Merchant Id", None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use api_models::user as api;
|
||||
use diesel_models::enums::UserStatus;
|
||||
use api_models::user as user_api;
|
||||
use diesel_models::{enums::UserStatus, user as storage_user};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::{ExposeInterface, Secret};
|
||||
use router_env::env;
|
||||
@ -9,16 +9,17 @@ use crate::{
|
||||
consts,
|
||||
db::user::UserInterface,
|
||||
routes::AppState,
|
||||
services::{authentication::UserFromToken, ApplicationResponse},
|
||||
services::{authentication as auth, ApplicationResponse},
|
||||
types::domain,
|
||||
utils,
|
||||
};
|
||||
|
||||
pub mod dashboard_metadata;
|
||||
|
||||
pub async fn connect_account(
|
||||
state: AppState,
|
||||
request: api::ConnectAccountRequest,
|
||||
) -> UserResponse<api::ConnectAccountResponse> {
|
||||
request: user_api::ConnectAccountRequest,
|
||||
) -> UserResponse<user_api::ConnectAccountResponse> {
|
||||
let find_user = state
|
||||
.store
|
||||
.find_user_by_email(request.email.clone().expose().expose().as_str())
|
||||
@ -34,15 +35,17 @@ pub async fn connect_account(
|
||||
.get_jwt_auth_token(state.clone(), user_role.org_id)
|
||||
.await?;
|
||||
|
||||
return Ok(ApplicationResponse::Json(api::ConnectAccountResponse {
|
||||
token: Secret::new(jwt_token),
|
||||
merchant_id: user_role.merchant_id,
|
||||
name: user_from_db.get_name(),
|
||||
email: user_from_db.get_email(),
|
||||
verification_days_left: None,
|
||||
user_role: user_role.role_id,
|
||||
user_id: user_from_db.get_user_id().to_string(),
|
||||
}));
|
||||
return Ok(ApplicationResponse::Json(
|
||||
user_api::ConnectAccountResponse {
|
||||
token: Secret::new(jwt_token),
|
||||
merchant_id: user_role.merchant_id,
|
||||
name: user_from_db.get_name(),
|
||||
email: user_from_db.get_email(),
|
||||
verification_days_left: None,
|
||||
user_role: user_role.role_id,
|
||||
user_id: user_from_db.get_user_id().to_string(),
|
||||
},
|
||||
));
|
||||
} else if find_user
|
||||
.map_err(|e| e.current_context().is_db_not_found())
|
||||
.err()
|
||||
@ -64,7 +67,7 @@ pub async fn connect_account(
|
||||
let user_role = new_user
|
||||
.insert_user_role_in_db(
|
||||
state.clone(),
|
||||
consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||
consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||
UserStatus::Active,
|
||||
)
|
||||
.await?;
|
||||
@ -94,15 +97,17 @@ pub async fn connect_account(
|
||||
logger::info!(?send_email_result);
|
||||
}
|
||||
|
||||
return Ok(ApplicationResponse::Json(api::ConnectAccountResponse {
|
||||
token: Secret::new(jwt_token),
|
||||
merchant_id: user_role.merchant_id,
|
||||
name: user_from_db.get_name(),
|
||||
email: user_from_db.get_email(),
|
||||
verification_days_left: None,
|
||||
user_role: user_role.role_id,
|
||||
user_id: user_from_db.get_user_id().to_string(),
|
||||
}));
|
||||
return Ok(ApplicationResponse::Json(
|
||||
user_api::ConnectAccountResponse {
|
||||
token: Secret::new(jwt_token),
|
||||
merchant_id: user_role.merchant_id,
|
||||
name: user_from_db.get_name(),
|
||||
email: user_from_db.get_email(),
|
||||
verification_days_left: None,
|
||||
user_role: user_role.role_id,
|
||||
user_id: user_from_db.get_user_id().to_string(),
|
||||
},
|
||||
));
|
||||
} else {
|
||||
Err(UserErrors::InternalServerError.into())
|
||||
}
|
||||
@ -110,8 +115,8 @@ pub async fn connect_account(
|
||||
|
||||
pub async fn change_password(
|
||||
state: AppState,
|
||||
request: api::ChangePasswordRequest,
|
||||
user_from_token: UserFromToken,
|
||||
request: user_api::ChangePasswordRequest,
|
||||
user_from_token: auth::UserFromToken,
|
||||
) -> UserResponse<()> {
|
||||
let user: domain::UserFromStorage =
|
||||
UserInterface::find_user_by_id(&*state.store, &user_from_token.user_id)
|
||||
@ -139,3 +144,180 @@ pub async fn change_password(
|
||||
|
||||
Ok(ApplicationResponse::StatusOk)
|
||||
}
|
||||
|
||||
pub async fn create_internal_user(
|
||||
state: AppState,
|
||||
request: user_api::CreateInternalUserRequest,
|
||||
) -> UserResponse<()> {
|
||||
let new_user = domain::NewUser::try_from(request)?;
|
||||
|
||||
let mut store_user: storage_user::UserNew = new_user.clone().try_into()?;
|
||||
store_user.set_is_verified(true);
|
||||
|
||||
let key_store = state
|
||||
.store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
consts::user_role::INTERNAL_USER_MERCHANT_ID,
|
||||
&state.store.get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.current_context().is_db_not_found() {
|
||||
e.change_context(UserErrors::MerchantIdNotFound)
|
||||
} else {
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
})?;
|
||||
|
||||
state
|
||||
.store
|
||||
.find_merchant_account_by_merchant_id(
|
||||
consts::user_role::INTERNAL_USER_MERCHANT_ID,
|
||||
&key_store,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.current_context().is_db_not_found() {
|
||||
e.change_context(UserErrors::MerchantIdNotFound)
|
||||
} else {
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
})?;
|
||||
|
||||
state
|
||||
.store
|
||||
.insert_user(store_user)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.current_context().is_db_unique_violation() {
|
||||
e.change_context(UserErrors::UserExists)
|
||||
} else {
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
})
|
||||
.map(domain::user::UserFromStorage::from)?;
|
||||
|
||||
new_user
|
||||
.insert_user_role_in_db(
|
||||
state,
|
||||
consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(),
|
||||
UserStatus::Active,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ApplicationResponse::StatusOk)
|
||||
}
|
||||
|
||||
pub async fn switch_merchant_id(
|
||||
state: AppState,
|
||||
request: user_api::SwitchMerchantIdRequest,
|
||||
user_from_token: auth::UserFromToken,
|
||||
) -> UserResponse<user_api::ConnectAccountResponse> {
|
||||
if !utils::user_role::is_internal_role(&user_from_token.role_id) {
|
||||
let merchant_list =
|
||||
utils::user_role::get_merchant_ids_for_user(state.clone(), &user_from_token.user_id)
|
||||
.await?;
|
||||
if !merchant_list.contains(&request.merchant_id) {
|
||||
return Err(UserErrors::InvalidRoleOperation.into())
|
||||
.attach_printable("User doesn't have access to switch");
|
||||
}
|
||||
}
|
||||
|
||||
if user_from_token.merchant_id == request.merchant_id {
|
||||
return Err(UserErrors::InvalidRoleOperation.into())
|
||||
.attach_printable("User switch to same merchant id.");
|
||||
}
|
||||
|
||||
let user = state
|
||||
.store
|
||||
.find_user_by_id(&user_from_token.user_id)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
let key_store = state
|
||||
.store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
request.merchant_id.as_str(),
|
||||
&state.store.get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.current_context().is_db_not_found() {
|
||||
e.change_context(UserErrors::MerchantIdNotFound)
|
||||
} else {
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
})?;
|
||||
|
||||
let org_id = state
|
||||
.store
|
||||
.find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.current_context().is_db_not_found() {
|
||||
e.change_context(UserErrors::MerchantIdNotFound)
|
||||
} else {
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
})?
|
||||
.organization_id;
|
||||
|
||||
let user = domain::UserFromStorage::from(user);
|
||||
let user_role = state
|
||||
.store
|
||||
.find_user_role_by_user_id(user.get_user_id())
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
let token = Box::pin(user.get_jwt_auth_token_with_custom_merchant_id(
|
||||
state.clone(),
|
||||
request.merchant_id.clone(),
|
||||
org_id,
|
||||
))
|
||||
.await?
|
||||
.into();
|
||||
|
||||
Ok(ApplicationResponse::Json(
|
||||
user_api::ConnectAccountResponse {
|
||||
merchant_id: request.merchant_id,
|
||||
token,
|
||||
name: user.get_name(),
|
||||
email: user.get_email(),
|
||||
user_id: user.get_user_id().to_string(),
|
||||
verification_days_left: None,
|
||||
user_role: user_role.role_id,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn create_merchant_account(
|
||||
state: AppState,
|
||||
user_from_token: auth::UserFromToken,
|
||||
req: user_api::UserMerchantCreate,
|
||||
) -> UserResponse<()> {
|
||||
let user_from_db: domain::UserFromStorage =
|
||||
user_from_token.get_user(state.clone()).await?.into();
|
||||
|
||||
let new_user = domain::NewUser::try_from((user_from_db, req, user_from_token))?;
|
||||
let new_merchant = new_user.get_new_merchant();
|
||||
new_merchant
|
||||
.create_new_merchant_and_insert_in_db(state.to_owned())
|
||||
.await?;
|
||||
|
||||
let role_insertion_res = new_user
|
||||
.insert_user_role_in_db(
|
||||
state.clone(),
|
||||
consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||
UserStatus::Active,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = role_insertion_res {
|
||||
let _ = state
|
||||
.store
|
||||
.delete_merchant_account_by_merchant_id(new_merchant.get_merchant_id().as_str())
|
||||
.await;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(ApplicationResponse::StatusOk)
|
||||
}
|
||||
|
||||
101
crates/router/src/core/user_role.rs
Normal file
101
crates/router/src/core/user_role.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use api_models::user_role as user_role_api;
|
||||
use diesel_models::user_role::UserRoleUpdate;
|
||||
use error_stack::ResultExt;
|
||||
|
||||
use crate::{
|
||||
core::errors::{UserErrors, UserResponse},
|
||||
routes::AppState,
|
||||
services::{
|
||||
authentication::{self as auth},
|
||||
authorization::{info, predefined_permissions},
|
||||
ApplicationResponse,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
|
||||
pub async fn get_authorization_info(
|
||||
_state: AppState,
|
||||
) -> UserResponse<user_role_api::AuthorizationInfoResponse> {
|
||||
Ok(ApplicationResponse::Json(
|
||||
user_role_api::AuthorizationInfoResponse(
|
||||
info::get_authorization_info()
|
||||
.into_iter()
|
||||
.filter_map(|module| module.try_into().ok())
|
||||
.collect(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_roles(_state: AppState) -> UserResponse<user_role_api::ListRolesResponse> {
|
||||
Ok(ApplicationResponse::Json(user_role_api::ListRolesResponse(
|
||||
predefined_permissions::PREDEFINED_PERMISSIONS
|
||||
.iter()
|
||||
.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(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn get_role(
|
||||
_state: AppState,
|
||||
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)?;
|
||||
|
||||
Ok(ApplicationResponse::Json(info))
|
||||
}
|
||||
|
||||
pub async fn update_user_role(
|
||||
state: AppState,
|
||||
user_from_token: auth::UserFromToken,
|
||||
req: user_role_api::UpdateUserRoleRequest,
|
||||
) -> UserResponse<()> {
|
||||
let merchant_id = user_from_token.merchant_id;
|
||||
let role_id = req.role_id.clone();
|
||||
utils::user_role::validate_role_id(role_id.as_str())?;
|
||||
|
||||
if user_from_token.user_id == req.user_id {
|
||||
return Err(UserErrors::InvalidRoleOperation.into())
|
||||
.attach_printable("Admin User Changing their role");
|
||||
}
|
||||
|
||||
state
|
||||
.store
|
||||
.update_user_role_by_user_id_merchant_id(
|
||||
req.user_id.as_str(),
|
||||
merchant_id.as_str(),
|
||||
UserRoleUpdate::UpdateRole {
|
||||
role_id,
|
||||
modified_by: user_from_token.user_id,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.current_context().is_db_not_found() {
|
||||
return e
|
||||
.change_context(UserErrors::InvalidRoleOperation)
|
||||
.attach_printable("UserId MerchantId not found");
|
||||
}
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
})?;
|
||||
|
||||
Ok(ApplicationResponse::StatusOk)
|
||||
}
|
||||
@ -27,6 +27,8 @@ pub mod refunds;
|
||||
pub mod routing;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user_role;
|
||||
#[cfg(all(feature = "olap", feature = "kms"))]
|
||||
pub mod verification;
|
||||
#[cfg(feature = "olap")]
|
||||
|
||||
@ -23,7 +23,7 @@ use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_ve
|
||||
#[cfg(feature = "olap")]
|
||||
use super::{
|
||||
admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, payment_link::*,
|
||||
user::*,
|
||||
user::*, user_role::*,
|
||||
};
|
||||
use super::{cache::*, health::*};
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
@ -812,6 +812,17 @@ impl User {
|
||||
.route(web::post().to(set_merchant_scoped_dashboard_metadata)),
|
||||
)
|
||||
.service(web::resource("/data").route(web::get().to(get_multiple_dashboard_metadata)))
|
||||
.service(web::resource("/internal_signup").route(web::post().to(internal_user_signup)))
|
||||
.service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id)))
|
||||
.service(
|
||||
web::resource("/create_merchant")
|
||||
.route(web::post().to(user_merchant_account_create)),
|
||||
)
|
||||
// User Role APIs
|
||||
.service(web::resource("/permission_info").route(web::get().to(get_authorization_info)))
|
||||
.service(web::resource("/user/update_role").route(web::post().to(update_user_role)))
|
||||
.service(web::resource("/role/list").route(web::get().to(list_roles)))
|
||||
.service(web::resource("/role/{role_id}").route(web::get().to(get_role)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ pub enum ApiIdentifier {
|
||||
RustLockerMigration,
|
||||
Gsm,
|
||||
User,
|
||||
UserRole,
|
||||
}
|
||||
|
||||
impl From<Flow> for ApiIdentifier {
|
||||
@ -151,7 +152,14 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::ChangePassword
|
||||
| Flow::SetDashboardMetadata
|
||||
| Flow::GetMutltipleDashboardMetadata
|
||||
| Flow::VerifyPaymentConnector => Self::User,
|
||||
| Flow::VerifyPaymentConnector
|
||||
| Flow::InternalUserSignup
|
||||
| Flow::SwitchMerchant
|
||||
| Flow::UserMerchantAccountCreate => Self::User,
|
||||
|
||||
Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => {
|
||||
Self::UserRole
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ use router_env::Flow;
|
||||
|
||||
use super::AppState;
|
||||
use crate::{
|
||||
core::{api_locking, user},
|
||||
core::{api_locking, user as user_core},
|
||||
services::{
|
||||
api,
|
||||
authentication::{self as auth},
|
||||
@ -26,7 +26,7 @@ pub async fn user_connect_account(
|
||||
state,
|
||||
&http_req,
|
||||
req_payload.clone(),
|
||||
|state, _, req_body| user::connect_account(state, req_body),
|
||||
|state, _, req_body| user_core::connect_account(state, req_body),
|
||||
&auth::NoAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
@ -44,7 +44,7 @@ pub async fn change_password(
|
||||
state.clone(),
|
||||
&http_req,
|
||||
json_payload.into_inner(),
|
||||
|state, user, req| user::change_password(state, req, user),
|
||||
|state, user, req| user_core::change_password(state, req, user),
|
||||
&auth::DashboardNoPermissionAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
@ -70,7 +70,7 @@ pub async fn set_merchant_scoped_dashboard_metadata(
|
||||
state,
|
||||
&req,
|
||||
payload,
|
||||
user::dashboard_metadata::set_metadata,
|
||||
user_core::dashboard_metadata::set_metadata,
|
||||
&auth::JWTAuth(Permission::MerchantAccountWrite),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
@ -96,9 +96,65 @@ pub async fn get_multiple_dashboard_metadata(
|
||||
state,
|
||||
&req,
|
||||
payload,
|
||||
user::dashboard_metadata::get_multiple_metadata,
|
||||
user_core::dashboard_metadata::get_multiple_metadata,
|
||||
&auth::DashboardNoPermissionAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn internal_user_signup(
|
||||
state: web::Data<AppState>,
|
||||
http_req: HttpRequest,
|
||||
json_payload: web::Json<user_api::CreateInternalUserRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::InternalUserSignup;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&http_req,
|
||||
json_payload.into_inner(),
|
||||
|state, _, req| user_core::create_internal_user(state, req),
|
||||
&auth::AdminApiAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn switch_merchant_id(
|
||||
state: web::Data<AppState>,
|
||||
http_req: HttpRequest,
|
||||
json_payload: web::Json<user_api::SwitchMerchantIdRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::SwitchMerchant;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&http_req,
|
||||
json_payload.into_inner(),
|
||||
|state, user, req| user_core::switch_merchant_id(state, req, user),
|
||||
&auth::DashboardNoPermissionAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn user_merchant_account_create(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
json_payload: web::Json<user_api::UserMerchantCreate>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::UserMerchantAccountCreate;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, auth: auth::UserFromToken, json_payload| {
|
||||
user_core::create_merchant_account(state, auth, json_payload)
|
||||
},
|
||||
&auth::JWTAuth(Permission::MerchantAccountCreate),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
84
crates/router/src/routes/user_role.rs
Normal file
84
crates/router/src/routes/user_role.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use api_models::user_role as user_role_api;
|
||||
use router_env::Flow;
|
||||
|
||||
use super::AppState;
|
||||
use crate::{
|
||||
core::{api_locking, user_role as user_role_core},
|
||||
services::{
|
||||
api,
|
||||
authentication::{self as auth},
|
||||
authorization::permissions::Permission,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn get_authorization_info(
|
||||
state: web::Data<AppState>,
|
||||
http_req: HttpRequest,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::GetAuthorizationInfo;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&http_req,
|
||||
(),
|
||||
|state, _: (), _| user_role_core::get_authorization_info(state),
|
||||
&auth::JWTAuth(Permission::UsersRead),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_roles(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
let flow = Flow::ListRoles;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
(),
|
||||
|state, _: (), _| user_role_core::list_roles(state),
|
||||
&auth::JWTAuth(Permission::UsersRead),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_role(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::GetRole;
|
||||
let request_payload = user_role_api::GetRoleRequest {
|
||||
role_id: path.into_inner(),
|
||||
};
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
request_payload,
|
||||
|state, _: (), req| user_role_core::get_role(state, req),
|
||||
&auth::JWTAuth(Permission::UsersRead),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_user_role(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
json_payload: web::Json<user_role_api::UpdateUserRoleRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::UpdateUserRole;
|
||||
let payload = json_payload.into_inner();
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
payload,
|
||||
user_role_core::update_user_role,
|
||||
&auth::JWTAuth(Permission::UsersWrite),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@ -444,6 +444,9 @@ where
|
||||
) -> RouterResult<(UserFromToken, AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.0, permissions)?;
|
||||
|
||||
Ok((
|
||||
UserFromToken {
|
||||
user_id: payload.user_id.clone(),
|
||||
|
||||
@ -28,7 +28,67 @@ impl RoleInfo {
|
||||
pub static PREDEFINED_PERMISSIONS: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|| {
|
||||
let mut roles = HashMap::new();
|
||||
roles.insert(
|
||||
consts::ROLE_ID_ORGANIZATION_ADMIN,
|
||||
consts::user_role::ROLE_ID_INTERNAL_ADMIN,
|
||||
RoleInfo {
|
||||
permissions: vec![
|
||||
Permission::PaymentRead,
|
||||
Permission::PaymentWrite,
|
||||
Permission::RefundRead,
|
||||
Permission::RefundWrite,
|
||||
Permission::ApiKeyRead,
|
||||
Permission::ApiKeyWrite,
|
||||
Permission::MerchantAccountRead,
|
||||
Permission::MerchantAccountWrite,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::MerchantConnectorAccountWrite,
|
||||
Permission::RoutingRead,
|
||||
Permission::RoutingWrite,
|
||||
Permission::ForexRead,
|
||||
Permission::ThreeDsDecisionManagerWrite,
|
||||
Permission::ThreeDsDecisionManagerRead,
|
||||
Permission::SurchargeDecisionManagerWrite,
|
||||
Permission::SurchargeDecisionManagerRead,
|
||||
Permission::DisputeRead,
|
||||
Permission::DisputeWrite,
|
||||
Permission::MandateRead,
|
||||
Permission::MandateWrite,
|
||||
Permission::FileRead,
|
||||
Permission::FileWrite,
|
||||
Permission::Analytics,
|
||||
Permission::UsersRead,
|
||||
Permission::UsersWrite,
|
||||
Permission::MerchantAccountCreate,
|
||||
],
|
||||
name: None,
|
||||
is_invitable: false,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER,
|
||||
RoleInfo {
|
||||
permissions: vec![
|
||||
Permission::PaymentRead,
|
||||
Permission::RefundRead,
|
||||
Permission::ApiKeyRead,
|
||||
Permission::MerchantAccountRead,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::RoutingRead,
|
||||
Permission::ForexRead,
|
||||
Permission::ThreeDsDecisionManagerRead,
|
||||
Permission::SurchargeDecisionManagerRead,
|
||||
Permission::Analytics,
|
||||
Permission::DisputeRead,
|
||||
Permission::MandateRead,
|
||||
Permission::FileRead,
|
||||
Permission::UsersRead,
|
||||
],
|
||||
name: None,
|
||||
is_invitable: false,
|
||||
},
|
||||
);
|
||||
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_ORGANIZATION_ADMIN,
|
||||
RoleInfo {
|
||||
permissions: vec![
|
||||
Permission::PaymentRead,
|
||||
@ -63,6 +123,164 @@ pub static PREDEFINED_PERMISSIONS: Lazy<HashMap<&'static str, RoleInfo>> = Lazy:
|
||||
is_invitable: false,
|
||||
},
|
||||
);
|
||||
|
||||
// MERCHANT ROLES
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_ADMIN,
|
||||
RoleInfo {
|
||||
permissions: vec![
|
||||
Permission::PaymentRead,
|
||||
Permission::PaymentWrite,
|
||||
Permission::RefundRead,
|
||||
Permission::RefundWrite,
|
||||
Permission::ApiKeyRead,
|
||||
Permission::ApiKeyWrite,
|
||||
Permission::MerchantAccountRead,
|
||||
Permission::MerchantAccountWrite,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::ForexRead,
|
||||
Permission::MerchantConnectorAccountWrite,
|
||||
Permission::RoutingRead,
|
||||
Permission::RoutingWrite,
|
||||
Permission::ThreeDsDecisionManagerWrite,
|
||||
Permission::ThreeDsDecisionManagerRead,
|
||||
Permission::SurchargeDecisionManagerWrite,
|
||||
Permission::SurchargeDecisionManagerRead,
|
||||
Permission::DisputeRead,
|
||||
Permission::DisputeWrite,
|
||||
Permission::MandateRead,
|
||||
Permission::MandateWrite,
|
||||
Permission::FileRead,
|
||||
Permission::FileWrite,
|
||||
Permission::Analytics,
|
||||
Permission::UsersRead,
|
||||
Permission::UsersWrite,
|
||||
],
|
||||
name: Some("Admin"),
|
||||
is_invitable: true,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY,
|
||||
RoleInfo {
|
||||
permissions: vec![
|
||||
Permission::PaymentRead,
|
||||
Permission::RefundRead,
|
||||
Permission::ApiKeyRead,
|
||||
Permission::MerchantAccountRead,
|
||||
Permission::ForexRead,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::RoutingRead,
|
||||
Permission::ThreeDsDecisionManagerRead,
|
||||
Permission::SurchargeDecisionManagerRead,
|
||||
Permission::DisputeRead,
|
||||
Permission::MandateRead,
|
||||
Permission::FileRead,
|
||||
Permission::Analytics,
|
||||
Permission::UsersRead,
|
||||
],
|
||||
name: Some("View Only"),
|
||||
is_invitable: true,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN,
|
||||
RoleInfo {
|
||||
permissions: vec![
|
||||
Permission::PaymentRead,
|
||||
Permission::RefundRead,
|
||||
Permission::ApiKeyRead,
|
||||
Permission::MerchantAccountRead,
|
||||
Permission::ForexRead,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::RoutingRead,
|
||||
Permission::ThreeDsDecisionManagerRead,
|
||||
Permission::SurchargeDecisionManagerRead,
|
||||
Permission::DisputeRead,
|
||||
Permission::MandateRead,
|
||||
Permission::FileRead,
|
||||
Permission::Analytics,
|
||||
Permission::UsersRead,
|
||||
Permission::UsersWrite,
|
||||
],
|
||||
name: Some("IAM"),
|
||||
is_invitable: true,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_DEVELOPER,
|
||||
RoleInfo {
|
||||
permissions: vec![
|
||||
Permission::PaymentRead,
|
||||
Permission::RefundRead,
|
||||
Permission::ApiKeyRead,
|
||||
Permission::ApiKeyWrite,
|
||||
Permission::MerchantAccountRead,
|
||||
Permission::ForexRead,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::RoutingRead,
|
||||
Permission::ThreeDsDecisionManagerRead,
|
||||
Permission::SurchargeDecisionManagerRead,
|
||||
Permission::DisputeRead,
|
||||
Permission::MandateRead,
|
||||
Permission::FileRead,
|
||||
Permission::Analytics,
|
||||
Permission::UsersRead,
|
||||
],
|
||||
name: Some("Developer"),
|
||||
is_invitable: true,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_OPERATOR,
|
||||
RoleInfo {
|
||||
permissions: vec![
|
||||
Permission::PaymentRead,
|
||||
Permission::PaymentWrite,
|
||||
Permission::RefundRead,
|
||||
Permission::RefundWrite,
|
||||
Permission::ApiKeyRead,
|
||||
Permission::MerchantAccountRead,
|
||||
Permission::ForexRead,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::MerchantConnectorAccountWrite,
|
||||
Permission::RoutingRead,
|
||||
Permission::RoutingWrite,
|
||||
Permission::ThreeDsDecisionManagerRead,
|
||||
Permission::ThreeDsDecisionManagerWrite,
|
||||
Permission::SurchargeDecisionManagerRead,
|
||||
Permission::SurchargeDecisionManagerWrite,
|
||||
Permission::DisputeRead,
|
||||
Permission::MandateRead,
|
||||
Permission::FileRead,
|
||||
Permission::Analytics,
|
||||
Permission::UsersRead,
|
||||
],
|
||||
name: Some("Operator"),
|
||||
is_invitable: true,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT,
|
||||
RoleInfo {
|
||||
permissions: vec![
|
||||
Permission::PaymentRead,
|
||||
Permission::RefundRead,
|
||||
Permission::RefundWrite,
|
||||
Permission::ForexRead,
|
||||
Permission::DisputeRead,
|
||||
Permission::DisputeWrite,
|
||||
Permission::MerchantAccountRead,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::MandateRead,
|
||||
Permission::FileRead,
|
||||
Permission::FileWrite,
|
||||
Permission::Analytics,
|
||||
],
|
||||
name: Some("Customer Support"),
|
||||
is_invitable: true,
|
||||
},
|
||||
);
|
||||
roles
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use std::{collections::HashSet, ops, str::FromStr};
|
||||
|
||||
use api_models::{admin as admin_api, organization as api_org, user as user_api};
|
||||
use api_models::{
|
||||
admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api,
|
||||
};
|
||||
use common_utils::pii;
|
||||
use diesel_models::{
|
||||
enums::UserStatus,
|
||||
@ -12,17 +14,21 @@ use diesel_models::{
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::{ExposeInterface, PeekInterface, Secret};
|
||||
use once_cell::sync::Lazy;
|
||||
use router_env::env;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{
|
||||
consts::user as consts,
|
||||
consts,
|
||||
core::{
|
||||
admin,
|
||||
errors::{UserErrors, UserResult},
|
||||
},
|
||||
db::StorageInterface,
|
||||
routes::AppState,
|
||||
services::authentication::AuthToken,
|
||||
services::{
|
||||
authentication::{AuthToken, UserFromToken},
|
||||
authorization::info,
|
||||
},
|
||||
types::transformers::ForeignFrom,
|
||||
utils::user::password,
|
||||
};
|
||||
@ -36,7 +42,7 @@ impl UserName {
|
||||
pub fn new(name: Secret<String>) -> UserResult<Self> {
|
||||
let name = name.expose();
|
||||
let is_empty_or_whitespace = name.trim().is_empty();
|
||||
let is_too_long = name.graphemes(true).count() > consts::MAX_NAME_LENGTH;
|
||||
let is_too_long = name.graphemes(true).count() > consts::user::MAX_NAME_LENGTH;
|
||||
|
||||
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
|
||||
let contains_forbidden_characters = name.chars().any(|g| forbidden_characters.contains(&g));
|
||||
@ -167,7 +173,8 @@ impl UserCompanyName {
|
||||
pub fn new(company_name: String) -> UserResult<Self> {
|
||||
let company_name = company_name.trim();
|
||||
let is_empty_or_whitespace = company_name.is_empty();
|
||||
let is_too_long = company_name.graphemes(true).count() > consts::MAX_COMPANY_NAME_LENGTH;
|
||||
let is_too_long =
|
||||
company_name.graphemes(true).count() > consts::user::MAX_COMPANY_NAME_LENGTH;
|
||||
|
||||
let is_all_valid_characters = company_name
|
||||
.chars()
|
||||
@ -216,9 +223,47 @@ impl From<user_api::ConnectAccountRequest> for NewUserOrganization {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<user_api::CreateInternalUserRequest> for NewUserOrganization {
|
||||
fn from(_value: user_api::CreateInternalUserRequest) -> Self {
|
||||
let new_organization = api_org::OrganizationNew::new(None);
|
||||
let db_organization = ForeignFrom::foreign_from(new_organization);
|
||||
Self(db_organization)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserMerchantCreateRequestWithToken> for NewUserOrganization {
|
||||
fn from(value: UserMerchantCreateRequestWithToken) -> Self {
|
||||
Self(diesel_org::OrganizationNew {
|
||||
org_id: value.2.org_id,
|
||||
org_name: Some(value.1.company_name),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MerchantId(String);
|
||||
|
||||
impl MerchantId {
|
||||
pub fn new(merchant_id: String) -> UserResult<Self> {
|
||||
let merchant_id = merchant_id.trim().to_lowercase().replace(' ', "_");
|
||||
let is_empty_or_whitespace = merchant_id.is_empty();
|
||||
|
||||
let is_all_valid_characters = merchant_id.chars().all(|x| x.is_alphanumeric() || x == '_');
|
||||
if is_empty_or_whitespace || !is_all_valid_characters {
|
||||
Err(UserErrors::MerchantIdParsingError.into())
|
||||
} else {
|
||||
Ok(Self(merchant_id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_secret(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NewUserMerchant {
|
||||
merchant_id: String,
|
||||
merchant_id: MerchantId,
|
||||
company_name: Option<UserCompanyName>,
|
||||
new_organization: NewUserOrganization,
|
||||
}
|
||||
@ -229,7 +274,7 @@ impl NewUserMerchant {
|
||||
}
|
||||
|
||||
pub fn get_merchant_id(&self) -> String {
|
||||
self.merchant_id.clone()
|
||||
self.merchant_id.get_secret()
|
||||
}
|
||||
|
||||
pub fn get_new_organization(&self) -> NewUserOrganization {
|
||||
@ -293,7 +338,10 @@ impl TryFrom<user_api::ConnectAccountRequest> for NewUserMerchant {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: user_api::ConnectAccountRequest) -> UserResult<Self> {
|
||||
let merchant_id = format!("merchant_{}", common_utils::date_time::now_unix_timestamp());
|
||||
let merchant_id = MerchantId::new(format!(
|
||||
"merchant_{}",
|
||||
common_utils::date_time::now_unix_timestamp()
|
||||
))?;
|
||||
let new_organization = NewUserOrganization::from(value);
|
||||
|
||||
Ok(Self {
|
||||
@ -304,6 +352,45 @@ impl TryFrom<user_api::ConnectAccountRequest> for NewUserMerchant {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<user_api::CreateInternalUserRequest> for NewUserMerchant {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: user_api::CreateInternalUserRequest) -> UserResult<Self> {
|
||||
let merchant_id =
|
||||
MerchantId::new(consts::user_role::INTERNAL_USER_MERCHANT_ID.to_string())?;
|
||||
let new_organization = NewUserOrganization::from(value);
|
||||
|
||||
Ok(Self {
|
||||
company_name: None,
|
||||
merchant_id,
|
||||
new_organization,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type UserMerchantCreateRequestWithToken =
|
||||
(UserFromStorage, user_api::UserMerchantCreate, UserFromToken);
|
||||
|
||||
impl TryFrom<UserMerchantCreateRequestWithToken> for NewUserMerchant {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: UserMerchantCreateRequestWithToken) -> UserResult<Self> {
|
||||
let merchant_id = if matches!(env::which(), env::Env::Production) {
|
||||
MerchantId::new(value.1.company_name.clone())?
|
||||
} else {
|
||||
MerchantId::new(format!(
|
||||
"merchant_{}",
|
||||
common_utils::date_time::now_unix_timestamp()
|
||||
))?
|
||||
};
|
||||
Ok(Self {
|
||||
merchant_id,
|
||||
company_name: Some(UserCompanyName::new(value.1.company_name.clone())?),
|
||||
new_organization: NewUserOrganization::from(value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NewUser {
|
||||
user_id: String,
|
||||
@ -428,6 +515,44 @@ impl TryFrom<user_api::ConnectAccountRequest> for NewUser {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<user_api::CreateInternalUserRequest> for NewUser {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: user_api::CreateInternalUserRequest) -> UserResult<Self> {
|
||||
let user_id = uuid::Uuid::new_v4().to_string();
|
||||
let email = value.email.clone().try_into()?;
|
||||
let name = UserName::new(value.name.clone())?;
|
||||
let password = UserPassword::new(value.password.clone())?;
|
||||
let new_merchant = NewUserMerchant::try_from(value)?;
|
||||
|
||||
Ok(Self {
|
||||
user_id,
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
new_merchant,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<UserMerchantCreateRequestWithToken> for NewUser {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: UserMerchantCreateRequestWithToken) -> Result<Self, Self::Error> {
|
||||
let user = value.0.clone();
|
||||
let new_merchant = NewUserMerchant::try_from(value)?;
|
||||
|
||||
Ok(Self {
|
||||
user_id: user.0.user_id,
|
||||
name: UserName::new(user.0.name)?,
|
||||
email: user.0.email.clone().try_into()?,
|
||||
password: UserPassword::new(user.0.password)?,
|
||||
new_merchant,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserFromStorage(pub storage_user::User);
|
||||
|
||||
impl From<storage_user::User> for UserFromStorage {
|
||||
@ -475,6 +600,23 @@ impl UserFromStorage {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_jwt_auth_token_with_custom_merchant_id(
|
||||
&self,
|
||||
state: AppState,
|
||||
merchant_id: String,
|
||||
org_id: String,
|
||||
) -> UserResult<String> {
|
||||
let role_id = self.get_role_from_db(state.clone()).await?.role_id;
|
||||
AuthToken::new_token(
|
||||
self.0.user_id.clone(),
|
||||
merchant_id,
|
||||
role_id,
|
||||
&state.conf,
|
||||
org_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_role_from_db(&self, state: AppState) -> UserResult<UserRole> {
|
||||
state
|
||||
.store
|
||||
@ -483,3 +625,49 @@ impl UserFromStorage {
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<info::ModuleInfo> for user_role_api::ModuleInfo {
|
||||
type Error = ();
|
||||
fn try_from(value: info::ModuleInfo) -> Result<Self, Self::Error> {
|
||||
let mut permissions = Vec::with_capacity(value.permissions.len());
|
||||
for permission in value.permissions {
|
||||
let permission = permission.try_into()?;
|
||||
permissions.push(permission);
|
||||
}
|
||||
Ok(Self {
|
||||
module: value.module.into(),
|
||||
description: value.description,
|
||||
permissions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<info::PermissionModule> for user_role_api::PermissionModule {
|
||||
fn from(value: info::PermissionModule) -> Self {
|
||||
match value {
|
||||
info::PermissionModule::Payments => Self::Payments,
|
||||
info::PermissionModule::Refunds => Self::Refunds,
|
||||
info::PermissionModule::MerchantAccount => Self::MerchantAccount,
|
||||
info::PermissionModule::Forex => Self::Forex,
|
||||
info::PermissionModule::Connectors => Self::Connectors,
|
||||
info::PermissionModule::Routing => Self::Routing,
|
||||
info::PermissionModule::Analytics => Self::Analytics,
|
||||
info::PermissionModule::Mandates => Self::Mandates,
|
||||
info::PermissionModule::Disputes => Self::Disputes,
|
||||
info::PermissionModule::Files => Self::Files,
|
||||
info::PermissionModule::ThreeDsDecisionManager => Self::ThreeDsDecisionManager,
|
||||
info::PermissionModule::SurchargeDecisionManager => Self::SurchargeDecisionManager,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<info::PermissionInfo> for user_role_api::PermissionInfo {
|
||||
type Error = ();
|
||||
fn try_from(value: info::PermissionInfo) -> Result<Self, Self::Error> {
|
||||
let enum_name = (&value.enum_name).try_into()?;
|
||||
Ok(Self {
|
||||
enum_name,
|
||||
description: value.description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ pub mod storage_partitioning;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user_role;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod verify_connector;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
@ -1,2 +1,51 @@
|
||||
use error_stack::ResultExt;
|
||||
|
||||
use crate::{
|
||||
core::errors::{UserErrors, UserResult},
|
||||
routes::AppState,
|
||||
services::authentication::UserFromToken,
|
||||
types::domain::MerchantAccount,
|
||||
};
|
||||
|
||||
pub mod dashboard_metadata;
|
||||
pub mod password;
|
||||
|
||||
impl UserFromToken {
|
||||
pub async fn get_merchant_account(&self, state: AppState) -> UserResult<MerchantAccount> {
|
||||
let key_store = state
|
||||
.store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
&self.merchant_id,
|
||||
&state.store.get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.current_context().is_db_not_found() {
|
||||
e.change_context(UserErrors::MerchantIdNotFound)
|
||||
} else {
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
})?;
|
||||
let merchant_account = state
|
||||
.store
|
||||
.find_merchant_account_by_merchant_id(&self.merchant_id, &key_store)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.current_context().is_db_not_found() {
|
||||
e.change_context(UserErrors::MerchantIdNotFound)
|
||||
} else {
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
})?;
|
||||
Ok(merchant_account)
|
||||
}
|
||||
|
||||
pub async fn get_user(&self, state: AppState) -> UserResult<diesel_models::user::User> {
|
||||
let user = state
|
||||
.store
|
||||
.find_user_by_id(&self.user_id)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
93
crates/router/src/utils/user_role.rs
Normal file
93
crates/router/src/utils/user_role.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use api_models::user_role as user_role_api;
|
||||
use diesel_models::enums::UserStatus;
|
||||
use error_stack::ResultExt;
|
||||
use router_env::logger;
|
||||
|
||||
use crate::{
|
||||
consts,
|
||||
core::errors::{UserErrors, UserResult},
|
||||
routes::AppState,
|
||||
services::authorization::{
|
||||
permissions::Permission,
|
||||
predefined_permissions::{self, 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 async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserResult<Vec<String>> {
|
||||
Ok(state
|
||||
.store
|
||||
.list_user_roles_by_user_id(user_id)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?
|
||||
.into_iter()
|
||||
.filter_map(|ele| {
|
||||
if ele.status == UserStatus::Active {
|
||||
return Some(ele.merchant_id);
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn validate_role_id(role_id: &str) -> UserResult<()> {
|
||||
if predefined_permissions::is_role_invitable(role_id) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(UserErrors::InvalidRoleId.into())
|
||||
}
|
||||
|
||||
pub fn get_role_name_and_permission_response(
|
||||
role_info: &RoleInfo,
|
||||
) -> Option<(Vec<user_role_api::Permission>, &'static str)> {
|
||||
role_info
|
||||
.get_permissions()
|
||||
.iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<Vec<user_role_api::Permission>, _>>()
|
||||
.ok()
|
||||
.zip(role_info.get_name())
|
||||
}
|
||||
|
||||
impl TryFrom<&Permission> for user_role_api::Permission {
|
||||
type Error = ();
|
||||
fn try_from(value: &Permission) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Permission::PaymentRead => Ok(Self::PaymentRead),
|
||||
Permission::PaymentWrite => Ok(Self::PaymentWrite),
|
||||
Permission::RefundRead => Ok(Self::RefundRead),
|
||||
Permission::RefundWrite => Ok(Self::RefundWrite),
|
||||
Permission::ApiKeyRead => Ok(Self::ApiKeyRead),
|
||||
Permission::ApiKeyWrite => Ok(Self::ApiKeyWrite),
|
||||
Permission::MerchantAccountRead => Ok(Self::MerchantAccountRead),
|
||||
Permission::MerchantAccountWrite => Ok(Self::MerchantAccountWrite),
|
||||
Permission::MerchantConnectorAccountRead => Ok(Self::MerchantConnectorAccountRead),
|
||||
Permission::MerchantConnectorAccountWrite => Ok(Self::MerchantConnectorAccountWrite),
|
||||
Permission::ForexRead => Ok(Self::ForexRead),
|
||||
Permission::RoutingRead => Ok(Self::RoutingRead),
|
||||
Permission::RoutingWrite => Ok(Self::RoutingWrite),
|
||||
Permission::DisputeRead => Ok(Self::DisputeRead),
|
||||
Permission::DisputeWrite => Ok(Self::DisputeWrite),
|
||||
Permission::MandateRead => Ok(Self::MandateRead),
|
||||
Permission::MandateWrite => Ok(Self::MandateWrite),
|
||||
Permission::FileRead => Ok(Self::FileRead),
|
||||
Permission::FileWrite => Ok(Self::FileWrite),
|
||||
Permission::Analytics => Ok(Self::Analytics),
|
||||
Permission::ThreeDsDecisionManagerWrite => Ok(Self::ThreeDsDecisionManagerWrite),
|
||||
Permission::ThreeDsDecisionManagerRead => Ok(Self::ThreeDsDecisionManagerRead),
|
||||
Permission::SurchargeDecisionManagerWrite => Ok(Self::SurchargeDecisionManagerWrite),
|
||||
Permission::SurchargeDecisionManagerRead => Ok(Self::SurchargeDecisionManagerRead),
|
||||
Permission::UsersRead => Ok(Self::UsersRead),
|
||||
Permission::UsersWrite => Ok(Self::UsersWrite),
|
||||
|
||||
Permission::MerchantAccountCreate => {
|
||||
logger::error!("Invalid use of internal permission");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -265,6 +265,20 @@ pub enum Flow {
|
||||
GetMutltipleDashboardMetadata,
|
||||
/// Payment Connector Verify
|
||||
VerifyPaymentConnector,
|
||||
/// Internal user signup
|
||||
InternalUserSignup,
|
||||
/// Switch merchant
|
||||
SwitchMerchant,
|
||||
/// Get permission info
|
||||
GetAuthorizationInfo,
|
||||
/// List roles
|
||||
ListRoles,
|
||||
/// Get role
|
||||
GetRole,
|
||||
/// Update user role
|
||||
UpdateUserRole,
|
||||
/// Create merchant account for user in a org
|
||||
UserMerchantAccountCreate,
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user