Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(roles): add list support for roles #5754

Merged
merged 9 commits into from
Sep 2, 2024
10 changes: 6 additions & 4 deletions crates/api_models/src/events/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use common_utils::events::{ApiEventMetric, ApiEventsType};

use crate::user_role::{
role::{
CreateRoleRequest, GetRoleFromTokenResponse, GetRoleRequest, ListRolesResponse,
RoleInfoResponse, RoleInfoWithGroupsResponse, RoleInfoWithPermissionsResponse,
UpdateRoleRequest,
CreateRoleRequest, GetRoleFromTokenResponse, GetRoleRequest, ListRolesAtEntityLevelRequest,
ListRolesResponse, RoleInfoResponse, RoleInfoResponseNew, RoleInfoWithGroupsResponse,
RoleInfoWithPermissionsResponse, UpdateRoleRequest,
},
AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest,
MerchantSelectRequest, UpdateUserRoleRequest,
Expand All @@ -25,6 +25,8 @@ common_utils::impl_api_event_type!(
ListRolesResponse,
RoleInfoResponse,
GetRoleFromTokenResponse,
RoleInfoWithGroupsResponse
RoleInfoWithGroupsResponse,
RoleInfoResponseNew,
ListRolesAtEntityLevelRequest
)
);
12 changes: 0 additions & 12 deletions crates/api_models/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,15 +418,3 @@ pub struct ListProfilesForUserInOrgAndMerchantAccountResponse {
pub profile_id: id_type::ProfileId,
pub profile_name: String,
}

#[derive(Debug, serde::Serialize)]
pub struct ListUsersInEntityResponse {
pub email: pii::Email,
pub roles: Vec<MinimalRoleInfo>,
}

#[derive(Debug, serde::Serialize, Clone)]
pub struct MinimalRoleInfo {
pub role_id: String,
pub role_name: String,
}
6 changes: 6 additions & 0 deletions crates/api_models/src/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,9 @@ pub struct AcceptInvitationRequest {
pub struct DeleteUserRoleRequest {
pub email: pii::Email,
}

#[derive(Debug, serde::Serialize)]
pub struct ListUsersInEntityResponse {
pub email: pii::Email,
pub roles: Vec<role::MinimalRoleInfo>,
}
27 changes: 26 additions & 1 deletion crates/api_models/src/user_role/role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ pub struct CreateRoleRequest {
pub role_name: String,
pub groups: Vec<PermissionGroup>,
pub role_scope: RoleScope,
pub entity_type: Option<EntityType>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
Expand Down Expand Up @@ -54,7 +53,33 @@ pub struct RoleInfoWithGroupsResponse {
pub role_scope: RoleScope,
}

#[derive(Debug, serde::Serialize)]
pub struct RoleInfoResponseNew {
pub role_id: String,
pub role_name: String,
pub entity_type: EntityType,
pub groups: Vec<PermissionGroup>,
pub scope: RoleScope,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct GetRoleRequest {
pub role_id: String,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct ListRolesAtEntityLevelRequest {
pub entity_type: EntityType,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub enum RoleCheckType {
Invite,
Update,
}

#[derive(Debug, serde::Serialize, Clone)]
pub struct MinimalRoleInfo {
pub role_id: String,
pub role_name: String,
}
55 changes: 53 additions & 2 deletions crates/diesel_models/src/query/role.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
use async_bb8_diesel::AsyncRunQueryDsl;
use common_utils::id_type;
use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods};
use diesel::{
associations::HasTable, debug_query, pg::Pg, result::Error as DieselError,
BoolExpressionMethods, ExpressionMethods, QueryDsl,
};
use error_stack::{report, ResultExt};

use crate::{
enums::RoleScope, query::generics, role::*, schema::roles::dsl, PgPooledConn, StorageResult,
enums::RoleScope, errors, query::generics, role::*, schema::roles::dsl, PgPooledConn,
StorageResult,
};

impl RoleNew {
Expand Down Expand Up @@ -81,4 +87,49 @@ impl Role {
)
.await
}

pub async fn generic_roles_list_for_org(
conn: &PgPooledConn,
org_id: id_type::OrganizationId,
merchant_id: Option<id_type::MerchantId>,
entity_type: Option<common_enums::EntityType>,
limit: Option<u32>,
) -> StorageResult<Vec<Self>> {
let mut query = <Self as HasTable>::table()
.filter(dsl::org_id.eq(org_id))
.into_boxed();

if let Some(merchant_id) = merchant_id {
query = query.filter(
dsl::merchant_id
.eq(merchant_id)
.or(dsl::scope.eq(RoleScope::Organization)),
);
}

if let Some(entity_type) = entity_type {
query = query.filter(dsl::entity_type.eq(entity_type))
}

if let Some(limit) = limit {
query = query.limit(limit.into());
}

router_env::logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());

match generics::db_metrics::track_database_call::<Self, _, _>(
query.get_results_async(conn),
generics::db_metrics::DatabaseOperation::Filter,
)
.await
{
Ok(value) => Ok(value),
Err(err) => match err {
DieselError::NotFound => {
Err(report!(err)).change_context(errors::DatabaseError::NotFound)
}
_ => Err(report!(err)).change_context(errors::DatabaseError::Others),
},
}
}
}
6 changes: 3 additions & 3 deletions crates/router/src/core/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ pub async fn delete_user_role(
pub async fn list_users_in_lineage(
state: SessionState,
user_from_token: auth::UserFromToken,
) -> UserResponse<Vec<user_api::ListUsersInEntityResponse>> {
) -> UserResponse<Vec<user_role_api::ListUsersInEntityResponse>> {
let requestor_role_info = roles::RoleInfo::from_role_id(
&state,
&user_from_token.role_id,
Expand Down Expand Up @@ -731,7 +731,7 @@ pub async fn list_users_in_lineage(
.map(|role_info| {
(
user_role.role_id.clone(),
user_api::MinimalRoleInfo {
user_role_api::role::MinimalRoleInfo {
role_id: user_role.role_id.clone(),
role_name: role_info.get_role_name().to_string(),
},
Expand All @@ -756,7 +756,7 @@ pub async fn list_users_in_lineage(
user_role_map
.into_iter()
.map(|(user_id, role_id_vec)| {
Ok::<_, error_stack::Report<UserErrors>>(user_api::ListUsersInEntityResponse {
Ok::<_, error_stack::Report<UserErrors>>(user_role_api::ListUsersInEntityResponse {
email: email_map
.remove(&user_id)
.ok_or(UserErrors::InternalServerError)?,
Expand Down
156 changes: 154 additions & 2 deletions crates/router/src/core/user_role/role.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use api_models::user_role::role::{self as role_api};
use common_enums::RoleScope;
use common_enums::{EntityType, RoleScope};
use common_utils::generate_id_with_default_len;
use diesel_models::role::{RoleNew, RoleUpdate};
use error_stack::{report, ResultExt};
Expand Down Expand Up @@ -86,7 +86,7 @@ pub async fn create_role(
org_id: user_from_token.org_id,
groups: req.groups,
scope: req.role_scope,
entity_type: req.entity_type,
entity_type: Some(EntityType::Merchant),
created_by: user_from_token.user_id.clone(),
last_modified_by: user_from_token.user_id,
created_at: now,
Expand Down Expand Up @@ -322,3 +322,155 @@ pub async fn update_role(
},
))
}

pub async fn list_roles_with_info(
state: SessionState,
user_from_token: UserFromToken,
) -> UserResponse<Vec<role_api::RoleInfoResponseNew>> {
let user_role_info = user_from_token
.get_role_info_from_db(&state)
.await
.attach_printable("Invalid role_id in JWT")?;

let predefined_roles_map = PREDEFINED_ROLES
.iter()
.filter(|(_, role_info)| user_role_info.get_entity_type() >= role_info.get_entity_type())
.map(|(role_id, role_info)| role_api::RoleInfoResponseNew {
role_id: role_id.to_string(),
role_name: role_info.get_role_name().to_string(),
entity_type: role_info.get_entity_type(),
groups: role_info.get_permission_groups().to_vec(),
scope: role_info.get_scope(),
});

let user_role_entity = user_role_info.get_entity_type();
let custom_roles = match user_role_entity {
EntityType::Organization => state
.store
.list_roles_for_org_by_parameters(&user_from_token.org_id, None, None, None)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to get roles")?,
EntityType::Merchant => state
.store
.list_roles_for_org_by_parameters(
&user_from_token.org_id,
Some(&user_from_token.merchant_id),
None,
None,
)
.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(),
EntityType::Internal => {
return Err(UserErrors::InvalidRoleOperationWithMessage(
"Internal roles are not allowed for this operation".to_string(),
)
.into());
}
};
let custom_roles_map = custom_roles.into_iter().filter_map(|role| {
let role_info = roles::RoleInfo::from(role);
(user_role_entity >= role_info.get_entity_type()).then_some(role_api::RoleInfoResponseNew {
role_id: role_info.get_role_id().to_string(),
role_name: role_info.get_role_name().to_string(),
groups: role_info.get_permission_groups().to_vec(),
entity_type: role_info.get_entity_type(),
scope: role_info.get_scope(),
})
});

Ok(ApplicationResponse::Json(
predefined_roles_map.chain(custom_roles_map).collect(),
))
}

pub async fn list_roles_at_entity_level(
state: SessionState,
user_from_token: UserFromToken,
req: role_api::ListRolesAtEntityLevelRequest,
check_type: role_api::RoleCheckType,
) -> UserResponse<Vec<role_api::MinimalRoleInfo>> {
let user_entity_type = user_from_token
.get_role_info_from_db(&state)
.await
.attach_printable("Invalid role_id in JWT")?
.get_entity_type();

if req.entity_type > user_entity_type {
return Err(UserErrors::InvalidRoleOperationWithMessage(
"User is attempting to request list roles above the current entity level".to_string(),
)
.into());
}

let predefined_roles_map = PREDEFINED_ROLES
.iter()
.filter(|(_, role_info)| {
let check_type = match check_type {
role_api::RoleCheckType::Invite => role_info.is_invitable(),
role_api::RoleCheckType::Update => role_info.is_updatable(),
};
check_type && role_info.get_entity_type() == req.entity_type
})
.map(|(role_id, role_info)| role_api::MinimalRoleInfo {
role_id: role_id.to_string(),
role_name: role_info.get_role_name().to_string(),
});

let custom_roles = match req.entity_type {
EntityType::Organization => state
.store
.list_roles_for_org_by_parameters(
&user_from_token.org_id,
None,
Some(req.entity_type),
None,
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to get roles")?,

EntityType::Merchant => state
.store
.list_roles_for_org_by_parameters(
&user_from_token.org_id,
Some(&user_from_token.merchant_id),
Some(req.entity_type),
None,
)
.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(),
EntityType::Internal => {
return Err(UserErrors::InvalidRoleOperationWithMessage(
"Internal roles are not allowed for this operation".to_string(),
)
.into());
}
};

let custom_roles_map = custom_roles.into_iter().filter_map(|role| {
let role_info = roles::RoleInfo::from(role);

if match check_type {
role_api::RoleCheckType::Invite => role_info.is_invitable(),
role_api::RoleCheckType::Update => role_info.is_updatable(),
} {
Some(role_api::MinimalRoleInfo {
role_id: role_info.get_role_id().to_string(),
role_name: role_info.get_role_name().to_string(),
})
} else {
None
}
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Push every thing to a vector once and then do these checks.


Ok(ApplicationResponse::Json(
predefined_roles_map.chain(custom_roles_map).collect(),
))
}
12 changes: 12 additions & 0 deletions crates/router/src/db/kafka_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3272,6 +3272,18 @@ impl RoleInterface for KafkaStore {
) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
self.diesel_store.list_all_roles(merchant_id, org_id).await
}

async fn list_roles_for_org_by_parameters(
&self,
org_id: &id_type::OrganizationId,
merchant_id: Option<&id_type::MerchantId>,
entity_type: Option<enums::EntityType>,
limit: Option<u32>,
) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
self.diesel_store
.list_roles_for_org_by_parameters(org_id, merchant_id, entity_type, limit)
.await
}
}

#[async_trait::async_trait]
Expand Down
Loading
Loading