diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs index 7c877cd74777..6b8736d3e764 100644 --- a/crates/api_models/src/user_role/role.rs +++ b/crates/api_models/src/user_role/role.rs @@ -7,6 +7,7 @@ pub struct CreateRoleRequest { pub role_name: String, pub groups: Vec, pub role_scope: RoleScope, + pub entity_type: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -21,6 +22,7 @@ pub struct RoleInfoWithGroupsResponse { pub groups: Vec, pub role_name: String, pub role_scope: RoleScope, + pub entity_type: EntityType, } #[derive(Debug, serde::Serialize)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 193998c3fd1c..1979502a849d 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2799,8 +2799,19 @@ pub enum TransactionType { #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoleScope { - Merchant, Organization, + Merchant, + Profile, +} + +impl From for EntityType { + fn from(role_scope: RoleScope) -> Self { + match role_scope { + RoleScope::Organization => Self::Organization, + RoleScope::Merchant => Self::Merchant, + RoleScope::Profile => Self::Profile, + } + } } /// Indicates the transaction status @@ -3255,6 +3266,7 @@ pub enum ApiVersion { serde::Serialize, strum::Display, strum::EnumString, + strum::EnumIter, ToSchema, Hash, )] diff --git a/crates/diesel_models/src/query/role.rs b/crates/diesel_models/src/query/role.rs index 6f6a1404ee2c..278750503d36 100644 --- a/crates/diesel_models/src/query/role.rs +++ b/crates/diesel_models/src/query/role.rs @@ -1,10 +1,12 @@ use async_bb8_diesel::AsyncRunQueryDsl; +use common_enums::EntityType; use common_utils::id_type; use diesel::{ associations::HasTable, debug_query, pg::Pg, result::Error as DieselError, BoolExpressionMethods, ExpressionMethods, QueryDsl, }; use error_stack::{report, ResultExt}; +use strum::IntoEnumIterator; use crate::{ enums::RoleScope, errors, query::generics, role::*, schema::roles::dsl, PgPooledConn, @@ -18,28 +20,23 @@ impl RoleNew { } impl Role { - pub async fn find_by_role_id(conn: &PgPooledConn, role_id: &str) -> StorageResult { - generics::generic_find_one::<::Table, _, _>( - conn, - dsl::role_id.eq(role_id.to_owned()), - ) - .await + fn get_entity_list( + current_entity: EntityType, + is_lineage_data_required: bool, + ) -> Vec { + is_lineage_data_required + .then(|| { + EntityType::iter() + .filter(|variant| *variant <= current_entity) + .collect() + }) + .unwrap_or_else(|| vec![current_entity]) } - // TODO: Remove once find_by_role_id_in_lineage is stable - pub async fn find_by_role_id_in_merchant_scope( - conn: &PgPooledConn, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - ) -> StorageResult { + pub async fn find_by_role_id(conn: &PgPooledConn, role_id: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, - dsl::role_id.eq(role_id.to_owned()).and( - dsl::merchant_id.eq(merchant_id.to_owned()).or(dsl::org_id - .eq(org_id.to_owned()) - .and(dsl::scope.eq(RoleScope::Organization))), - ), + dsl::role_id.eq(role_id.to_owned()), ) .await } @@ -49,6 +46,7 @@ impl Role { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, ) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, @@ -56,9 +54,14 @@ impl Role { .eq(role_id.to_owned()) .and(dsl::org_id.eq(org_id.to_owned())) .and( - dsl::scope.eq(RoleScope::Organization).or(dsl::merchant_id - .eq(merchant_id.to_owned()) - .and(dsl::scope.eq(RoleScope::Merchant))), + dsl::scope + .eq(RoleScope::Organization) + .or(dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::scope.eq(RoleScope::Merchant))) + .or(dsl::profile_id + .eq(profile_id.to_owned()) + .and(dsl::scope.eq(RoleScope::Profile))), ), ) .await @@ -104,32 +107,12 @@ impl Role { .await } - pub async fn list_roles( - conn: &PgPooledConn, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - ) -> StorageResult> { - let predicate = dsl::org_id.eq(org_id.to_owned()).and( - dsl::scope.eq(RoleScope::Organization).or(dsl::merchant_id - .eq(merchant_id.to_owned()) - .and(dsl::scope.eq(RoleScope::Merchant))), - ); - - generics::generic_filter::<::Table, _, _, _>( - conn, - predicate, - None, - None, - Some(dsl::last_modified_at.asc()), - ) - .await - } - + //TODO: Remove once generic_list_roles_by_entity_type is stable pub async fn generic_roles_list_for_org( conn: &PgPooledConn, org_id: id_type::OrganizationId, merchant_id: Option, - entity_type: Option, + entity_type: Option, limit: Option, ) -> StorageResult> { let mut query = ::table() @@ -170,4 +153,63 @@ impl Role { }, } } + + pub async fn generic_list_roles_by_entity_type( + conn: &PgPooledConn, + payload: ListRolesByEntityPayload, + is_lineage_data_required: bool, + ) -> StorageResult> { + let mut query = ::table().into_boxed(); + + match payload { + ListRolesByEntityPayload::Organization(org_id) => { + let entity_in_vec = + Self::get_entity_list(EntityType::Organization, is_lineage_data_required); + query = query + .filter(dsl::org_id.eq(org_id)) + .filter(dsl::entity_type.eq_any(entity_in_vec)) + } + + ListRolesByEntityPayload::Merchant(org_id, merchant_id) => { + let entity_in_vec = + Self::get_entity_list(EntityType::Merchant, is_lineage_data_required); + query = query + .filter(dsl::org_id.eq(org_id)) + .filter( + dsl::scope + .eq(RoleScope::Organization) + .or(dsl::merchant_id.eq(merchant_id)), + ) + .filter(dsl::entity_type.eq_any(entity_in_vec)) + } + + ListRolesByEntityPayload::Profile(org_id, merchant_id, profile_id) => { + let entity_in_vec = + Self::get_entity_list(EntityType::Profile, is_lineage_data_required); + query = query + .filter(dsl::org_id.eq(org_id)) + .filter( + dsl::scope + .eq(RoleScope::Organization) + .or(dsl::scope + .eq(RoleScope::Merchant) + .and(dsl::merchant_id.eq(merchant_id.clone()))) + .or(dsl::profile_id.eq(profile_id)), + ) + .filter(dsl::entity_type.eq_any(entity_in_vec)) + } + }; + + router_env::logger::debug!(query = %debug_query::(&query).to_string()); + + match generics::db_metrics::track_database_call::( + query.get_results_async(conn), + generics::db_metrics::DatabaseOperation::Filter, + ) + .await + { + Ok(value) => Ok(value), + Err(err) => Err(report!(err)).change_context(errors::DatabaseError::Others), + } + } } diff --git a/crates/diesel_models/src/role.rs b/crates/diesel_models/src/role.rs index 8199bd3979ce..8adad4e63a05 100644 --- a/crates/diesel_models/src/role.rs +++ b/crates/diesel_models/src/role.rs @@ -19,6 +19,7 @@ pub struct Role { pub last_modified_at: PrimitiveDateTime, pub last_modified_by: String, pub entity_type: enums::EntityType, + pub profile_id: Option, } #[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -36,6 +37,7 @@ pub struct RoleNew { pub last_modified_at: PrimitiveDateTime, pub last_modified_by: String, pub entity_type: enums::EntityType, + pub profile_id: Option, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -73,3 +75,38 @@ impl From for RoleUpdateInternal { } } } + +#[derive(Clone, Debug)] +pub enum ListRolesByEntityPayload { + Profile( + id_type::OrganizationId, + id_type::MerchantId, + id_type::ProfileId, + ), + Merchant(id_type::OrganizationId, id_type::MerchantId), + Organization(id_type::OrganizationId), +} + +impl ListRolesByEntityPayload { + pub fn get_organization_id(&self) -> Option { + match self { + Self::Organization(org_id) + | Self::Merchant(org_id, _) + | Self::Profile(org_id, _, _) => Some(org_id.to_owned()), + } + } + pub fn get_merchant_id(&self) -> Option { + match self { + Self::Organization(_) => None, + Self::Merchant(_, merchant_id) | Self::Profile(_, merchant_id, _) => { + Some(merchant_id.to_owned()) + } + } + } + pub fn get_profile_id(&self) -> Option { + match self { + Self::Organization(_) | Self::Merchant(_, _) => None, + Self::Profile(_, _, profile_id) => Some(profile_id.to_owned()), + } + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 366c917d2d11..bf8360190dba 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1305,6 +1305,8 @@ diesel::table! { last_modified_by -> Varchar, #[max_length = 64] entity_type -> Varchar, + #[max_length = 64] + profile_id -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index d85dc601490c..157812c12ad5 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1252,6 +1252,8 @@ diesel::table! { last_modified_by -> Varchar, #[max_length = 64] entity_type -> Varchar, + #[max_length = 64] + profile_id -> Nullable, } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 19f257ef4f37..78720b2b0a34 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -602,6 +602,7 @@ async fn handle_invitation( &request.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, ) .await .to_not_found_response(UserErrors::InvalidRoleId)?; diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index d8fdff0e6233..da2efe4404c0 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -123,6 +123,7 @@ pub async fn update_user_role( &req.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, ) .await .to_not_found_response(UserErrors::InvalidRoleId)?; @@ -527,6 +528,7 @@ pub async fn delete_user_role( &role_to_be_deleted.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, ) .await .change_context(UserErrors::InternalServerError)?; @@ -597,6 +599,7 @@ pub async fn delete_user_role( &role_to_be_deleted.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, ) .await .change_context(UserErrors::InternalServerError)?; diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs index e897e1b336a2..05299f52d66b 100644 --- a/crates/router/src/core/user_role/role.rs +++ b/crates/router/src/core/user_role/role.rs @@ -1,9 +1,9 @@ -use std::collections::HashSet; +use std::{cmp, collections::HashSet}; use api_models::user_role::role as role_api; -use common_enums::{EntityType, ParentGroup, PermissionGroup, RoleScope}; +use common_enums::{EntityType, ParentGroup, PermissionGroup}; use common_utils::generate_id_with_default_len; -use diesel_models::role::{RoleNew, RoleUpdate}; +use diesel_models::role::{ListRolesByEntityPayload, RoleNew, RoleUpdate}; use error_stack::{report, ResultExt}; use crate::{ @@ -65,6 +65,43 @@ pub async fn create_role( _req_state: ReqState, ) -> UserResponse { let now = common_utils::date_time::now(); + + let user_entity_type = user_from_token + .get_role_info_from_db(&state) + .await + .attach_printable("Invalid role_id in JWT")? + .get_entity_type(); + + let role_entity_type = req.entity_type.unwrap_or(EntityType::Merchant); + + if matches!(role_entity_type, EntityType::Organization) { + return Err(report!(UserErrors::InvalidRoleOperation)) + .attach_printable("User trying to create org level custom role"); + } + + // TODO: Remove in PR custom-role-write-pr + if matches!(role_entity_type, EntityType::Profile) { + return Err(report!(UserErrors::InvalidRoleOperation)) + .attach_printable("User trying to create profile level custom role"); + } + + let requestor_entity_from_role_scope = EntityType::from(req.role_scope); + + if requestor_entity_from_role_scope < role_entity_type { + return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!( + "User is trying to create role of type {} and scope {}", + requestor_entity_from_role_scope, role_entity_type + )); + } + let max_from_scope_and_entity = cmp::max(requestor_entity_from_role_scope, role_entity_type); + + if user_entity_type < max_from_scope_and_entity { + return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!( + "{} is trying to create of scope {} and of type {}", + user_entity_type, requestor_entity_from_role_scope, role_entity_type + )); + } + let role_name = RoleName::new(req.role_name)?; utils::user_role::validate_role_groups(&req.groups)?; @@ -73,32 +110,38 @@ pub async fn create_role( &role_name, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, + &role_entity_type, ) .await?; - let user_role_info = user_from_token.get_role_info_from_db(&state).await?; - - if matches!(req.role_scope, RoleScope::Organization) - && user_role_info.get_entity_type() != EntityType::Organization - { - return Err(report!(UserErrors::InvalidRoleOperation)) - .attach_printable("Non org admin user creating org level role"); - } + let (org_id, merchant_id, profile_id) = match role_entity_type { + EntityType::Organization | EntityType::Tenant => { + (user_from_token.org_id, user_from_token.merchant_id, None) + } + EntityType::Merchant => (user_from_token.org_id, user_from_token.merchant_id, None), + EntityType::Profile => ( + user_from_token.org_id, + user_from_token.merchant_id, + Some(user_from_token.profile_id), + ), + }; let role = state .global_store .insert_role(RoleNew { role_id: generate_id_with_default_len("role"), role_name: role_name.get_role_name(), - merchant_id: user_from_token.merchant_id, - org_id: user_from_token.org_id, + merchant_id, + org_id, groups: req.groups, scope: req.role_scope, - entity_type: EntityType::Merchant, + entity_type: role_entity_type, created_by: user_from_token.user_id.clone(), last_modified_by: user_from_token.user_id, created_at: now, last_modified_at: now, + profile_id, }) .await .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?; @@ -109,6 +152,7 @@ pub async fn create_role( role_id: role.role_id, role_name: role.role_name, role_scope: role.scope, + entity_type: role.entity_type, }, )) } @@ -133,6 +177,7 @@ pub async fn get_role_with_groups( role_id: role.role_id, role_name: role_info.get_role_name().to_string(), role_scope: role_info.get_scope(), + entity_type: role_info.get_entity_type(), }, )) } @@ -187,12 +232,47 @@ pub async fn update_role( ) -> UserResponse { let role_name = req.role_name.map(RoleName::new).transpose()?; + let role_info = roles::RoleInfo::from_role_id_in_lineage( + &state, + role_id, + &user_from_token.merchant_id, + &user_from_token.org_id, + &user_from_token.profile_id, + ) + .await + .to_not_found_response(UserErrors::InvalidRoleOperation)?; + + let user_role_info = user_from_token.get_role_info_from_db(&state).await?; + + let requested_entity_from_role_scope = EntityType::from(role_info.get_scope()); + let requested_role_entity_type = role_info.get_entity_type(); + + if requested_entity_from_role_scope < requested_role_entity_type { + return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!( + "User is trying to create role of type {} and scope {}", + requested_entity_from_role_scope, requested_role_entity_type + )); + } + let max_from_scope_and_entity = + cmp::max(requested_entity_from_role_scope, requested_role_entity_type); + + if user_role_info.get_entity_type() < max_from_scope_and_entity { + return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!( + "{} is trying to create of scope {} and of type {}", + user_role_info.get_entity_type(), + requested_entity_from_role_scope, + requested_role_entity_type + )); + } + if let Some(ref role_name) = role_name { utils::user_role::validate_role_name( &state, role_name, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, + &role_info.get_entity_type(), ) .await?; } @@ -201,24 +281,6 @@ pub async fn update_role( utils::user_role::validate_role_groups(groups)?; } - let role_info = roles::RoleInfo::from_role_id_in_lineage( - &state, - role_id, - &user_from_token.merchant_id, - &user_from_token.org_id, - ) - .await - .to_not_found_response(UserErrors::InvalidRoleOperation)?; - - let user_role_info = user_from_token.get_role_info_from_db(&state).await?; - - if matches!(role_info.get_scope(), RoleScope::Organization) - && user_role_info.get_entity_type() != EntityType::Organization - { - return Err(report!(UserErrors::InvalidRoleOperation)) - .attach_printable("Non org admin user changing org level role"); - } - let updated_role = state .global_store .update_role_by_role_id( @@ -241,6 +303,7 @@ pub async fn update_role( role_id: updated_role.role_id, role_name: updated_role.role_name, role_scope: updated_role.scope, + entity_type: updated_role.entity_type, }, )) } @@ -268,32 +331,44 @@ pub async fn list_roles_with_info( .collect::>(); let user_role_entity = user_role_info.get_entity_type(); + let is_lineage_data_required = request.entity_type.is_none(); let custom_roles = match utils::user_role::get_min_entity(user_role_entity, request.entity_type)? { EntityType::Tenant | EntityType::Organization => state .global_store - .list_roles_for_org_by_parameters( - &user_from_token.org_id, - None, - request.entity_type, - None, + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Organization(user_from_token.org_id), + is_lineage_data_required, ) .await .change_context(UserErrors::InternalServerError) .attach_printable("Failed to get roles")?, EntityType::Merchant => state .global_store - .list_roles_for_org_by_parameters( - &user_from_token.org_id, - Some(&user_from_token.merchant_id), - request.entity_type, - None, + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Merchant( + user_from_token.org_id, + user_from_token.merchant_id, + ), + is_lineage_data_required, + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to get roles")?, + + EntityType::Profile => state + .global_store + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Profile( + user_from_token.org_id, + user_from_token.merchant_id, + user_from_token.profile_id, + ), + is_lineage_data_required, ) .await .change_context(UserErrors::InternalServerError) .attach_printable("Failed to get roles")?, - // TODO: Populate this from Db function when support for profile id and profile level custom roles is added - EntityType::Profile => Vec::new(), }; role_info_vec.extend(custom_roles.into_iter().map(roles::RoleInfo::from)); @@ -342,14 +417,13 @@ pub async fn list_roles_at_entity_level( .map(|(_, role_info)| role_info.clone()) .collect::>(); + let is_lineage_data_required = false; let custom_roles = match req.entity_type { EntityType::Tenant | EntityType::Organization => state .global_store - .list_roles_for_org_by_parameters( - &user_from_token.org_id, - None, - Some(req.entity_type), - None, + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Organization(user_from_token.org_id), + is_lineage_data_required, ) .await .change_context(UserErrors::InternalServerError) @@ -357,17 +431,30 @@ pub async fn list_roles_at_entity_level( EntityType::Merchant => state .global_store - .list_roles_for_org_by_parameters( - &user_from_token.org_id, - Some(&user_from_token.merchant_id), - Some(req.entity_type), - None, + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Merchant( + user_from_token.org_id, + user_from_token.merchant_id, + ), + is_lineage_data_required, + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to get roles")?, + + EntityType::Profile => state + .global_store + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Profile( + user_from_token.org_id, + user_from_token.merchant_id, + user_from_token.profile_id, + ), + is_lineage_data_required, ) .await .change_context(UserErrors::InternalServerError) .attach_printable("Failed to get roles")?, - // TODO: Populate this from Db function when support for profile id and profile level custom roles is added - EntityType::Profile => Vec::new(), }; role_info_vec.extend(custom_roles.into_iter().map(roles::RoleInfo::from)); diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 525c5f12dc4e..3f8bc8b7fc8d 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -3600,26 +3600,15 @@ impl RoleInterface for KafkaStore { self.diesel_store.find_role_by_role_id(role_id).await } - //TODO:Remove once find_by_role_id_in_lineage is stable - async fn find_role_by_role_id_in_merchant_scope( - &self, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - ) -> CustomResult { - self.diesel_store - .find_role_by_role_id_in_merchant_scope(role_id, merchant_id, org_id) - .await - } - async fn find_role_by_role_id_in_lineage( &self, role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, ) -> CustomResult { self.diesel_store - .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id) + .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id, profile_id) .await } @@ -3650,14 +3639,7 @@ impl RoleInterface for KafkaStore { self.diesel_store.delete_role_by_role_id(role_id).await } - async fn list_all_roles( - &self, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - ) -> CustomResult, errors::StorageError> { - self.diesel_store.list_all_roles(merchant_id, org_id).await - } - + //TODO: Remove once generic_list_roles_by_entity_type is stable async fn list_roles_for_org_by_parameters( &self, org_id: &id_type::OrganizationId, @@ -3669,6 +3651,16 @@ impl RoleInterface for KafkaStore { .list_roles_for_org_by_parameters(org_id, merchant_id, entity_type, limit) .await } + + async fn generic_list_roles_by_entity_type( + &self, + payload: diesel_models::role::ListRolesByEntityPayload, + is_lineage_data_required: bool, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .generic_list_roles_by_entity_type(payload, is_lineage_data_required) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs index 877a4c540774..b185f6f1c53a 100644 --- a/crates/router/src/db/role.rs +++ b/crates/router/src/db/role.rs @@ -1,6 +1,8 @@ -use common_enums::enums; use common_utils::id_type; -use diesel_models::role as storage; +use diesel_models::{ + enums::{EntityType, RoleScope}, + role as storage, +}; use error_stack::report; use router_env::{instrument, tracing}; @@ -23,19 +25,12 @@ pub trait RoleInterface { role_id: &str, ) -> CustomResult; - //TODO:Remove once find_by_role_id_in_lineage is stable - async fn find_role_by_role_id_in_merchant_scope( - &self, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - ) -> CustomResult; - async fn find_role_by_role_id_in_lineage( &self, role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, ) -> CustomResult; async fn find_by_role_id_and_org_id( @@ -55,19 +50,20 @@ pub trait RoleInterface { role_id: &str, ) -> CustomResult; - async fn list_all_roles( - &self, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - ) -> CustomResult, errors::StorageError>; - + //TODO: Remove once generic_list_roles_by_entity_type is stable async fn list_roles_for_org_by_parameters( &self, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, - entity_type: Option, + entity_type: Option, limit: Option, ) -> CustomResult, errors::StorageError>; + + async fn generic_list_roles_by_entity_type( + &self, + payload: storage::ListRolesByEntityPayload, + is_lineage_data_required: bool, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -94,29 +90,16 @@ impl RoleInterface for Store { .map_err(|error| report!(errors::StorageError::from(error))) } - //TODO:Remove once find_by_role_id_in_lineage is stable - #[instrument(skip_all)] - async fn find_role_by_role_id_in_merchant_scope( - &self, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - storage::Role::find_by_role_id_in_merchant_scope(&conn, role_id, merchant_id, org_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - } - #[instrument(skip_all)] async fn find_role_by_role_id_in_lineage( &self, role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; - storage::Role::find_by_role_id_in_lineage(&conn, role_id, merchant_id, org_id) + storage::Role::find_by_role_id_in_lineage(&conn, role_id, merchant_id, org_id, profile_id) .await .map_err(|error| report!(errors::StorageError::from(error))) } @@ -156,24 +139,13 @@ impl RoleInterface for Store { .map_err(|error| report!(errors::StorageError::from(error))) } - #[instrument(skip_all)] - async fn list_all_roles( - &self, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_connection_read(self).await?; - storage::Role::list_roles(&conn, merchant_id, org_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - } - + //TODO: Remove once generic_list_roles_by_entity_type is stable #[instrument(skip_all)] async fn list_roles_for_org_by_parameters( &self, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, - entity_type: Option, + entity_type: Option, limit: Option, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; @@ -187,6 +159,18 @@ impl RoleInterface for Store { .await .map_err(|error| report!(errors::StorageError::from(error))) } + + #[instrument(skip_all)] + async fn generic_list_roles_by_entity_type( + &self, + payload: storage::ListRolesByEntityPayload, + is_lineage_data_required: bool, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::Role::generic_list_roles_by_entity_type(&conn, payload, is_lineage_data_required) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } } #[async_trait::async_trait] @@ -217,6 +201,7 @@ impl RoleInterface for MockDb { created_at: role.created_at, last_modified_at: role.last_modified_at, last_modified_by: role.last_modified_by, + profile_id: role.profile_id, }; roles.push(role.clone()); Ok(role) @@ -239,36 +224,12 @@ impl RoleInterface for MockDb { ) } - // TODO: Remove once find_by_role_id_in_lineage is stable - async fn find_role_by_role_id_in_merchant_scope( - &self, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - ) -> CustomResult { - let roles = self.roles.lock().await; - roles - .iter() - .find(|role| { - role.role_id == role_id - && (role.merchant_id == *merchant_id - || (role.org_id == *org_id && role.scope == enums::RoleScope::Organization)) - }) - .cloned() - .ok_or( - errors::StorageError::ValueNotFound(format!( - "No role available in merchant scope for role_id = {role_id}, \ - merchant_id = {merchant_id:?} and org_id = {org_id:?}" - )) - .into(), - ) - } - async fn find_role_by_role_id_in_lineage( &self, role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, ) -> CustomResult { let roles = self.roles.lock().await; roles @@ -276,9 +237,15 @@ impl RoleInterface for MockDb { .find(|role| { role.role_id == role_id && role.org_id == *org_id - && ((role.scope == enums::RoleScope::Organization) - || (role.merchant_id == *merchant_id - && role.scope == enums::RoleScope::Merchant)) + && ((role.scope == RoleScope::Organization) + || (role.merchant_id == *merchant_id && role.scope == RoleScope::Merchant) + || (role + .profile_id + .as_ref() + .is_some_and(|profile_id_from_role| { + profile_id_from_role == profile_id + && role.scope == RoleScope::Profile + }))) }) .cloned() .ok_or( @@ -357,55 +324,90 @@ impl RoleInterface for MockDb { Ok(roles.remove(role_index)) } - async fn list_all_roles( + //TODO: Remove once generic_list_roles_by_entity_type is stable + #[instrument(skip_all)] + async fn list_roles_for_org_by_parameters( &self, - merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + merchant_id: Option<&id_type::MerchantId>, + entity_type: Option, + limit: Option, ) -> CustomResult, errors::StorageError> { let roles = self.roles.lock().await; - + let limit_usize = limit.unwrap_or(u32::MAX).try_into().unwrap_or(usize::MAX); let roles_list: Vec<_> = roles .iter() .filter(|role| { - role.merchant_id == *merchant_id - || (role.org_id == *org_id - && role.scope == diesel_models::enums::RoleScope::Organization) + let matches_merchant = match merchant_id { + Some(merchant_id) => role.merchant_id == *merchant_id, + None => true, + }; + + matches_merchant && role.org_id == *org_id && Some(role.entity_type) == entity_type }) + .take(limit_usize) .cloned() .collect(); - if roles_list.is_empty() { - return Err(errors::StorageError::ValueNotFound(format!( - "No role found for merchant id = {:?} and org_id = {:?}", - merchant_id, org_id - )) - .into()); - } - Ok(roles_list) } #[instrument(skip_all)] - async fn list_roles_for_org_by_parameters( + async fn generic_list_roles_by_entity_type( &self, - org_id: &id_type::OrganizationId, - merchant_id: Option<&id_type::MerchantId>, - entity_type: Option, - limit: Option, + payload: storage::ListRolesByEntityPayload, + is_lineage_data_required: bool, ) -> CustomResult, errors::StorageError> { let roles = self.roles.lock().await; - let limit_usize = limit.unwrap_or(u32::MAX).try_into().unwrap_or(usize::MAX); let roles_list: Vec<_> = roles .iter() - .filter(|role| { - let matches_merchant = match merchant_id { - Some(merchant_id) => role.merchant_id == *merchant_id, - None => true, - }; - - matches_merchant && role.org_id == *org_id && Some(role.entity_type) == entity_type + .filter(|role| match &payload { + storage::ListRolesByEntityPayload::Organization(org_id) => { + let entity_in_vec = if is_lineage_data_required { + vec![ + EntityType::Organization, + EntityType::Merchant, + EntityType::Profile, + ] + } else { + vec![EntityType::Organization] + }; + + role.org_id == *org_id && entity_in_vec.contains(&role.entity_type) + } + storage::ListRolesByEntityPayload::Merchant(org_id, merchant_id) => { + let entity_in_vec = if is_lineage_data_required { + vec![EntityType::Merchant, EntityType::Profile] + } else { + vec![EntityType::Merchant] + }; + + role.org_id == *org_id + && (role.scope == RoleScope::Organization + || role.merchant_id == *merchant_id) + && entity_in_vec.contains(&role.entity_type) + } + storage::ListRolesByEntityPayload::Profile(org_id, merchant_id, profile_id) => { + let entity_in_vec = [EntityType::Profile]; + + let matches_merchant = + role.merchant_id == *merchant_id && role.scope == RoleScope::Merchant; + + let matches_profile = + role.profile_id + .as_ref() + .is_some_and(|profile_id_from_role| { + profile_id_from_role == profile_id + && role.scope == RoleScope::Profile + }); + + role.org_id == *org_id + && (role.scope == RoleScope::Organization + || matches_merchant + || matches_profile) + && entity_in_vec.contains(&role.entity_type) + } }) - .take(limit_usize) .cloned() .collect(); diff --git a/crates/router/src/services/authorization/roles.rs b/crates/router/src/services/authorization/roles.rs index c9c64b76143d..78211ff0aaf5 100644 --- a/crates/router/src/services/authorization/roles.rs +++ b/crates/router/src/services/authorization/roles.rs @@ -121,13 +121,14 @@ impl RoleInfo { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, ) -> CustomResult { if let Some(role) = predefined_roles::PREDEFINED_ROLES.get(role_id) { Ok(role.clone()) } else { state .global_store - .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id) + .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id, profile_id) .await .map(Self::from) } diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index ac8ee11fc6a2..8de6ac0052b8 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -4,6 +4,7 @@ use common_enums::{EntityType, PermissionGroup}; use common_utils::id_type; use diesel_models::{ enums::{UserRoleVersion, UserStatus}, + role::ListRolesByEntityPayload, user_role::{UserRole, UserRoleUpdate}, }; use error_stack::{report, Report, ResultExt}; @@ -48,6 +49,8 @@ pub async fn validate_role_name( role_name: &domain::RoleName, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, + entity_type: &EntityType, ) -> UserResult<()> { let role_name_str = role_name.clone().get_role_name(); @@ -55,16 +58,38 @@ pub async fn validate_role_name( .iter() .any(|(_, role_info)| role_info.get_role_name() == role_name_str); - // TODO: Create and use find_by_role_name to make this efficient - let is_present_in_custom_roles = state + let entity_type_for_role = match entity_type { + EntityType::Tenant | EntityType::Organization => { + ListRolesByEntityPayload::Organization(org_id.to_owned()) + } + EntityType::Merchant => { + ListRolesByEntityPayload::Merchant(org_id.to_owned(), merchant_id.to_owned()) + } + EntityType::Profile => ListRolesByEntityPayload::Profile( + org_id.to_owned(), + merchant_id.to_owned(), + profile_id.to_owned(), + ), + }; + + let is_present_in_custom_role = match state .global_store - .list_all_roles(merchant_id, org_id) + .generic_list_roles_by_entity_type(entity_type_for_role, false) .await - .change_context(UserErrors::InternalServerError)? - .iter() - .any(|role| role.role_name == role_name_str); + { + Ok(roles_list) => roles_list + .iter() + .any(|role| role.role_name == role_name_str), + Err(e) => { + if e.current_context().is_db_not_found() { + false + } else { + return Err(UserErrors::InternalServerError.into()); + } + } + }; - if is_present_in_predefined_roles || is_present_in_custom_roles { + if is_present_in_predefined_roles || is_present_in_custom_role { return Err(UserErrors::RoleNameAlreadyExists.into()); } diff --git a/migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql b/migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql new file mode 100644 index 000000000000..d611be2c3da6 --- /dev/null +++ b/migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE roles DROP COLUMN IF EXISTS profile_id; \ No newline at end of file diff --git a/migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql b/migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql new file mode 100644 index 000000000000..b3873266fec7 --- /dev/null +++ b/migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE roles ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64); \ No newline at end of file diff --git a/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql new file mode 100644 index 000000000000..c7c9cbeb4017 --- /dev/null +++ b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql new file mode 100644 index 000000000000..6fd9b07fd508 --- /dev/null +++ b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TYPE "RoleScope" +ADD VALUE IF NOT EXISTS 'profile'; \ No newline at end of file diff --git a/migrations/2024-12-18-061400_change-roles-index/down.sql b/migrations/2024-12-18-061400_change-roles-index/down.sql new file mode 100644 index 000000000000..f59b8c876691 --- /dev/null +++ b/migrations/2024-12-18-061400_change-roles-index/down.sql @@ -0,0 +1,10 @@ +-- This file should undo anything in `up.sql` +CREATE UNIQUE INDEX role_name_org_id_org_scope_index ON roles (org_id, role_name) +WHERE + scope = 'organization'; + +CREATE UNIQUE INDEX role_name_merchant_id_merchant_scope_index ON roles (merchant_id, role_name) +WHERE + scope = 'merchant'; + +DROP INDEX IF EXISTS roles_merchant_org_index; \ No newline at end of file diff --git a/migrations/2024-12-18-061400_change-roles-index/up.sql b/migrations/2024-12-18-061400_change-roles-index/up.sql new file mode 100644 index 000000000000..08203559eb9a --- /dev/null +++ b/migrations/2024-12-18-061400_change-roles-index/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here + +DROP INDEX IF EXISTS role_name_org_id_org_scope_index; + +DROP INDEX IF EXISTS role_name_merchant_id_merchant_scope_index; + +DROP INDEX IF EXISTS roles_merchant_org_index; + +CREATE INDEX roles_merchant_org_index ON roles ( + org_id, + merchant_id, + profile_id +); \ No newline at end of file