Issue:
Bug: feat(users): add tenant_id in user roles



- Use global interface for user roles
- Add column tenant_id in user_roles
- Handle insertion of tenant_id
- Hadle token validation, a token issued in one tenant should not work in other tenancy

Pull request description:
## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
This PR includes
- Use of global interface for user roles table
- Add tenant_id column in user_roles table
- Handle user_role insertions with correct value for tenant_id
- Auth Changes: Token validation, token issued in one tenancy should not be valid in other

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
Closes [#6457](https://github.com/juspay/hyperswitch/issues/6457)


## How did you test it?
With the multi -tenancy feature flag enabled, when trying to signup by sending different headers for different tenancies>
```
curl --location 'http://localhost:8080/user/signup?token_only=true' \
--header 'x-tenant-id: test' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "global_test@juspay.in",
    "password": "Hyper@123"
}'
```
```
curl --location 'http://localhost:8080/user/signup?token_only=true' \
--header 'x-tenant-id: public' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "global_public@juspay.in",
    "password": "Hyper@123"
}'
```
The user role is getting inserted properly, with correct values of tenant_id.
<img width="1473" alt="Screenshot 2024-10-29 at 2 42 08 AM" src="https://github.com/user-attachments/assets/f312adce-b405-4684-8891-16caf324cbb4">

When Trying to use one JWT token with some other header that doesn't match with origin tenancy where it is issued it is authentication part is throwing error. Which is correct.



## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible


Testing:
With the multi -tenancy feature flag enabled, when trying to signup by sending different headers for different tenancies>
```
curl --location 'http://localhost:8080/user/signup?token_only=true' \
--header 'x-tenant-id: test' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "global_test@juspay.in",
    "password": "Hyper@123"
}'
```
```
curl --location 'http://localhost:8080/user/signup?token_only=true' \
--header 'x-tenant-id: public' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "global_public@juspay.in",
    "password": "Hyper@123"
}'
```
The user role is getting inserted properly, with correct values of tenant_id.
<img width="1473" alt="Screenshot 2024-10-29 at 2 42 08 AM" src="https://github.com/user-attachments/assets/f312adce-b405-4684-8891-16caf324cbb4">

When Trying to use one JWT token with some other header that doesn't match with origin tenancy where it is issued it is authentication part is throwing error. Which is correct.

Patch:
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index e2ab676b2d3..782d7f50eac 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<Varchar>,
         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 5651bf95dd9..73d2de8c573 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<Varchar>,
         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 71caa41deac..ceddbfd61e4 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<String>,
     pub entity_type: Option<EntityType>,
     pub version: enums::UserRoleVersion,
+    pub tenant_id: String,
 }
 
 impl UserRole {
@@ -87,6 +88,7 @@ pub struct UserRoleNew {
     pub entity_id: Option<String>,
     pub entity_type: Option<EntityType>,
     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 ed25725358a..2c9aec39b04 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<UserRole> = 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<UserRole> = 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 df4ce1d8148..5c7b77246e2 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<api_models::errors::types::ApiErrorResponse> for UserErrors {
@@ -239,6 +241,9 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
             Self::MaxRecoveryCodeAttemptsReached => {
                 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 ea3459709c6..521c978e3c2 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 35b26926ed2..aad2537c3d9 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 27c1e2ae494..95a8b7d51d1 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<Vec<user_role_api::ListInvitationForUserResponse>> {
     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 81319ae4c0b..de0c6282fc2 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 d32c6b254eb..2d9a949879a 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 cbd694f3acd..b80ade89178 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<TokenPurpose>,
+    pub tenant_id: Option<String>,
 }
 
 #[cfg(feature = "olap")]
@@ -189,6 +190,7 @@ pub struct SinglePurposeToken {
     pub origin: domain::Origin,
     pub path: Vec<TokenPurpose>,
     pub exp: u64,
+    pub tenant_id: Option<String>,
 }
 
 #[cfg(feature = "olap")]
@@ -199,6 +201,7 @@ impl SinglePurposeToken {
         origin: domain::Origin,
         settings: &Settings,
         path: Vec<TokenPurpose>,
+        tenant_id: Option<String>,
     ) -> UserResult<String> {
         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<id_type::ProfileId>,
+    pub tenant_id: Option<String>,
 }
 
 #[cfg(feature = "olap")]
@@ -233,6 +238,7 @@ impl AuthToken {
         settings: &Settings,
         org_id: id_type::OrganizationId,
         profile_id: Option<id_type::ProfileId>,
+        tenant_id: Option<String>,
     ) -> UserResult<String> {
         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<id_type::ProfileId>,
+    pub tenant_id: Option<String>,
 }
 
 pub struct UserIdFromAuth {
@@ -268,6 +276,7 @@ pub struct SinglePurposeOrLoginToken {
     pub role_id: Option<String>,
     pub purpose: Option<TokenPurpose>,
     pub exp: u64,
+    pub tenant_id: Option<String>,
 }
 
 pub trait AuthInfo {
@@ -751,6 +760,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());
@@ -761,6 +774,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,
@@ -785,6 +799,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());
@@ -795,6 +813,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,
@@ -824,6 +843,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
@@ -1396,6 +1419,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)?;
 
@@ -1424,6 +1452,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)?;
@@ -1435,6 +1467,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,
@@ -1459,6 +1492,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)?;
@@ -1518,6 +1555,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)?;
@@ -1559,6 +1600,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)?;
@@ -1595,6 +1640,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)?;
 
@@ -1740,6 +1789,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)?;
@@ -1773,6 +1826,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));
@@ -1912,6 +1969,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));
@@ -1987,6 +2048,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)?;
@@ -2153,6 +2218,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)?;
@@ -2210,6 +2279,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::<id_type::ProfileId>(headers::X_PROFILE_ID)?;
@@ -2282,6 +2355,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)?;
@@ -2341,6 +2418,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(),
@@ -2348,6 +2430,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,
@@ -2372,6 +2455,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))
     }
@@ -2389,6 +2476,11 @@ where
         state: &A,
     ) -> RouterResult<(AuthenticationData, AuthenticationType)> {
         let payload = parse_jwt_payload::<A, AuthToken>(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()
@@ -2709,6 +2801,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 fe6ffac6ffc..35a0b159ab2 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<String>, 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<A: SessionStateInfo>(state: &A) -> RouterResult<Arc<RedisConnectionPool>> {
     state
         .store()
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index c48fe3320f3..5a881728b07 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<NoLevel> {
 }
 
 pub struct EntityInfo {
+    tenant_id: String,
     org_id: id_type::OrganizationId,
     merchant_id: Option<id_type::MerchantId>,
     profile_id: Option<id_type::ProfileId>,
@@ -1175,6 +1182,7 @@ impl From<OrganizationLevel> 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<MerchantLevel> 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<ProfileLevel> 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 3fc05285ea8..86f10f1ceb7 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 8975519fd97..c4647907ff9 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<String>,
 ) -> UserResult<Secret<String>> {
     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 6f0d94d2927..aaf313a196e 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<UserRole, Report<StorageError>>,
 ) {
     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<EntityType>,
 ) -> UserResult<HashSet<UserRole>> {
     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 00000000000..fb9a32c0767
--- /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 00000000000..68e75f7f5d8
--- /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';