From 58809ab1f9c00d802b9a2a3d30b17dff1614431d Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:55:47 +0530 Subject: [PATCH] feat(user): create apis for custom role (#3763) Co-authored-by: Mani Chandra Dulam Co-authored-by: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> --- crates/api_models/src/events/user_role.rs | 11 +- crates/api_models/src/user_role.rs | 17 +- crates/api_models/src/user_role/role.rs | 30 +++ crates/diesel_models/src/role.rs | 26 +- crates/router/src/consts/user_role.rs | 1 + crates/router/src/core/errors/user.rs | 12 + crates/router/src/core/user_role.rs | 94 +------- crates/router/src/core/user_role/role.rs | 223 ++++++++++++++++++ crates/router/src/db/role.rs | 20 +- crates/router/src/routes/app.rs | 12 +- crates/router/src/routes/lock_utils.rs | 5 +- crates/router/src/routes/user_role.rs | 57 ++++- crates/router/src/types/domain/user.rs | 20 ++ crates/router/src/utils/user_role.rs | 28 ++- crates/router_env/src/logger/types.rs | 4 + .../down.sql | 3 + .../up.sql | 3 + 17 files changed, 410 insertions(+), 156 deletions(-) create mode 100644 crates/api_models/src/user_role/role.rs create mode 100644 crates/router/src/core/user_role/role.rs create mode 100644 migrations/2024-02-22-100718_role_name_org_id_constraint/down.sql create mode 100644 migrations/2024-02-22-100718_role_name_org_id_constraint/up.sql diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs index 2b8d022149..f19ff95f9e 100644 --- a/crates/api_models/src/events/user_role.rs +++ b/crates/api_models/src/events/user_role.rs @@ -1,8 +1,11 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::user_role::{ - AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, GetRoleRequest, - ListRolesResponse, RoleInfoResponse, TransferOrgOwnershipRequest, UpdateUserRoleRequest, + role::{ + CreateRoleRequest, GetRoleRequest, ListRolesResponse, RoleInfoResponse, UpdateRoleRequest, + }, + AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, + TransferOrgOwnershipRequest, UpdateUserRoleRequest, }; common_utils::impl_misc_api_event_type!( @@ -13,5 +16,7 @@ common_utils::impl_misc_api_event_type!( UpdateUserRoleRequest, AcceptInvitationRequest, DeleteUserRoleRequest, - TransferOrgOwnershipRequest + TransferOrgOwnershipRequest, + CreateRoleRequest, + UpdateRoleRequest ); diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 1c4c28aa99..ed0838adc2 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -1,23 +1,8 @@ -use common_enums::RoleScope; use common_utils::pii; use crate::user::DashboardEntryResponse; -#[derive(Debug, serde::Serialize)] -pub struct ListRolesResponse(pub Vec); - -#[derive(Debug, serde::Serialize)] -pub struct RoleInfoResponse { - pub role_id: String, - pub permissions: Vec, - pub role_name: String, - pub role_scope: RoleScope, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct GetRoleRequest { - pub role_id: String, -} +pub mod role; #[derive(Debug, serde::Serialize)] pub enum Permission { diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs new file mode 100644 index 0000000000..4a767cfa72 --- /dev/null +++ b/crates/api_models/src/user_role/role.rs @@ -0,0 +1,30 @@ +use common_enums::{PermissionGroup, RoleScope}; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct CreateRoleRequest { + pub role_name: String, + pub groups: Vec, + pub role_scope: RoleScope, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UpdateRoleRequest { + pub groups: Option>, + pub role_name: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct ListRolesResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct RoleInfoResponse { + pub role_id: String, + pub permissions: Vec, + pub role_name: String, + pub role_scope: RoleScope, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetRoleRequest { + pub role_id: String, +} diff --git a/crates/diesel_models/src/role.rs b/crates/diesel_models/src/role.rs index 6b21b1461c..075d060255 100644 --- a/crates/diesel_models/src/role.rs +++ b/crates/diesel_models/src/role.rs @@ -46,35 +46,25 @@ pub struct RoleUpdateInternal { } pub enum RoleUpdate { - UpdateGroup { - groups: Vec, - last_modified_by: String, - }, - UpdateRoleName { - role_name: String, + UpdateDetails { + groups: Option>, + role_name: Option, + last_modified_at: PrimitiveDateTime, last_modified_by: String, }, } impl From for RoleUpdateInternal { fn from(value: RoleUpdate) -> Self { - let last_modified_at = common_utils::date_time::now(); match value { - RoleUpdate::UpdateGroup { + RoleUpdate::UpdateDetails { groups, - last_modified_by, - } => Self { - groups: Some(groups), - role_name: None, - last_modified_at, - last_modified_by, - }, - RoleUpdate::UpdateRoleName { role_name, last_modified_by, + last_modified_at, } => Self { - groups: None, - role_name: Some(role_name), + groups, + role_name, last_modified_at, last_modified_by, }, diff --git a/crates/router/src/consts/user_role.rs b/crates/router/src/consts/user_role.rs index ae1436bcd6..0a5d6556a1 100644 --- a/crates/router/src/consts/user_role.rs +++ b/crates/router/src/consts/user_role.rs @@ -9,3 +9,4 @@ pub const ROLE_ID_MERCHANT_DEVELOPER: &str = "merchant_developer"; pub const ROLE_ID_MERCHANT_OPERATOR: &str = "merchant_operator"; pub const ROLE_ID_MERCHANT_CUSTOMER_SUPPORT: &str = "merchant_customer_support"; pub const INTERNAL_USER_MERCHANT_ID: &str = "juspay000"; +pub const MAX_ROLE_NAME_LENGTH: usize = 64; diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index c837fd7c20..8a7d77cdbc 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -62,6 +62,10 @@ pub enum UserErrors { RoleNotFound, #[error("InvalidRoleOperationWithMessage")] InvalidRoleOperationWithMessage(String), + #[error("RoleNameParsingError")] + RoleNameParsingError, + #[error("RoleNameAlreadyExists")] + RoleNameAlreadyExists, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -159,6 +163,12 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 33, self.get_error_message(), None)) } + Self::RoleNameParsingError => { + AER::BadRequest(ApiError::new(sub_code, 34, self.get_error_message(), None)) + } + Self::RoleNameAlreadyExists => { + AER::BadRequest(ApiError::new(sub_code, 35, self.get_error_message(), None)) + } } } } @@ -193,6 +203,8 @@ impl UserErrors { Self::MaxInvitationsError => "Maximum invite count per request exceeded", Self::RoleNotFound => "Role Not Found", Self::InvalidRoleOperationWithMessage(error_message) => error_message, + Self::RoleNameParsingError => "Invalid Role Name", + Self::RoleNameAlreadyExists => "Role name already exists", } } } diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index c2dfd34322..b954e0df6c 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -17,6 +17,8 @@ use crate::{ utils, }; +pub mod role; + pub async fn get_authorization_info( _state: AppState, ) -> UserResponse { @@ -30,98 +32,6 @@ pub async fn get_authorization_info( )) } -pub async fn list_invitable_roles( - state: AppState, - user_from_token: auth::UserFromToken, -) -> UserResponse { - let predefined_roles_map = roles::predefined_roles::PREDEFINED_ROLES - .iter() - .filter(|(_, role_info)| role_info.is_invitable()) - .map(|(role_id, role_info)| user_role_api::RoleInfoResponse { - permissions: role_info - .get_permissions_set() - .into_iter() - .map(Into::into) - .collect(), - role_id: role_id.to_string(), - role_name: role_info.get_role_name().to_string(), - role_scope: role_info.get_scope(), - }); - - let custom_roles_map = state - .store - .list_all_roles(&user_from_token.merchant_id, &user_from_token.org_id) - .await - .change_context(UserErrors::InternalServerError)? - .into_iter() - .map(roles::RoleInfo::from) - .filter(|role_info| role_info.is_invitable()) - .map(|role_info| user_role_api::RoleInfoResponse { - permissions: role_info - .get_permissions_set() - .into_iter() - .map(Into::into) - .collect(), - role_id: role_info.get_role_id().to_string(), - role_name: role_info.get_role_name().to_string(), - role_scope: role_info.get_scope(), - }); - - Ok(ApplicationResponse::Json(user_role_api::ListRolesResponse( - predefined_roles_map.chain(custom_roles_map).collect(), - ))) -} - -pub async fn get_role( - state: AppState, - user_from_token: auth::UserFromToken, - role: user_role_api::GetRoleRequest, -) -> UserResponse { - let role_info = roles::get_role_info_from_role_id( - &state, - &role.role_id, - &user_from_token.merchant_id, - &user_from_token.org_id, - ) - .await - .to_not_found_response(UserErrors::InvalidRoleId)?; - - if role_info.is_internal() { - return Err(UserErrors::InvalidRoleId.into()); - } - - let permissions = role_info - .get_permissions_set() - .into_iter() - .map(Into::into) - .collect(); - - Ok(ApplicationResponse::Json(user_role_api::RoleInfoResponse { - permissions, - role_id: role.role_id, - role_name: role_info.get_role_name().to_string(), - role_scope: role_info.get_scope(), - })) -} - -pub async fn get_role_from_token( - state: AppState, - user_from_token: auth::UserFromToken, -) -> UserResponse> { - let role_info = user_from_token - .get_role_info_from_db(&state) - .await - .attach_printable("Invalid role_id in JWT")?; - - let permissions = role_info - .get_permissions_set() - .into_iter() - .map(Into::into) - .collect(); - - Ok(ApplicationResponse::Json(permissions)) -} - pub async fn update_user_role( state: AppState, user_from_token: auth::UserFromToken, diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs new file mode 100644 index 0000000000..7ce72779bb --- /dev/null +++ b/crates/router/src/core/user_role/role.rs @@ -0,0 +1,223 @@ +use api_models::user_role::{ + role::{self as role_api}, + Permission, +}; +use common_enums::RoleScope; +use common_utils::generate_id_with_default_len; +use diesel_models::role::{RoleNew, RoleUpdate}; +use error_stack::ResultExt; + +use crate::{ + consts, + core::errors::{StorageErrorExt, UserErrors, UserResponse}, + routes::AppState, + services::{ + authentication::UserFromToken, + authorization::roles::{self, predefined_roles::PREDEFINED_ROLES}, + ApplicationResponse, + }, + types::domain::user::RoleName, + utils, +}; + +pub async fn get_role_from_token( + state: AppState, + user_from_token: UserFromToken, +) -> UserResponse> { + let role_info = user_from_token + .get_role_info_from_db(&state) + .await + .attach_printable("Invalid role_id in JWT")?; + + let permissions = role_info + .get_permissions_set() + .into_iter() + .map(Into::into) + .collect(); + + Ok(ApplicationResponse::Json(permissions)) +} + +pub async fn create_role( + state: AppState, + user_from_token: UserFromToken, + req: role_api::CreateRoleRequest, +) -> UserResponse<()> { + let now = common_utils::date_time::now(); + let role_name = RoleName::new(req.role_name)?.get_role_name(); + + if req.groups.is_empty() { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("Role groups cannot be empty"); + } + + if matches!(req.role_scope, RoleScope::Organization) + && user_from_token.role_id != consts::user_role::ROLE_ID_ORGANIZATION_ADMIN + { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("Non org admin user creating org level role"); + } + + utils::user_role::is_role_name_already_present_for_merchant( + &state, + &role_name, + &user_from_token.merchant_id, + &user_from_token.org_id, + ) + .await?; + + state + .store + .insert_role(RoleNew { + role_id: generate_id_with_default_len("role"), + role_name, + merchant_id: user_from_token.merchant_id, + org_id: user_from_token.org_id, + groups: req.groups, + scope: req.role_scope, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id, + created_at: now, + last_modified_at: now, + }) + .await + .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn list_invitable_roles( + state: AppState, + user_from_token: UserFromToken, +) -> UserResponse { + let predefined_roles_map = PREDEFINED_ROLES + .iter() + .filter(|(_, role_info)| role_info.is_invitable()) + .map(|(role_id, role_info)| role_api::RoleInfoResponse { + permissions: role_info + .get_permissions_set() + .into_iter() + .map(Into::into) + .collect(), + role_id: role_id.to_string(), + role_name: role_info.get_role_name().to_string(), + role_scope: role_info.get_scope(), + }); + + let custom_roles_map = state + .store + .list_all_roles(&user_from_token.merchant_id, &user_from_token.org_id) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .map(roles::RoleInfo::from) + .filter(|role_info| role_info.is_invitable()) + .map(|role_info| role_api::RoleInfoResponse { + permissions: role_info + .get_permissions_set() + .into_iter() + .map(Into::into) + .collect(), + role_id: role_info.get_role_id().to_string(), + role_name: role_info.get_role_name().to_string(), + role_scope: role_info.get_scope(), + }); + + Ok(ApplicationResponse::Json(role_api::ListRolesResponse( + predefined_roles_map.chain(custom_roles_map).collect(), + ))) +} + +pub async fn get_role( + state: AppState, + user_from_token: UserFromToken, + role: role_api::GetRoleRequest, +) -> UserResponse { + let role_info = roles::get_role_info_from_role_id( + &state, + &role.role_id, + &user_from_token.merchant_id, + &user_from_token.org_id, + ) + .await + .to_not_found_response(UserErrors::InvalidRoleId)?; + + if role_info.is_internal() { + return Err(UserErrors::InvalidRoleId.into()); + } + + let permissions = role_info + .get_permissions_set() + .into_iter() + .map(Into::into) + .collect(); + + Ok(ApplicationResponse::Json(role_api::RoleInfoResponse { + permissions, + role_id: role.role_id, + role_name: role_info.get_role_name().to_string(), + role_scope: role_info.get_scope(), + })) +} + +pub async fn update_role( + state: AppState, + user_from_token: UserFromToken, + req: role_api::UpdateRoleRequest, + role_id: &str, +) -> UserResponse<()> { + let role_name = req + .role_name + .map(RoleName::new) + .transpose()? + .map(RoleName::get_role_name); + + if let Some(ref role_name) = role_name { + utils::user_role::is_role_name_already_present_for_merchant( + &state, + role_name, + &user_from_token.merchant_id, + &user_from_token.org_id, + ) + .await?; + } + + let role_info = roles::get_role_info_from_role_id( + &state, + role_id, + &user_from_token.merchant_id, + &user_from_token.org_id, + ) + .await + .to_not_found_response(UserErrors::InvalidRoleOperation)?; + + if matches!(role_info.get_scope(), RoleScope::Organization) + && user_from_token.role_id != consts::user_role::ROLE_ID_ORGANIZATION_ADMIN + { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("Non org admin user creating org level role"); + } + + if let Some(ref groups) = req.groups { + if groups.is_empty() { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("role groups cannot be empty"); + } + } + + state + .store + .update_role_by_role_id( + role_id, + RoleUpdate::UpdateDetails { + groups: req.groups, + role_name, + last_modified_at: common_utils::date_time::now(), + last_modified_by: user_from_token.user_id, + }, + ) + .await + .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs index 90e1e97e37..111b531e2f 100644 --- a/crates/router/src/db/role.rs +++ b/crates/router/src/db/role.rs @@ -200,29 +200,21 @@ impl RoleInterface for MockDb { role_update: storage::RoleUpdate, ) -> CustomResult { let mut roles = self.roles.lock().await; - let last_modified_at = common_utils::date_time::now(); - roles .iter_mut() .find(|role| role.role_id == role_id) .map(|role| { *role = match role_update { - storage::RoleUpdate::UpdateGroup { + storage::RoleUpdate::UpdateDetails { groups, - last_modified_by, - } => storage::Role { - groups, - last_modified_by, - last_modified_at, - ..role.to_owned() - }, - storage::RoleUpdate::UpdateRoleName { - role_name, - last_modified_by, - } => storage::Role { role_name, last_modified_at, last_modified_by, + } => storage::Role { + groups: groups.unwrap_or(role.groups.to_owned()), + role_name: role_name.unwrap_or(role.role_name.to_owned()), + last_modified_by, + last_modified_at, ..role.to_owned() }, }; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 26f5b52807..a11265fde8 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1052,9 +1052,17 @@ impl User { // Role information route = route.service( web::scope("/role") - .service(web::resource("").route(web::get().to(get_role_from_token))) + .service( + web::resource("") + .route(web::get().to(get_role_from_token)) + .route(web::post().to(create_role)), + ) .service(web::resource("/list").route(web::get().to(list_all_roles))) - .service(web::resource("/{role_id}").route(web::get().to(get_role))), + .service( + web::resource("/{role_id}") + .route(web::get().to(get_role)) + .route(web::put().to(update_role)), + ), ); #[cfg(feature = "dummy_connector")] diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index e3b04bd3f7..0214d3b5e3 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -29,6 +29,7 @@ pub enum ApiIdentifier { Forex, RustLockerMigration, Gsm, + Role, User, UserRole, ConnectorOnboarding, @@ -200,7 +201,9 @@ impl From for ApiIdentifier { | Flow::GetAuthorizationInfo | Flow::AcceptInvitation | Flow::DeleteUserRole - | Flow::TransferOrgOwnership => Self::UserRole, + | Flow::TransferOrgOwnership + | Flow::CreateRole + | Flow::UpdateRole => Self::UserRole, Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { Self::ConnectorOnboarding diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index 63e2ce3726..52e739c36e 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -7,7 +7,7 @@ use crate::{ core::{api_locking, user_role as user_role_core}, services::{ api, - authentication::{self as auth, UserFromToken}, + authentication::{self as auth}, authorization::permissions::Permission, }, }; @@ -29,6 +29,38 @@ pub async fn get_authorization_info( .await } +pub async fn get_role_from_token(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::GetRoleFromToken; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, user, _| user_role_core::role::get_role_from_token(state, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn create_role( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::CreateRole; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + user_role_core::role::create_role, + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn list_all_roles(state: web::Data, req: HttpRequest) -> HttpResponse { let flow = Flow::ListRoles; Box::pin(api::server_wrap( @@ -36,7 +68,7 @@ pub async fn list_all_roles(state: web::Data, req: HttpRequest) -> Htt state.clone(), &req, (), - |state, user, _| user_role_core::list_invitable_roles(state, user), + |state, user, _| user_role_core::role::list_invitable_roles(state, user), &auth::JWTAuth(Permission::UsersRead), api_locking::LockAction::NotApplicable, )) @@ -49,7 +81,7 @@ pub async fn get_role( path: web::Path, ) -> HttpResponse { let flow = Flow::GetRole; - let request_payload = user_role_api::GetRoleRequest { + let request_payload = user_role_api::role::GetRoleRequest { role_id: path.into_inner(), }; Box::pin(api::server_wrap( @@ -57,22 +89,29 @@ pub async fn get_role( state.clone(), &req, request_payload, - user_role_core::get_role, + user_role_core::role::get_role, &auth::JWTAuth(Permission::UsersRead), api_locking::LockAction::NotApplicable, )) .await } -pub async fn get_role_from_token(state: web::Data, req: HttpRequest) -> HttpResponse { - let flow = Flow::GetRoleFromToken; +pub async fn update_role( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> HttpResponse { + let flow = Flow::UpdateRole; + let role_id = path.into_inner(); + Box::pin(api::server_wrap( flow, state.clone(), &req, - (), - |state, user: UserFromToken, _| user_role_core::get_role_from_token(state, user), - &auth::DashboardNoPermissionAuth, + json_payload.into_inner(), + |state, user, req| user_role_core::role::update_role(state, user, req, &role_id), + &auth::JWTAuth(Permission::UsersWrite), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 263b0e52b8..a759bf0080 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -926,3 +926,23 @@ impl ForeignFrom for user_role_api::UserStatus { } } } + +#[derive(Clone)] +pub struct RoleName(String); + +impl RoleName { + pub fn new(name: String) -> UserResult { + let is_empty_or_whitespace = name.trim().is_empty(); + let is_too_long = name.graphemes(true).count() > consts::user_role::MAX_ROLE_NAME_LENGTH; + + if is_empty_or_whitespace || is_too_long || name.contains(' ') { + Err(UserErrors::RoleNameParsingError.into()) + } else { + Ok(Self(name.to_lowercase())) + } + } + + pub fn get_role_name(self) -> String { + self.0 + } +} diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index ef69219b4c..8ae65ce86e 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -1,6 +1,11 @@ use api_models::user_role as user_role_api; +use error_stack::ResultExt; -use crate::services::authorization::permissions::Permission; +use crate::{ + core::errors::{UserErrors, UserResult}, + routes::AppState, + services::authorization::permissions::Permission, +}; impl From for user_role_api::Permission { fn from(value: Permission) -> Self { @@ -34,3 +39,24 @@ impl From for user_role_api::Permission { } } } + +pub async fn is_role_name_already_present_for_merchant( + state: &AppState, + role_name: &str, + merchant_id: &str, + org_id: &str, +) -> UserResult<()> { + let role_name_list: Vec = state + .store + .list_all_roles(merchant_id, org_id) + .await + .change_context(UserErrors::InternalServerError)? + .iter() + .map(|role| role.role_name.to_owned()) + .collect(); + + if role_name_list.contains(&role_name.to_string()) { + return Err(UserErrors::RoleNameAlreadyExists.into()); + } + Ok(()) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index b2e665059e..4af81ae49c 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -363,6 +363,10 @@ pub enum Flow { UpdateUserAccountDetails, /// Accept user invitation AcceptInvitation, + /// Create Role + CreateRole, + /// Update Role + UpdateRole, } /// diff --git a/migrations/2024-02-22-100718_role_name_org_id_constraint/down.sql b/migrations/2024-02-22-100718_role_name_org_id_constraint/down.sql new file mode 100644 index 0000000000..3fcac875de --- /dev/null +++ b/migrations/2024-02-22-100718_role_name_org_id_constraint/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP INDEX role_name_org_id_org_scope_index; +DROP INDEX role_name_merchant_id_merchant_scope_index; diff --git a/migrations/2024-02-22-100718_role_name_org_id_constraint/up.sql b/migrations/2024-02-22-100718_role_name_org_id_constraint/up.sql new file mode 100644 index 0000000000..9780e01b77 --- /dev/null +++ b/migrations/2024-02-22-100718_role_name_org_id_constraint/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +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';