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:
Mani Chandra
2023-11-30 20:02:47 +05:30
committed by GitHub
parent 2e2dbe4715
commit 3fa0bdf765
24 changed files with 1207 additions and 46 deletions

View File

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

View File

@ -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
);

View 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
);

View File

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

View File

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

View 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,
}

View File

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

View 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";

View File

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

View File

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

View File

@ -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,7 +35,8 @@ pub async fn connect_account(
.get_jwt_auth_token(state.clone(), user_role.org_id)
.await?;
return Ok(ApplicationResponse::Json(api::ConnectAccountResponse {
return Ok(ApplicationResponse::Json(
user_api::ConnectAccountResponse {
token: Secret::new(jwt_token),
merchant_id: user_role.merchant_id,
name: user_from_db.get_name(),
@ -42,7 +44,8 @@ pub async fn connect_account(
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,7 +97,8 @@ pub async fn connect_account(
logger::info!(?send_email_result);
}
return Ok(ApplicationResponse::Json(api::ConnectAccountResponse {
return Ok(ApplicationResponse::Json(
user_api::ConnectAccountResponse {
token: Secret::new(jwt_token),
merchant_id: user_role.merchant_id,
name: user_from_db.get_name(),
@ -102,7 +106,8 @@ pub async fn connect_account(
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)
}

View 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)
}

View File

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

View File

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

View File

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

View File

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

View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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(())
}
}
}
}

View File

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