From 357e8a007ac5d418c143e90b829d938e7cbcb69e Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:31:55 +0530 Subject: [PATCH] feat(users): add support for tenant level users (#6708) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 20 +- crates/api_models/src/user.rs | 15 + crates/common_enums/src/enums.rs | 1 + crates/common_utils/src/consts.rs | 2 + .../src/query/merchant_account.rs | 20 ++ crates/router/src/analytics.rs | 6 + crates/router/src/consts/user.rs | 2 + crates/router/src/core/user.rs | 278 ++++++++++++++---- crates/router/src/core/user_role.rs | 10 +- crates/router/src/core/user_role/role.rs | 4 +- crates/router/src/db/kafka_store.rs | 13 + crates/router/src/db/merchant_account.rs | 72 +++++ crates/router/src/routes/app.rs | 4 + crates/router/src/routes/lock_utils.rs | 2 + crates/router/src/routes/user.rs | 41 +++ .../src/services/authorization/permissions.rs | 3 +- .../authorization/roles/predefined_roles.rs | 37 ++- crates/router/src/types/domain/user.rs | 110 ++++++- .../src/types/domain/user/decision_manager.rs | 16 +- crates/router/src/utils/user.rs | 38 +++ crates/router/src/utils/user_role.rs | 131 ++++++--- crates/router_env/src/logger/types.rs | 4 + 22 files changed, 701 insertions(+), 128 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 88c084d2f0bc..aab8f518abe7 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -12,15 +12,15 @@ use crate::user::{ }, AcceptInviteFromEmailRequest, AuthSelectRequest, AuthorizeResponse, BeginTotpResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, - CreateUserAuthenticationMethodRequest, ForgotPasswordRequest, GetSsoAuthUrlRequest, - GetUserAuthenticationMethodsRequest, GetUserDetailsResponse, GetUserRoleDetailsRequest, - GetUserRoleDetailsResponseV2, InviteUserRequest, ReInviteUserRequest, RecoveryCodes, - ResetPasswordRequest, RotatePasswordRequest, SendVerifyEmailRequest, SignUpRequest, - SignUpWithMerchantIdRequest, SsoSignInRequest, SwitchMerchantRequest, - SwitchOrganizationRequest, SwitchProfileRequest, TokenResponse, TwoFactorAuthStatusResponse, - TwoFactorStatus, UpdateUserAccountDetailsRequest, UpdateUserAuthenticationMethodRequest, - UserFromEmailRequest, UserMerchantCreate, VerifyEmailRequest, VerifyRecoveryCodeRequest, - VerifyTotpRequest, + CreateTenantUserRequest, CreateUserAuthenticationMethodRequest, ForgotPasswordRequest, + GetSsoAuthUrlRequest, GetUserAuthenticationMethodsRequest, GetUserDetailsResponse, + GetUserRoleDetailsRequest, GetUserRoleDetailsResponseV2, InviteUserRequest, + ReInviteUserRequest, RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest, + SendVerifyEmailRequest, SignUpRequest, SignUpWithMerchantIdRequest, SsoSignInRequest, + SwitchMerchantRequest, SwitchOrganizationRequest, SwitchProfileRequest, TokenResponse, + TwoFactorAuthStatusResponse, TwoFactorStatus, UpdateUserAccountDetailsRequest, + UpdateUserAuthenticationMethodRequest, UserFromEmailRequest, UserMerchantCreate, + UserOrgMerchantCreateRequest, VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest, }; common_utils::impl_api_event_type!( @@ -37,6 +37,8 @@ common_utils::impl_api_event_type!( SwitchMerchantRequest, SwitchProfileRequest, CreateInternalUserRequest, + CreateTenantUserRequest, + UserOrgMerchantCreateRequest, UserMerchantCreate, AuthorizeResponse, ConnectAccountRequest, diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index de0116058cb9..e61696803a20 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -115,6 +115,21 @@ pub struct CreateInternalUserRequest { pub password: Secret, } +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct CreateTenantUserRequest { + pub name: Secret, + pub email: pii::Email, + pub password: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct UserOrgMerchantCreateRequest { + pub organization_name: Secret, + pub organization_details: Option, + pub metadata: Option, + pub merchant_name: Secret, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct UserMerchantCreate { pub company_name: String, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index c3c876b22aa6..e94b40a4af85 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -3207,6 +3207,7 @@ pub enum ApiVersion { #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum EntityType { + Tenant = 3, Organization = 2, Merchant = 1, Profile = 0, diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index b43efcb1feb1..e430d59e54dd 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -119,6 +119,8 @@ pub const MAX_ALLOWED_MERCHANT_NAME_LENGTH: usize = 64; /// Default locale pub const DEFAULT_LOCALE: &str = "en"; +/// Role ID for Tenant Admin +pub const ROLE_ID_TENANT_ADMIN: &str = "tenant_admin"; /// Role ID for Org Admin pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; /// Role ID for Internal View Only diff --git a/crates/diesel_models/src/query/merchant_account.rs b/crates/diesel_models/src/query/merchant_account.rs index fd5a171b7eb8..03945646014b 100644 --- a/crates/diesel_models/src/query/merchant_account.rs +++ b/crates/diesel_models/src/query/merchant_account.rs @@ -121,6 +121,26 @@ impl MerchantAccount { .await } + pub async fn list_all_merchant_accounts( + conn: &PgPooledConn, + limit: u32, + offset: Option, + ) -> StorageResult> { + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + _, + >( + conn, + dsl_identifier.ne_all(vec![""]), + Some(i64::from(limit)), + offset.map(i64::from), + None, + ) + .await + } + pub async fn update_all_merchant_accounts( conn: &PgPooledConn, merchant_account: MerchantAccountUpdateInternal, diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index d957c3071ff0..752c3a52284a 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -1931,6 +1931,9 @@ pub mod routes { EntityType::Organization => Some(AuthInfo::OrgLevel { org_id: user_role.org_id.clone()?, }), + EntityType::Tenant => Some(AuthInfo::OrgLevel { + org_id: auth.org_id.clone(), + }), }) }) .collect(); @@ -2054,6 +2057,9 @@ pub mod routes { EntityType::Organization => Some(AuthInfo::OrgLevel { org_id: user_role.org_id.clone()?, }), + EntityType::Tenant => Some(AuthInfo::OrgLevel { + org_id: auth.org_id.clone(), + }), }) }) .collect(); diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs index 32ca4ad31d7d..d4eb12e39bf9 100644 --- a/crates/router/src/consts/user.rs +++ b/crates/router/src/consts/user.rs @@ -19,6 +19,8 @@ pub const TOTP_TOLERANCE: u8 = 1; pub const TOTP_MAX_ATTEMPTS: u8 = 4; /// Number of maximum attempts user has for recovery code pub const RECOVERY_CODE_MAX_ATTEMPTS: u8 = 4; +/// The default number of organizations to fetch for a tenant-level user +pub const ORG_LIST_LIMIT_FOR_TENANT: u32 = 20; pub const MAX_PASSWORD_LENGTH: usize = 70; pub const MIN_PASSWORD_LENGTH: usize = 8; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index e4ac902f7baa..f99c44cf2980 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -25,9 +25,13 @@ use router_env::logger; #[cfg(not(feature = "email"))] use user_api::dashboard_metadata::SetMetaDataRequest; +#[cfg(feature = "v1")] +use super::admin; use super::errors::{StorageErrorExt, UserErrors, UserResponse, UserResult}; #[cfg(feature = "email")] use crate::services::email::types as email_types; +#[cfg(feature = "v1")] +use crate::types::transformers::ForeignFrom; use crate::{ consts, core::encryption::send_request_to_key_service_for_user, @@ -648,6 +652,12 @@ async fn handle_existing_user_invitation( } let (org_id, merchant_id, profile_id) = match role_info.get_entity_type() { + EntityType::Tenant => { + return Err(UserErrors::InvalidRoleOperationWithMessage( + "Tenant roles are not allowed for this operation".to_string(), + ) + .into()); + } EntityType::Organization => (Some(&user_from_token.org_id), None, None), EntityType::Merchant => ( Some(&user_from_token.org_id), @@ -701,6 +711,12 @@ async fn handle_existing_user_invitation( }; let _user_role = match role_info.get_entity_type() { + EntityType::Tenant => { + return Err(UserErrors::InvalidRoleOperationWithMessage( + "Tenant roles are not allowed for this operation".to_string(), + ) + .into()); + } EntityType::Organization => { user_role .add_entity(domain::OrganizationLevel { @@ -747,6 +763,12 @@ async fn handle_existing_user_invitation( { let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; let entity = match role_info.get_entity_type() { + EntityType::Tenant => { + return Err(UserErrors::InvalidRoleOperationWithMessage( + "Tenant roles are not allowed for this operation".to_string(), + ) + .into()); + } EntityType::Organization => email_types::Entity { entity_id: user_from_token.org_id.get_string_repr().to_owned(), entity_type: EntityType::Organization, @@ -830,6 +852,12 @@ async fn handle_new_user_invitation( }; let _user_role = match role_info.get_entity_type() { + EntityType::Tenant => { + return Err(UserErrors::InvalidRoleOperationWithMessage( + "Tenant roles are not allowed for this operation".to_string(), + ) + .into()); + } EntityType::Organization => { user_role .add_entity(domain::OrganizationLevel { @@ -880,6 +908,12 @@ async fn handle_new_user_invitation( let _ = req_state.clone(); let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; let entity = match role_info.get_entity_type() { + EntityType::Tenant => { + return Err(UserErrors::InvalidRoleOperationWithMessage( + "Tenant roles are not allowed for this operation".to_string(), + ) + .into()); + } EntityType::Organization => email_types::Entity { entity_id: user_from_token.org_id.get_string_repr().to_owned(), entity_type: EntityType::Organization, @@ -1235,6 +1269,83 @@ pub async fn create_internal_user( Ok(ApplicationResponse::StatusOk) } +pub async fn create_tenant_user( + state: SessionState, + request: user_api::CreateTenantUserRequest, +) -> UserResponse<()> { + let key_manager_state = &(&state).into(); + + let (merchant_id, org_id) = state + .store + .list_merchant_and_org_ids(key_manager_state, 1, None) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to get merchants list for org")? + .pop() + .ok_or(UserErrors::InternalServerError) + .attach_printable("No merchants found in the tenancy")?; + + let new_user = domain::NewUser::try_from(( + request, + domain::MerchantAccountIdentifier { + merchant_id, + org_id, + }, + ))?; + let mut store_user: storage_user::UserNew = new_user.clone().try_into()?; + store_user.set_is_verified(true); + + state + .global_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 + .get_no_level_user_role( + common_utils::consts::ROLE_ID_TENANT_ADMIN.to_string(), + UserStatus::Active, + ) + .add_entity(domain::TenantLevel { + tenant_id: state.tenant.tenant_id.clone(), + }) + .insert_in_v2(&state) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(ApplicationResponse::StatusOk) +} + +#[cfg(feature = "v1")] +pub async fn create_org_merchant_for_user( + state: SessionState, + req: user_api::UserOrgMerchantCreateRequest, +) -> UserResponse<()> { + let db_organization = ForeignFrom::foreign_from(req.clone()); + let org: diesel_models::organization::Organization = state + .store + .insert_organization(db_organization) + .await + .change_context(UserErrors::InternalServerError)?; + + let merchant_account_create_request = + utils::user::create_merchant_account_request_for_org(req, org)?; + + admin::create_merchant_account(state.clone(), merchant_account_create_request) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error while creating a merchant")?; + + Ok(ApplicationResponse::StatusOk) +} + pub async fn create_merchant_account( state: SessionState, user_from_token: auth::UserFromToken, @@ -1352,7 +1463,7 @@ pub async fn list_user_roles_details( merchant.push(merchant_id.clone()); merchant_profile.push((merchant_id, profile_id)) } - EntityType::Organization => (), + EntityType::Tenant | EntityType::Organization => (), }; Ok::<_, error_stack::Report>((merchant, merchant_profile)) @@ -1438,7 +1549,7 @@ pub async fn list_user_roles_details( .ok_or(UserErrors::InternalServerError)?; let (merchant, profile) = match entity_type { - EntityType::Organization => (None, None), + EntityType::Tenant | EntityType::Organization => (None, None), EntityType::Merchant => { let merchant_id = &user_role .merchant_id @@ -2437,28 +2548,44 @@ pub async fn list_orgs_for_user( ) .into()); } - - let orgs = state - .global_store - .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { - user_id: user_from_token.user_id.as_str(), - tenant_id: user_from_token - .tenant_id - .as_ref() - .unwrap_or(&state.tenant.tenant_id), - org_id: None, - merchant_id: None, - profile_id: None, - entity_id: None, - version: None, - status: Some(UserStatus::Active), - limit: None, - }) - .await - .change_context(UserErrors::InternalServerError)? - .into_iter() - .filter_map(|user_role| user_role.org_id) - .collect::>(); + let orgs = match role_info.get_entity_type() { + EntityType::Tenant => { + let key_manager_state = &(&state).into(); + state + .store + .list_merchant_and_org_ids( + key_manager_state, + consts::user::ORG_LIST_LIMIT_FOR_TENANT, + None, + ) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .map(|(_, org_id)| org_id) + .collect::>() + } + EntityType::Organization | EntityType::Merchant | EntityType::Profile => state + .global_store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: user_from_token.user_id.as_str(), + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + org_id: None, + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: Some(UserStatus::Active), + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .filter_map(|user_role| user_role.org_id) + .collect::>(), + }; let resp = futures::future::try_join_all( orgs.iter() @@ -2501,7 +2628,7 @@ pub async fn list_merchants_for_user_in_org( } let merchant_accounts = match role_info.get_entity_type() { - EntityType::Organization => state + EntityType::Tenant | EntityType::Organization => state .store .list_merchant_accounts_by_organization_id(&(&state).into(), &user_from_token.org_id) .await @@ -2580,7 +2707,7 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account( .await .change_context(UserErrors::InternalServerError)?; let profiles = match role_info.get_entity_type() { - EntityType::Organization | EntityType::Merchant => state + EntityType::Tenant | EntityType::Organization | EntityType::Merchant => state .store .list_profile_by_merchant_id( key_manager_state, @@ -2670,39 +2797,80 @@ pub async fn switch_org_for_user( .into()); } - let user_role = state - .global_store - .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { - user_id: &user_from_token.user_id, - tenant_id: user_from_token - .tenant_id - .as_ref() - .unwrap_or(&state.tenant.tenant_id), - org_id: Some(&request.org_id), - merchant_id: None, - profile_id: None, - entity_id: None, - version: None, - status: Some(UserStatus::Active), - limit: Some(1), - }) - .await - .change_context(UserErrors::InternalServerError) - .attach_printable("Failed to list user roles by user_id and org_id")? - .pop() - .ok_or(UserErrors::InvalidRoleOperationWithMessage( - "No user role found for the requested org_id".to_string(), - ))?; + let (merchant_id, profile_id, role_id) = match role_info.get_entity_type() { + EntityType::Tenant => { + let merchant_id = state + .store + .list_merchant_accounts_by_organization_id(&(&state).into(), &request.org_id) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to get merchant list for org")? + .pop() + .ok_or(UserErrors::InvalidRoleOperation) + .attach_printable("No merchants found for the org id")? + .get_id() + .to_owned(); - let (merchant_id, profile_id) = - utils::user_role::get_single_merchant_id_and_profile_id(&state, &user_role).await?; + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &(&state).into(), + &merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + let profile_id = state + .store + .list_profile_by_merchant_id(&(&state).into(), &key_store, &merchant_id) + .await + .change_context(UserErrors::InternalServerError)? + .pop() + .ok_or(UserErrors::InternalServerError)? + .get_id() + .to_owned(); + + (merchant_id, profile_id, user_from_token.role_id) + } + EntityType::Organization | EntityType::Merchant | EntityType::Profile => { + let user_role = state + .global_store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: &user_from_token.user_id, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + org_id: Some(&request.org_id), + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: Some(UserStatus::Active), + limit: Some(1), + }) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to list user roles by user_id and org_id")? + .pop() + .ok_or(UserErrors::InvalidRoleOperationWithMessage( + "No user role found for the requested org_id".to_string(), + ))?; + + let (merchant_id, profile_id) = + utils::user_role::get_single_merchant_id_and_profile_id(&state, &user_role).await?; + + (merchant_id, profile_id, user_role.role_id) + } + }; let token = utils::user::generate_jwt_auth_token_with_attributes( &state, user_from_token.user_id, merchant_id.clone(), request.org_id.clone(), - user_role.role_id.clone(), + role_id.clone(), profile_id.clone(), user_from_token.tenant_id, ) @@ -2710,7 +2878,7 @@ pub async fn switch_org_for_user( utils::user_role::set_role_permissions_in_cache_by_role_id_merchant_id_org_id( &state, - &user_role.role_id, + &role_id, &merchant_id, &request.org_id, ) @@ -2794,7 +2962,7 @@ pub async fn switch_merchant_for_user_in_org( } else { // Match based on the other entity types match role_info.get_entity_type() { - EntityType::Organization => { + EntityType::Tenant | EntityType::Organization => { let merchant_key_store = state .store .get_merchant_key_store_by_merchant_id( @@ -2937,7 +3105,7 @@ pub async fn switch_profile_for_user_in_org_and_merchant( .attach_printable("Failed to retrieve role information")?; let (profile_id, role_id) = match role_info.get_entity_type() { - EntityType::Organization | EntityType::Merchant => { + EntityType::Tenant | EntityType::Organization | EntityType::Merchant => { let merchant_key_store = state .store .get_merchant_key_store_by_merchant_id( diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 273b863afe8d..31ec665b2ab2 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -698,7 +698,7 @@ pub async fn list_users_in_lineage( requestor_role_info.get_entity_type(), request.entity_type, )? { - EntityType::Organization => { + EntityType::Tenant | EntityType::Organization => { utils::user_role::fetch_user_roles_by_payload( &state, ListUserRolesByOrgIdPayload { @@ -871,6 +871,10 @@ pub async fn list_invitations_for_user( .attach_printable("Failed to compute entity id and type")?; match entity_type { + EntityType::Tenant => { + return Err(report!(UserErrors::InternalServerError)) + .attach_printable("Tenant roles are not allowed for this operation"); + } EntityType::Organization => org_ids.push( user_role .org_id @@ -975,6 +979,10 @@ pub async fn list_invitations_for_user( .attach_printable("Failed to compute entity id and type")?; let entity_name = match entity_type { + EntityType::Tenant => { + return Err(report!(UserErrors::InternalServerError)) + .attach_printable("Tenant roles are not allowed for this operation"); + } EntityType::Organization => user_role .org_id .as_ref() diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs index c01d585feb76..9f251f55bdf0 100644 --- a/crates/router/src/core/user_role/role.rs +++ b/crates/router/src/core/user_role/role.rs @@ -274,7 +274,7 @@ pub async fn list_roles_with_info( let user_role_entity = user_role_info.get_entity_type(); let custom_roles = match utils::user_role::get_min_entity(user_role_entity, request.entity_type)? { - EntityType::Organization => state + EntityType::Tenant | EntityType::Organization => state .store .list_roles_for_org_by_parameters( &user_from_token.org_id, @@ -347,7 +347,7 @@ pub async fn list_roles_at_entity_level( .collect::>(); let custom_roles = match req.entity_type { - EntityType::Organization => state + EntityType::Tenant | EntityType::Organization => state .store .list_roles_for_org_by_parameters( &user_from_token.org_id, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 9a284f1399d5..47c9adafb718 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1048,6 +1048,19 @@ impl MerchantAccountInterface for KafkaStore { .list_multiple_merchant_accounts(state, merchant_ids) .await } + + #[cfg(feature = "olap")] + async fn list_merchant_and_org_ids( + &self, + state: &KeyManagerState, + limit: u32, + offset: Option, + ) -> CustomResult, errors::StorageError> + { + self.diesel_store + .list_merchant_and_org_ids(state, limit, offset) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 6eb355e4434f..4f4a3f1cf00b 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -87,6 +87,20 @@ where state: &KeyManagerState, merchant_ids: Vec, ) -> CustomResult, errors::StorageError>; + + #[cfg(feature = "olap")] + async fn list_merchant_and_org_ids( + &self, + state: &KeyManagerState, + limit: u32, + offset: Option, + ) -> CustomResult< + Vec<( + common_utils::id_type::MerchantId, + common_utils::id_type::OrganizationId, + )>, + errors::StorageError, + >; } #[async_trait::async_trait] @@ -411,6 +425,37 @@ impl MerchantAccountInterface for Store { Ok(merchant_accounts) } + #[cfg(feature = "olap")] + #[instrument(skip_all)] + async fn list_merchant_and_org_ids( + &self, + _state: &KeyManagerState, + limit: u32, + offset: Option, + ) -> CustomResult< + Vec<( + common_utils::id_type::MerchantId, + common_utils::id_type::OrganizationId, + )>, + errors::StorageError, + > { + let conn = connection::pg_connection_read(self).await?; + let encrypted_merchant_accounts = + storage::MerchantAccount::list_all_merchant_accounts(&conn, limit, offset) + .await + .map_err(|error| report!(errors::StorageError::from(error)))?; + + let merchant_and_org_ids = encrypted_merchant_accounts + .into_iter() + .map(|merchant_account| { + let merchant_id = merchant_account.get_id().clone(); + let org_id = merchant_account.organization_id; + (merchant_id, org_id) + }) + .collect(); + Ok(merchant_and_org_ids) + } + async fn update_all_merchant_account( &self, merchant_account: storage::MerchantAccountUpdate, @@ -694,6 +739,33 @@ impl MerchantAccountInterface for MockDb { .into_iter() .collect() } + + #[cfg(feature = "olap")] + async fn list_merchant_and_org_ids( + &self, + _state: &KeyManagerState, + limit: u32, + offset: Option, + ) -> CustomResult< + Vec<( + common_utils::id_type::MerchantId, + common_utils::id_type::OrganizationId, + )>, + errors::StorageError, + > { + let accounts = self.merchant_accounts.lock().await; + let limit = limit.try_into().unwrap_or(accounts.len()); + let offset = offset.unwrap_or(0).try_into().unwrap_or(0); + + let merchant_and_org_ids = accounts + .iter() + .skip(offset) + .take(limit) + .map(|account| (account.get_id().clone(), account.organization_id.clone())) + .collect::>(); + + Ok(merchant_and_org_ids) + } } #[cfg(feature = "accounts_cache")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index be24084bcc05..ad0b42cd933e 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1887,6 +1887,10 @@ impl User { .service( web::resource("/internal_signup").route(web::post().to(user::internal_user_signup)), ) + .service( + web::resource("/tenant_signup").route(web::post().to(user::create_tenant_user)), + ) + .service(web::resource("/create_org").route(web::post().to(user::user_org_create))) .service( web::resource("/create_merchant") .route(web::post().to(user::user_merchant_account_create)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 762a52272079..fb8d14ce8a1f 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -224,9 +224,11 @@ impl From for ApiIdentifier { | Flow::GetMultipleDashboardMetadata | Flow::VerifyPaymentConnector | Flow::InternalUserSignup + | Flow::TenantUserCreate | Flow::SwitchOrg | Flow::SwitchMerchantV2 | Flow::SwitchProfile + | Flow::UserOrgMerchantCreate | Flow::UserMerchantAccountCreate | Flow::GenerateSampleData | Flow::DeleteSampleData diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index d5f31be39659..c32746c87185 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -229,6 +229,47 @@ pub async fn internal_user_signup( .await } +pub async fn create_tenant_user( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::TenantUserCreate; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, _, req, _| user_core::create_tenant_user(state, req), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "v1")] +pub async fn user_org_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserOrgMerchantCreate; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, _auth: auth::UserFromToken, json_payload, _| { + user_core::create_org_merchant_for_user(state, json_payload) + }, + &auth::JWTAuth { + permission: Permission::TenantAccountWrite, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn user_merchant_account_create( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs index b9872b371e07..04d3d4367360 100644 --- a/crates/router/src/services/authorization/permissions.rs +++ b/crates/router/src/services/authorization/permissions.rs @@ -33,7 +33,7 @@ generate_permissions! { }, Account: { scopes: [Read, Write], - entities: [Profile, Merchant, Organization] + entities: [Profile, Merchant, Organization, Tenant] }, Connector: { scopes: [Read, Write], @@ -125,6 +125,7 @@ pub fn get_resource_name(resource: Resource, entity_type: EntityType) -> &'stati (Resource::Account, EntityType::Profile) => "Business Profile Account", (Resource::Account, EntityType::Merchant) => "Merchant Account", (Resource::Account, EntityType::Organization) => "Organization Account", + (Resource::Account, EntityType::Tenant) => "Tenant Account", } } diff --git a/crates/router/src/services/authorization/roles/predefined_roles.rs b/crates/router/src/services/authorization/roles/predefined_roles.rs index 9c67c12f5277..fe7498de9eba 100644 --- a/crates/router/src/services/authorization/roles/predefined_roles.rs +++ b/crates/router/src/services/authorization/roles/predefined_roles.rs @@ -68,7 +68,42 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| }, ); - // Merchant Roles + // Tenant Roles + roles.insert( + common_utils::consts::ROLE_ID_TENANT_ADMIN, + RoleInfo { + groups: vec![ + PermissionGroup::OperationsView, + PermissionGroup::OperationsManage, + PermissionGroup::ConnectorsView, + PermissionGroup::ConnectorsManage, + PermissionGroup::WorkflowsView, + PermissionGroup::WorkflowsManage, + PermissionGroup::AnalyticsView, + PermissionGroup::UsersView, + PermissionGroup::UsersManage, + PermissionGroup::MerchantDetailsView, + PermissionGroup::AccountView, + PermissionGroup::MerchantDetailsManage, + PermissionGroup::AccountManage, + PermissionGroup::OrganizationManage, + PermissionGroup::ReconOpsView, + PermissionGroup::ReconOpsManage, + PermissionGroup::ReconReportsView, + PermissionGroup::ReconReportsManage, + ], + role_id: common_utils::consts::ROLE_ID_TENANT_ADMIN.to_string(), + role_name: "tenant_admin".to_string(), + scope: RoleScope::Organization, + entity_type: EntityType::Tenant, + is_invitable: false, + is_deletable: false, + is_updatable: false, + is_internal: false, + }, + ); + + // Organization Roles roles.insert( common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN, RoleInfo { diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 21212bf6b626..6efaac7bfedc 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -312,6 +312,40 @@ impl From for NewUserOrganization { } } +impl From<(user_api::CreateTenantUserRequest, MerchantAccountIdentifier)> for NewUserOrganization { + fn from( + (_value, merchant_account_identifier): ( + user_api::CreateTenantUserRequest, + MerchantAccountIdentifier, + ), + ) -> Self { + let new_organization = api_org::OrganizationNew { + org_id: merchant_account_identifier.org_id, + org_name: None, + }; + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + +impl ForeignFrom + for diesel_models::organization::OrganizationNew +{ + fn foreign_from(item: api_models::user::UserOrgMerchantCreateRequest) -> Self { + let org_id = id_type::OrganizationId::default(); + let api_models::user::UserOrgMerchantCreateRequest { + organization_name, + organization_details, + metadata, + .. + } = item; + let mut org_new_db = Self::new(org_id, Some(organization_name.expose())); + org_new_db.organization_details = organization_details; + org_new_db.metadata = metadata; + org_new_db + } +} + #[derive(Clone)] pub struct MerchantId(String); @@ -535,6 +569,18 @@ impl TryFrom for NewUserMerchant { } } +impl From<(user_api::CreateTenantUserRequest, MerchantAccountIdentifier)> for NewUserMerchant { + fn from(value: (user_api::CreateTenantUserRequest, MerchantAccountIdentifier)) -> Self { + let merchant_id = value.1.merchant_id.clone(); + let new_organization = NewUserOrganization::from(value); + Self { + company_name: None, + merchant_id, + new_organization, + } + } +} + type UserMerchantCreateRequestWithToken = (UserFromStorage, user_api::UserMerchantCreate, UserFromToken); @@ -555,6 +601,12 @@ impl TryFrom for NewUserMerchant { } } +#[derive(Debug, Clone)] +pub struct MerchantAccountIdentifier { + pub merchant_id: id_type::MerchantId, + pub org_id: id_type::OrganizationId, +} + #[derive(Clone)] pub struct NewUser { user_id: String, @@ -856,6 +908,34 @@ impl TryFrom for NewUser { } } +impl TryFrom<(user_api::CreateTenantUserRequest, MerchantAccountIdentifier)> for NewUser { + type Error = error_stack::Report; + + fn try_from( + (value, merchant_account_identifier): ( + user_api::CreateTenantUserRequest, + MerchantAccountIdentifier, + ), + ) -> UserResult { + 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 = NewUserPassword { + password: UserPassword::new(value.password.clone())?, + is_temporary: false, + }; + let new_merchant = NewUserMerchant::from((value, merchant_account_identifier)); + + Ok(Self { + user_id, + name, + email, + password: Some(password), + new_merchant, + }) + } +} + #[derive(Clone)] pub struct UserFromStorage(pub storage_user::User); @@ -1108,6 +1188,11 @@ impl RecoveryCodes { #[derive(Clone)] pub struct NoLevel; +#[derive(Clone)] +pub struct TenantLevel { + pub tenant_id: id_type::TenantId, +} + #[derive(Clone)] pub struct OrganizationLevel { pub tenant_id: id_type::TenantId, @@ -1161,20 +1246,33 @@ impl NewUserRole { pub struct EntityInfo { tenant_id: id_type::TenantId, - org_id: id_type::OrganizationId, + org_id: Option, merchant_id: Option, profile_id: Option, entity_id: String, entity_type: EntityType, } +impl From for EntityInfo { + fn from(value: TenantLevel) -> Self { + Self { + entity_id: value.tenant_id.get_string_repr().to_owned(), + entity_type: EntityType::Tenant, + tenant_id: value.tenant_id, + org_id: None, + merchant_id: None, + profile_id: None, + } + } +} + impl From for EntityInfo { fn from(value: OrganizationLevel) -> Self { Self { entity_id: value.org_id.get_string_repr().to_owned(), entity_type: EntityType::Organization, tenant_id: value.tenant_id, - org_id: value.org_id, + org_id: Some(value.org_id), merchant_id: None, profile_id: None, } @@ -1187,9 +1285,9 @@ impl From for EntityInfo { entity_id: value.merchant_id.get_string_repr().to_owned(), entity_type: EntityType::Merchant, tenant_id: value.tenant_id, - org_id: value.org_id, - profile_id: None, + org_id: Some(value.org_id), merchant_id: Some(value.merchant_id), + profile_id: None, } } } @@ -1200,7 +1298,7 @@ impl From for EntityInfo { entity_id: value.profile_id.get_string_repr().to_owned(), entity_type: EntityType::Profile, tenant_id: value.tenant_id, - org_id: value.org_id, + org_id: Some(value.org_id), merchant_id: Some(value.merchant_id), profile_id: Some(value.profile_id), } @@ -1220,7 +1318,7 @@ where last_modified_by: self.last_modified_by, created_at: self.created_at, last_modified: self.last_modified, - org_id: Some(entity.org_id), + org_id: entity.org_id, merchant_id: entity.merchant_id, profile_id: entity.profile_id, entity_id: Some(entity.entity_id), diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs index 10990da6ccbd..519edf4e9ce5 100644 --- a/crates/router/src/types/domain/user/decision_manager.rs +++ b/crates/router/src/types/domain/user/decision_manager.rs @@ -1,7 +1,7 @@ use common_enums::TokenPurpose; use common_utils::id_type; use diesel_models::{enums::UserStatus, user_role::UserRole}; -use error_stack::{report, ResultExt}; +use error_stack::ResultExt; use masking::Secret; use super::UserFromStorage; @@ -124,18 +124,18 @@ impl JWTFlow { next_flow: &NextFlow, user_role: &UserRole, ) -> UserResult> { - let (merchant_id, profile_id) = - utils::user_role::get_single_merchant_id_and_profile_id(state, user_role).await?; + let org_id = utils::user_role::get_single_org_id(state, user_role).await?; + let merchant_id = + utils::user_role::get_single_merchant_id(state, user_role, &org_id).await?; + let profile_id = + utils::user_role::get_single_profile_id(state, user_role, &merchant_id).await?; + auth::AuthToken::new_token( next_flow.user.get_user_id().to_string(), merchant_id, user_role.role_id.clone(), &state.conf, - user_role - .org_id - .clone() - .ok_or(report!(UserErrors::InternalServerError)) - .attach_printable("org_id not found")?, + org_id, profile_id, Some(user_role.tenant_id.clone()), ) diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index e2c251418084..9886705ec66b 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -5,9 +5,11 @@ use common_enums::UserAuthType; use common_utils::{ encryption::Encryption, errors::CustomResult, id_type, type_name, types::keymanager::Identifier, }; +use diesel_models::{organization, organization::OrganizationBridge}; use error_stack::ResultExt; use masking::{ExposeInterface, Secret}; use redis_interface::RedisConnectionPool; +use router_env::env; use crate::{ consts::user::{REDIS_SSO_PREFIX, REDIS_SSO_TTL}, @@ -279,3 +281,39 @@ pub fn is_sso_auth_type(auth_type: UserAuthType) -> bool { UserAuthType::Password | UserAuthType::MagicLink => false, } } + +#[cfg(feature = "v1")] +pub fn create_merchant_account_request_for_org( + req: user_api::UserOrgMerchantCreateRequest, + org: organization::Organization, +) -> UserResult { + let merchant_id = if matches!(env::which(), env::Env::Production) { + id_type::MerchantId::try_from(domain::MerchantId::new(req.merchant_name.clone().expose())?)? + } else { + id_type::MerchantId::new_from_unix_timestamp() + }; + + let company_name = domain::UserCompanyName::new(req.merchant_name.expose())?; + Ok(api_models::admin::MerchantAccountCreate { + merchant_id, + metadata: None, + locker_id: None, + return_url: None, + merchant_name: Some(Secret::new(company_name.get_secret())), + webhook_details: None, + publishable_key: None, + organization_id: Some(org.get_organization_id()), + merchant_details: None, + routing_algorithm: None, + parent_merchant_id: None, + sub_merchants_enabled: None, + frm_routing_algorithm: None, + #[cfg(feature = "payouts")] + payout_routing_algorithm: None, + primary_business_details: None, + payment_response_hash_key: None, + enable_payment_response_hash: None, + redirect_to_merchant_with_http_post: None, + pm_collect_link_config: None, + }) +} diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index 8acab4364911..3412219d0e97 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -13,7 +13,10 @@ use storage_impl::errors::StorageError; use crate::{ consts, core::errors::{UserErrors, UserResult}, - db::user_role::{ListUserRolesByOrgIdPayload, ListUserRolesByUserIdPayload}, + db::{ + errors::StorageErrorExt, + user_role::{ListUserRolesByOrgIdPayload, ListUserRolesByUserIdPayload}, + }, routes::SessionState, services::authorization::{self as authz, roles}, types::domain, @@ -179,30 +182,54 @@ pub async fn update_v1_and_v2_user_roles_in_db( (updated_v1_role, updated_v2_role) } +pub async fn get_single_org_id( + state: &SessionState, + user_role: &UserRole, +) -> UserResult { + let (_, entity_type) = user_role + .get_entity_id_and_type() + .ok_or(UserErrors::InternalServerError)?; + match entity_type { + EntityType::Tenant => Ok(state + .store + .list_merchant_and_org_ids(&state.into(), 1, None) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to get merchants list for org")? + .pop() + .ok_or(UserErrors::InternalServerError) + .attach_printable("No merchants to get merchant or org id")? + .1), + EntityType::Organization | EntityType::Merchant | EntityType::Profile => user_role + .org_id + .clone() + .ok_or(UserErrors::InternalServerError) + .attach_printable("Org_id not found"), + } +} + pub async fn get_single_merchant_id( state: &SessionState, user_role: &UserRole, + org_id: &id_type::OrganizationId, ) -> UserResult { - match user_role.entity_type { - Some(EntityType::Organization) => Ok(state + let (_, entity_type) = user_role + .get_entity_id_and_type() + .ok_or(UserErrors::InternalServerError)?; + match entity_type { + EntityType::Tenant | EntityType::Organization => Ok(state .store - .list_merchant_accounts_by_organization_id( - &state.into(), - user_role - .org_id - .as_ref() - .ok_or(UserErrors::InternalServerError) - .attach_printable("org_id not found")?, - ) + .list_merchant_accounts_by_organization_id(&state.into(), org_id) .await - .change_context(UserErrors::InternalServerError) - .attach_printable("Failed to get merchant list for org")? + .to_not_found_response(UserErrors::InvalidRoleOperationWithMessage( + "Invalid Org Id".to_string(), + ))? .first() .ok_or(UserErrors::InternalServerError) .attach_printable("No merchants found for org_id")? .get_id() .clone()), - Some(EntityType::Merchant) | Some(EntityType::Profile) | None => user_role + EntityType::Merchant | EntityType::Profile => user_role .merchant_id .clone() .ok_or(UserErrors::InternalServerError) @@ -210,6 +237,44 @@ pub async fn get_single_merchant_id( } } +pub async fn get_single_profile_id( + state: &SessionState, + user_role: &UserRole, + merchant_id: &id_type::MerchantId, +) -> UserResult { + let (_, entity_type) = user_role + .get_entity_id_and_type() + .ok_or(UserErrors::InternalServerError)?; + match entity_type { + EntityType::Tenant | EntityType::Organization | EntityType::Merchant => { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &state.into(), + merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(state + .store + .list_profile_by_merchant_id(&state.into(), &key_store, merchant_id) + .await + .change_context(UserErrors::InternalServerError)? + .pop() + .ok_or(UserErrors::InternalServerError)? + .get_id() + .to_owned()) + } + EntityType::Profile => user_role + .profile_id + .clone() + .ok_or(UserErrors::InternalServerError) + .attach_printable("profile_id not found"), + } +} + pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( state: &SessionState, user_id: &str, @@ -224,6 +289,10 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( )>, > { match entity_type { + EntityType::Tenant => Err(UserErrors::InvalidRoleOperationWithMessage( + "Tenant roles are not allowed for this operation".to_string(), + ) + .into()), EntityType::Organization => { let Ok(org_id) = id_type::OrganizationId::try_from(std::borrow::Cow::from(entity_id.clone())) @@ -373,37 +442,9 @@ pub async fn get_single_merchant_id_and_profile_id( state: &SessionState, user_role: &UserRole, ) -> UserResult<(id_type::MerchantId, id_type::ProfileId)> { - let merchant_id = get_single_merchant_id(state, user_role).await?; - let (_, entity_type) = user_role - .get_entity_id_and_type() - .ok_or(UserErrors::InternalServerError)?; - let profile_id = match entity_type { - EntityType::Organization | EntityType::Merchant => { - let key_store = state - .store - .get_merchant_key_store_by_merchant_id( - &state.into(), - &merchant_id, - &state.store.get_master_key().to_vec().into(), - ) - .await - .change_context(UserErrors::InternalServerError)?; - - state - .store - .list_profile_by_merchant_id(&state.into(), &key_store, &merchant_id) - .await - .change_context(UserErrors::InternalServerError)? - .pop() - .ok_or(UserErrors::InternalServerError)? - .get_id() - .to_owned() - } - EntityType::Profile => user_role - .profile_id - .clone() - .ok_or(UserErrors::InternalServerError)?, - }; + let org_id = get_single_org_id(state, user_role).await?; + let merchant_id = get_single_merchant_id(state, user_role, &org_id).await?; + let profile_id = get_single_profile_id(state, user_role, &merchant_id).await?; Ok((merchant_id, profile_id)) } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 27ddc10766d2..1394a1bf3fb2 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -361,6 +361,8 @@ pub enum Flow { VerifyPaymentConnector, /// Internal user signup InternalUserSignup, + /// Create tenant level user + TenantUserCreate, /// Switch org SwitchOrg, /// Switch merchant v2 @@ -391,6 +393,8 @@ pub enum Flow { UpdateUserRole, /// Create merchant account for user in a org UserMerchantAccountCreate, + /// Create Org in a given tenancy + UserOrgMerchantCreate, /// Generate Sample Data GenerateSampleData, /// Delete Sample Data