From b79183f8b3dbbb2215163f1227598de6950467a7 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:21:03 +0530 Subject: [PATCH] feat(users): add global support in user roles (#6458) --- crates/diesel_models/src/schema.rs | 2 + crates/diesel_models/src/schema_v2.rs | 2 + crates/diesel_models/src/user_role.rs | 2 + crates/router/src/analytics.rs | 4 +- crates/router/src/core/errors/user.rs | 6 ++ crates/router/src/core/recon.rs | 1 + crates/router/src/core/user.rs | 58 ++++++++--- crates/router/src/core/user_role.rs | 20 ++-- crates/router/src/db.rs | 2 +- crates/router/src/db/user_role.rs | 1 + crates/router/src/services/authentication.rs | 96 +++++++++++++++++++ crates/router/src/services/authorization.rs | 12 +++ crates/router/src/types/domain/user.rs | 15 ++- .../src/types/domain/user/decision_manager.rs | 6 +- crates/router/src/utils/user.rs | 2 + crates/router/src/utils/user_role.rs | 12 +-- .../down.sql | 2 + .../up.sql | 2 + 18 files changed, 211 insertions(+), 34 deletions(-) create mode 100644 migrations/2024-10-26-105654_add_column_tenant_id_to_user_roles/down.sql create mode 100644 migrations/2024-10-26-105654_add_column_tenant_id_to_user_roles/up.sql diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e2ab676b2d30..782d7f50eac2 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1343,6 +1343,8 @@ diesel::table! { #[max_length = 64] entity_type -> Nullable, version -> UserRoleVersion, + #[max_length = 64] + tenant_id -> Varchar, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 3bd31f5cbf3f..1c287567fba4 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1289,6 +1289,8 @@ diesel::table! { #[max_length = 64] entity_type -> Nullable, version -> UserRoleVersion, + #[max_length = 64] + tenant_id -> Varchar, } } diff --git a/crates/diesel_models/src/user_role.rs b/crates/diesel_models/src/user_role.rs index 71caa41deac4..ceddbfd61e4a 100644 --- a/crates/diesel_models/src/user_role.rs +++ b/crates/diesel_models/src/user_role.rs @@ -24,6 +24,7 @@ pub struct UserRole { pub entity_id: Option, pub entity_type: Option, pub version: enums::UserRoleVersion, + pub tenant_id: String, } impl UserRole { @@ -87,6 +88,7 @@ pub struct UserRoleNew { pub entity_id: Option, pub entity_type: Option, pub version: enums::UserRoleVersion, + pub tenant_id: String, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index ed25725358a0..2c9aec39b041 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -1853,7 +1853,7 @@ pub mod routes { return Err(OpenSearchError::AccessForbiddenError)?; } let user_roles: HashSet = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &auth.user_id, org_id: Some(&auth.org_id), @@ -1976,7 +1976,7 @@ pub mod routes { return Err(OpenSearchError::AccessForbiddenError)?; } let user_roles: HashSet = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &auth.user_id, org_id: Some(&auth.org_id), diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index df4ce1d81486..5c7b77246e27 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -94,6 +94,8 @@ pub enum UserErrors { MaxTotpAttemptsReached, #[error("Maximum attempts reached for Recovery Code")] MaxRecoveryCodeAttemptsReached, + #[error("Forbidden tenant id")] + ForbiddenTenantId, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -239,6 +241,9 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 49, self.get_error_message(), None)) } + Self::ForbiddenTenantId => { + AER::BadRequest(ApiError::new(sub_code, 50, self.get_error_message(), None)) + } } } } @@ -289,6 +294,7 @@ impl UserErrors { Self::AuthConfigParsingError => "Auth config parsing error", Self::SSOFailed => "Invalid SSO request", Self::JwtProfileIdMissing => "profile_id missing in JWT", + Self::ForbiddenTenantId => "Forbidden tenant id", } } } diff --git a/crates/router/src/core/recon.rs b/crates/router/src/core/recon.rs index ea3459709c62..521c978e3c2b 100644 --- a/crates/router/src/core/recon.rs +++ b/crates/router/src/core/recon.rs @@ -94,6 +94,7 @@ pub async fn generate_recon_token( &state.conf, user.org_id.clone(), user.profile_id.clone(), + user.tenant_id, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 35b26926ed2b..aad2537c3d9c 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -598,7 +598,7 @@ async fn handle_existing_user_invitation( let now = common_utils::date_time::now(); if state - .store + .global_store .find_user_role_by_user_id_and_lineage( invitee_user_from_db.get_user_id(), &user_from_token.org_id, @@ -614,7 +614,7 @@ async fn handle_existing_user_invitation( } if state - .store + .global_store .find_user_role_by_user_id_and_lineage( invitee_user_from_db.get_user_id(), &user_from_token.org_id, @@ -650,6 +650,10 @@ async fn handle_existing_user_invitation( EntityType::Organization => { user_role .add_entity(domain::OrganizationLevel { + tenant_id: user_from_token + .tenant_id + .clone() + .unwrap_or(state.tenant.tenant_id.clone()), org_id: user_from_token.org_id.clone(), }) .insert_in_v2(state) @@ -658,6 +662,10 @@ async fn handle_existing_user_invitation( EntityType::Merchant => { user_role .add_entity(domain::MerchantLevel { + tenant_id: user_from_token + .tenant_id + .clone() + .unwrap_or(state.tenant.tenant_id.clone()), org_id: user_from_token.org_id.clone(), merchant_id: user_from_token.merchant_id.clone(), }) @@ -671,6 +679,10 @@ async fn handle_existing_user_invitation( .ok_or(UserErrors::InternalServerError)?; user_role .add_entity(domain::ProfileLevel { + tenant_id: user_from_token + .tenant_id + .clone() + .unwrap_or(state.tenant.tenant_id.clone()), org_id: user_from_token.org_id.clone(), merchant_id: user_from_token.merchant_id.clone(), profile_id: profile_id.clone(), @@ -777,6 +789,10 @@ async fn handle_new_user_invitation( EntityType::Organization => { user_role .add_entity(domain::OrganizationLevel { + tenant_id: user_from_token + .tenant_id + .clone() + .unwrap_or(state.tenant.tenant_id.clone()), org_id: user_from_token.org_id.clone(), }) .insert_in_v2(state) @@ -785,6 +801,10 @@ async fn handle_new_user_invitation( EntityType::Merchant => { user_role .add_entity(domain::MerchantLevel { + tenant_id: user_from_token + .tenant_id + .clone() + .unwrap_or(state.tenant.tenant_id.clone()), org_id: user_from_token.org_id.clone(), merchant_id: user_from_token.merchant_id.clone(), }) @@ -798,6 +818,10 @@ async fn handle_new_user_invitation( .ok_or(UserErrors::InternalServerError)?; user_role .add_entity(domain::ProfileLevel { + tenant_id: user_from_token + .tenant_id + .clone() + .unwrap_or(state.tenant.tenant_id.clone()), org_id: user_from_token.org_id.clone(), merchant_id: user_from_token.merchant_id.clone(), profile_id: profile_id.clone(), @@ -864,6 +888,7 @@ async fn handle_new_user_invitation( org_id: user_from_token.org_id.clone(), role_id: request.role_id.clone(), profile_id: None, + tenant_id: user_from_token.tenant_id.clone(), }; let set_metadata_request = SetMetaDataRequest::IsChangePasswordRequired; @@ -909,7 +934,7 @@ pub async fn resend_invite( .into(); let user_role = match state - .store + .global_store .find_user_role_by_user_id_and_lineage( user.get_user_id(), &user_from_token.org_id, @@ -932,7 +957,7 @@ pub async fn resend_invite( let user_role = match user_role { Some(user_role) => user_role, None => state - .store + .global_store .find_user_role_by_user_id_and_lineage( user.get_user_id(), &user_from_token.org_id, @@ -1102,6 +1127,13 @@ pub async fn create_internal_user( } })?; + let default_tenant_id = common_utils::consts::DEFAULT_TENANT.to_string(); + + if state.tenant.tenant_id != default_tenant_id { + return Err(UserErrors::ForbiddenTenantId) + .attach_printable("Operation allowed only for the default tenant."); + } + let internal_merchant_id = common_utils::id_type::MerchantId::get_internal_user_merchant_id( consts::user_role::INTERNAL_USER_MERCHANT_ID, ); @@ -1142,6 +1174,7 @@ pub async fn create_internal_user( UserStatus::Active, ) .add_entity(domain::MerchantLevel { + tenant_id: default_tenant_id, org_id: internal_merchant.organization_id, merchant_id: internal_merchant_id, }) @@ -1195,7 +1228,7 @@ pub async fn list_user_roles_details( } let user_roles_set = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: required_user.get_user_id(), org_id: Some(&user_from_token.org_id), @@ -2372,7 +2405,7 @@ pub async fn list_orgs_for_user( } let orgs = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user_from_token.user_id.as_str(), org_id: None, @@ -2437,7 +2470,7 @@ pub async fn list_merchants_for_user_in_org( .change_context(UserErrors::InternalServerError)?, EntityType::Merchant | EntityType::Profile => { let merchant_ids = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user_from_token.user_id.as_str(), org_id: Some(&user_from_token.org_id), @@ -2516,7 +2549,7 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account( .change_context(UserErrors::InternalServerError)?, EntityType::Profile => { let profile_ids = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user_from_token.user_id.as_str(), org_id: Some(&user_from_token.org_id), @@ -2592,7 +2625,7 @@ pub async fn switch_org_for_user( } let user_role = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &user_from_token.user_id, org_id: Some(&request.org_id), @@ -2621,6 +2654,7 @@ pub async fn switch_org_for_user( request.org_id.clone(), user_role.role_id.clone(), profile_id.clone(), + user_from_token.tenant_id, ) .await?; @@ -2764,7 +2798,7 @@ pub async fn switch_merchant_for_user_in_org( EntityType::Merchant | EntityType::Profile => { let user_role = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &user_from_token.user_id, org_id: Some(&user_from_token.org_id), @@ -2805,6 +2839,7 @@ pub async fn switch_merchant_for_user_in_org( org_id.clone(), role_id.clone(), profile_id, + user_from_token.tenant_id, ) .await?; @@ -2879,7 +2914,7 @@ pub async fn switch_profile_for_user_in_org_and_merchant( EntityType::Profile => { let user_role = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload{ user_id:&user_from_token.user_id, org_id: Some(&user_from_token.org_id), @@ -2910,6 +2945,7 @@ pub async fn switch_profile_for_user_in_org_and_merchant( user_from_token.org_id.clone(), role_id.clone(), profile_id, + user_from_token.tenant_id, ) .await?; diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 27c1e2ae4945..95a8b7d51d1d 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -156,7 +156,7 @@ pub async fn update_user_role( let mut is_updated = false; let v2_user_role_to_be_updated = match state - .store + .global_store .find_user_role_by_user_id_and_lineage( user_to_be_updated.get_user_id(), &user_from_token.org_id, @@ -210,7 +210,7 @@ pub async fn update_user_role( } state - .store + .global_store .update_user_role_by_user_id_and_lineage( user_to_be_updated.get_user_id(), &user_from_token.org_id, @@ -229,7 +229,7 @@ pub async fn update_user_role( } let v1_user_role_to_be_updated = match state - .store + .global_store .find_user_role_by_user_id_and_lineage( user_to_be_updated.get_user_id(), &user_from_token.org_id, @@ -283,7 +283,7 @@ pub async fn update_user_role( } state - .store + .global_store .update_user_role_by_user_id_and_lineage( user_to_be_updated.get_user_id(), &user_from_token.org_id, @@ -470,7 +470,7 @@ pub async fn delete_user_role( // Find in V2 let user_role_v2 = match state - .store + .global_store .find_user_role_by_user_id_and_lineage( user_from_db.get_user_id(), &user_from_token.org_id, @@ -517,7 +517,7 @@ pub async fn delete_user_role( user_role_deleted_flag = true; state - .store + .global_store .delete_user_role_by_user_id_and_lineage( user_from_db.get_user_id(), &user_from_token.org_id, @@ -532,7 +532,7 @@ pub async fn delete_user_role( // Find in V1 let user_role_v1 = match state - .store + .global_store .find_user_role_by_user_id_and_lineage( user_from_db.get_user_id(), &user_from_token.org_id, @@ -579,7 +579,7 @@ pub async fn delete_user_role( user_role_deleted_flag = true; state - .store + .global_store .delete_user_role_by_user_id_and_lineage( user_from_db.get_user_id(), &user_from_token.org_id, @@ -599,7 +599,7 @@ pub async fn delete_user_role( // Check if user has any more role associations let remaining_roles = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user_from_db.get_user_id(), org_id: None, @@ -780,7 +780,7 @@ pub async fn list_invitations_for_user( user_from_token: auth::UserIdFromAuth, ) -> UserResponse> { let user_roles = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &user_from_token.user_id, org_id: None, diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 81319ae4c0be..de0c6282fc21 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -123,7 +123,6 @@ pub trait StorageInterface: + routing_algorithm::RoutingAlgorithmInterface + gsm::GsmInterface + unified_translations::UnifiedTranslationsInterface - + user_role::UserRoleInterface + authorization::AuthorizationInterface + user::sample_data::BatchSampleDataInterface + health_check::HealthCheckDbInterface @@ -144,6 +143,7 @@ pub trait GlobalStorageInterface: + Sync + dyn_clone::DynClone + user::UserInterface + + user_role::UserRoleInterface + user_key_store::UserKeyStoreInterface + 'static { diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index d32c6b254eb3..2d9a949879aa 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -234,6 +234,7 @@ impl UserRoleInterface for MockDb { entity_id: None, entity_type: None, version: enums::UserRoleVersion::V1, + tenant_id: user_role.tenant_id, }; db_user_roles.push(user_role.clone()); Ok(user_role) diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 891d2601c73d..a22d6bf1df9f 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -179,6 +179,7 @@ pub struct UserFromSinglePurposeToken { pub user_id: String, pub origin: domain::Origin, pub path: Vec, + pub tenant_id: Option, } #[cfg(feature = "olap")] @@ -189,6 +190,7 @@ pub struct SinglePurposeToken { pub origin: domain::Origin, pub path: Vec, pub exp: u64, + pub tenant_id: Option, } #[cfg(feature = "olap")] @@ -199,6 +201,7 @@ impl SinglePurposeToken { origin: domain::Origin, settings: &Settings, path: Vec, + tenant_id: Option, ) -> UserResult { let exp_duration = std::time::Duration::from_secs(consts::SINGLE_PURPOSE_TOKEN_TIME_IN_SECS); @@ -209,6 +212,7 @@ impl SinglePurposeToken { origin, exp, path, + tenant_id, }; jwt::generate_jwt(&token_payload, settings).await } @@ -222,6 +226,7 @@ pub struct AuthToken { pub exp: u64, pub org_id: id_type::OrganizationId, pub profile_id: Option, + pub tenant_id: Option, } #[cfg(feature = "olap")] @@ -233,6 +238,7 @@ impl AuthToken { settings: &Settings, org_id: id_type::OrganizationId, profile_id: Option, + tenant_id: Option, ) -> UserResult { let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); let exp = jwt::generate_exp(exp_duration)?.as_secs(); @@ -243,6 +249,7 @@ impl AuthToken { exp, org_id, profile_id, + tenant_id, }; jwt::generate_jwt(&token_payload, settings).await } @@ -255,6 +262,7 @@ pub struct UserFromToken { pub role_id: String, pub org_id: id_type::OrganizationId, pub profile_id: Option, + pub tenant_id: Option, } pub struct UserIdFromAuth { @@ -268,6 +276,7 @@ pub struct SinglePurposeOrLoginToken { pub role_id: Option, pub purpose: Option, pub exp: u64, + pub tenant_id: Option, } pub trait AuthInfo { @@ -748,6 +757,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; if self.0 != payload.purpose { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); @@ -758,6 +771,7 @@ where user_id: payload.user_id.clone(), origin: payload.origin.clone(), path: payload.path, + tenant_id: payload.tenant_id, }, AuthenticationType::SinglePurposeJwt { user_id: payload.user_id, @@ -782,6 +796,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; if self.0 != payload.purpose { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); @@ -792,6 +810,7 @@ where user_id: payload.user_id.clone(), origin: payload.origin.clone(), path: payload.path, + tenant_id: payload.tenant_id, }), AuthenticationType::SinglePurposeJwt { user_id: payload.user_id, @@ -821,6 +840,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let is_purpose_equal = payload .purpose @@ -1448,6 +1471,11 @@ where return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; + let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.permission, &role_info)?; @@ -1476,6 +1504,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.permission, &role_info)?; @@ -1487,6 +1519,7 @@ where org_id: payload.org_id, role_id: payload.role_id, profile_id: payload.profile_id, + tenant_id: payload.tenant_id, }, AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, @@ -1511,6 +1544,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.permission, &role_info)?; @@ -1570,6 +1607,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.required_permission, &role_info)?; @@ -1611,6 +1652,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.required_permission, &role_info)?; @@ -1647,6 +1692,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.required_permission, &role_info)?; @@ -1792,6 +1841,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.required_permission, &role_info)?; @@ -1825,6 +1878,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; if payload.merchant_id != self.merchant_id { return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); @@ -1964,6 +2021,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; if payload.merchant_id != self.merchant_id { return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); @@ -2039,6 +2100,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.required_permission, &role_info)?; @@ -2205,6 +2270,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.permission, &role_info)?; @@ -2262,6 +2331,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let profile_id = HeaderMapStruct::new(request_headers) .get_id_type_from_header::(headers::X_PROFILE_ID)?; @@ -2334,6 +2407,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.permission, &role_info)?; @@ -2393,6 +2470,11 @@ where return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; + Ok(( UserFromToken { user_id: payload.user_id.clone(), @@ -2400,6 +2482,7 @@ where org_id: payload.org_id, role_id: payload.role_id, profile_id: payload.profile_id, + tenant_id: payload.tenant_id, }, AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, @@ -2424,6 +2507,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; Ok(((), AuthenticationType::NoAuth)) } @@ -2441,6 +2528,11 @@ where state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; + let key_manager_state = &(&state.session_state()).into(); let key_store = state .store() @@ -2809,6 +2901,10 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } + authorization::check_tenant( + payload.tenant_id.clone(), + &state.session_state().tenant.tenant_id, + )?; let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.permission, &role_info)?; diff --git a/crates/router/src/services/authorization.rs b/crates/router/src/services/authorization.rs index fe6ffac6ffc0..35a0b159ab2e 100644 --- a/crates/router/src/services/authorization.rs +++ b/crates/router/src/services/authorization.rs @@ -112,6 +112,18 @@ pub fn check_permission( ) } +pub fn check_tenant(token_tenant_id: Option, header_tenant_id: &str) -> RouterResult<()> { + if let Some(tenant_id) = token_tenant_id { + if tenant_id != header_tenant_id { + return Err(ApiErrorResponse::InvalidJwtToken).attach_printable(format!( + "Token tenant ID: '{}' does not match Header tenant ID: '{}'", + tenant_id, header_tenant_id + )); + } + } + Ok(()) +} + fn get_redis_connection(state: &A) -> RouterResult> { state .store() diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index c48fe3320f3d..5a881728b07e 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -689,7 +689,10 @@ impl NewUser { let org_user_role = self .get_no_level_user_role(role_id, user_status) - .add_entity(OrganizationLevel { org_id }); + .add_entity(OrganizationLevel { + tenant_id: state.tenant.tenant_id.clone(), + org_id, + }); org_user_role.insert_in_v2(&state).await } @@ -1116,17 +1119,20 @@ pub struct NoLevel; #[derive(Clone)] pub struct OrganizationLevel { + pub tenant_id: String, pub org_id: id_type::OrganizationId, } #[derive(Clone)] pub struct MerchantLevel { + pub tenant_id: String, pub org_id: id_type::OrganizationId, pub merchant_id: id_type::MerchantId, } #[derive(Clone)] pub struct ProfileLevel { + pub tenant_id: String, pub org_id: id_type::OrganizationId, pub merchant_id: id_type::MerchantId, pub profile_id: id_type::ProfileId, @@ -1163,6 +1169,7 @@ impl NewUserRole { } pub struct EntityInfo { + tenant_id: String, org_id: id_type::OrganizationId, merchant_id: Option, profile_id: Option, @@ -1175,6 +1182,7 @@ impl From for EntityInfo { 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, merchant_id: None, profile_id: None, @@ -1187,6 +1195,7 @@ impl From for EntityInfo { Self { 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, merchant_id: Some(value.merchant_id), @@ -1199,6 +1208,7 @@ impl From for EntityInfo { Self { entity_id: value.profile_id.get_string_repr().to_owned(), entity_type: EntityType::Profile, + tenant_id: value.tenant_id, org_id: value.org_id, merchant_id: Some(value.merchant_id), profile_id: Some(value.profile_id), @@ -1225,6 +1235,7 @@ where entity_id: Some(entity.entity_id), entity_type: Some(entity.entity_type), version: UserRoleVersion::V2, + tenant_id: entity.tenant_id, } } @@ -1234,7 +1245,7 @@ where let new_v2_role = self.convert_to_new_v2_role(entity.into()); state - .store + .global_store .insert_user_role(new_v2_role) .await .change_context(UserErrors::InternalServerError) diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs index 3fc05285ea89..86f10f1ceb78 100644 --- a/crates/router/src/types/domain/user/decision_manager.rs +++ b/crates/router/src/types/domain/user/decision_manager.rs @@ -65,7 +65,7 @@ impl SPTFlow { .is_password_rotate_required(state) .map(|rotate_required| rotate_required && !path.contains(&TokenPurpose::SSO)), Self::MerchantSelect => Ok(state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user.get_user_id(), org_id: None, @@ -93,6 +93,7 @@ impl SPTFlow { next_flow.origin.clone(), &state.conf, next_flow.path.to_vec(), + Some(state.tenant.tenant_id.clone()), ) .await .map(|token| token.into()) @@ -132,6 +133,7 @@ impl JWTFlow { .ok_or(report!(UserErrors::InternalServerError)) .attach_printable("org_id not found")?, Some(profile_id), + Some(user_role.tenant_id.clone()), ) .await .map(|token| token.into()) @@ -299,7 +301,7 @@ impl NextFlow { self.user.get_verification_days_left(state)?; } let user_role = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: self.user.get_user_id(), org_id: None, diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 8975519fd97d..c4647907ff9b 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -92,6 +92,7 @@ pub async fn generate_jwt_auth_token_with_attributes( org_id: id_type::OrganizationId, role_id: String, profile_id: id_type::ProfileId, + tenant_id: Option, ) -> UserResult> { let token = AuthToken::new_token( user_id, @@ -100,6 +101,7 @@ pub async fn generate_jwt_auth_token_with_attributes( &state.conf, org_id, Some(profile_id), + tenant_id, ) .await?; Ok(Secret::new(token)) diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index 6f0d94d2927f..aaf313a196e5 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -142,7 +142,7 @@ pub async fn update_v1_and_v2_user_roles_in_db( Result>, ) { let updated_v1_role = state - .store + .global_store .update_user_role_by_user_id_and_lineage( user_id, org_id, @@ -158,7 +158,7 @@ pub async fn update_v1_and_v2_user_roles_in_db( }); let updated_v2_role = state - .store + .global_store .update_user_role_by_user_id_and_lineage( user_id, org_id, @@ -228,7 +228,7 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( }; let user_roles = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id, org_id: Some(&org_id), @@ -272,7 +272,7 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( }; let user_roles = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id, org_id: None, @@ -317,7 +317,7 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( }; let user_roles = state - .store + .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id, org_id: None, @@ -407,7 +407,7 @@ pub async fn fetch_user_roles_by_payload( request_entity_type: Option, ) -> UserResult> { Ok(state - .store + .global_store .list_user_roles_by_org_id(payload) .await .change_context(UserErrors::InternalServerError)? diff --git a/migrations/2024-10-26-105654_add_column_tenant_id_to_user_roles/down.sql b/migrations/2024-10-26-105654_add_column_tenant_id_to_user_roles/down.sql new file mode 100644 index 000000000000..fb9a32c0767b --- /dev/null +++ b/migrations/2024-10-26-105654_add_column_tenant_id_to_user_roles/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE user_roles DROP COLUMN IF EXISTS tenant_id; \ No newline at end of file diff --git a/migrations/2024-10-26-105654_add_column_tenant_id_to_user_roles/up.sql b/migrations/2024-10-26-105654_add_column_tenant_id_to_user_roles/up.sql new file mode 100644 index 000000000000..68e75f7f5d8b --- /dev/null +++ b/migrations/2024-10-26-105654_add_column_tenant_id_to_user_roles/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE user_roles ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(64) NOT NULL DEFAULT 'public';