mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 03:13:56 +08:00
feat(user): create apis for custom role (#3763)
Co-authored-by: Mani Chandra Dulam <mani.dchandra@juspay.in> Co-authored-by: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com>
This commit is contained in:
@ -1,8 +1,11 @@
|
|||||||
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
||||||
|
|
||||||
use crate::user_role::{
|
use crate::user_role::{
|
||||||
AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, GetRoleRequest,
|
role::{
|
||||||
ListRolesResponse, RoleInfoResponse, TransferOrgOwnershipRequest, UpdateUserRoleRequest,
|
CreateRoleRequest, GetRoleRequest, ListRolesResponse, RoleInfoResponse, UpdateRoleRequest,
|
||||||
|
},
|
||||||
|
AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest,
|
||||||
|
TransferOrgOwnershipRequest, UpdateUserRoleRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
common_utils::impl_misc_api_event_type!(
|
common_utils::impl_misc_api_event_type!(
|
||||||
@ -13,5 +16,7 @@ common_utils::impl_misc_api_event_type!(
|
|||||||
UpdateUserRoleRequest,
|
UpdateUserRoleRequest,
|
||||||
AcceptInvitationRequest,
|
AcceptInvitationRequest,
|
||||||
DeleteUserRoleRequest,
|
DeleteUserRoleRequest,
|
||||||
TransferOrgOwnershipRequest
|
TransferOrgOwnershipRequest,
|
||||||
|
CreateRoleRequest,
|
||||||
|
UpdateRoleRequest
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,23 +1,8 @@
|
|||||||
use common_enums::RoleScope;
|
|
||||||
use common_utils::pii;
|
use common_utils::pii;
|
||||||
|
|
||||||
use crate::user::DashboardEntryResponse;
|
use crate::user::DashboardEntryResponse;
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
pub mod role;
|
||||||
pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
|
||||||
pub struct RoleInfoResponse {
|
|
||||||
pub role_id: String,
|
|
||||||
pub permissions: Vec<Permission>,
|
|
||||||
pub role_name: String,
|
|
||||||
pub role_scope: RoleScope,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct GetRoleRequest {
|
|
||||||
pub role_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
#[derive(Debug, serde::Serialize)]
|
||||||
pub enum Permission {
|
pub enum Permission {
|
||||||
|
|||||||
30
crates/api_models/src/user_role/role.rs
Normal file
30
crates/api_models/src/user_role/role.rs
Normal file
@ -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<PermissionGroup>,
|
||||||
|
pub role_scope: RoleScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct UpdateRoleRequest {
|
||||||
|
pub groups: Option<Vec<PermissionGroup>>,
|
||||||
|
pub role_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct RoleInfoResponse {
|
||||||
|
pub role_id: String,
|
||||||
|
pub permissions: Vec<super::Permission>,
|
||||||
|
pub role_name: String,
|
||||||
|
pub role_scope: RoleScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct GetRoleRequest {
|
||||||
|
pub role_id: String,
|
||||||
|
}
|
||||||
@ -46,35 +46,25 @@ pub struct RoleUpdateInternal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub enum RoleUpdate {
|
pub enum RoleUpdate {
|
||||||
UpdateGroup {
|
UpdateDetails {
|
||||||
groups: Vec<enums::PermissionGroup>,
|
groups: Option<Vec<enums::PermissionGroup>>,
|
||||||
last_modified_by: String,
|
role_name: Option<String>,
|
||||||
},
|
last_modified_at: PrimitiveDateTime,
|
||||||
UpdateRoleName {
|
|
||||||
role_name: String,
|
|
||||||
last_modified_by: String,
|
last_modified_by: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RoleUpdate> for RoleUpdateInternal {
|
impl From<RoleUpdate> for RoleUpdateInternal {
|
||||||
fn from(value: RoleUpdate) -> Self {
|
fn from(value: RoleUpdate) -> Self {
|
||||||
let last_modified_at = common_utils::date_time::now();
|
|
||||||
match value {
|
match value {
|
||||||
RoleUpdate::UpdateGroup {
|
RoleUpdate::UpdateDetails {
|
||||||
groups,
|
groups,
|
||||||
last_modified_by,
|
|
||||||
} => Self {
|
|
||||||
groups: Some(groups),
|
|
||||||
role_name: None,
|
|
||||||
last_modified_at,
|
|
||||||
last_modified_by,
|
|
||||||
},
|
|
||||||
RoleUpdate::UpdateRoleName {
|
|
||||||
role_name,
|
role_name,
|
||||||
last_modified_by,
|
last_modified_by,
|
||||||
|
last_modified_at,
|
||||||
} => Self {
|
} => Self {
|
||||||
groups: None,
|
groups,
|
||||||
role_name: Some(role_name),
|
role_name,
|
||||||
last_modified_at,
|
last_modified_at,
|
||||||
last_modified_by,
|
last_modified_by,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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_OPERATOR: &str = "merchant_operator";
|
||||||
pub const ROLE_ID_MERCHANT_CUSTOMER_SUPPORT: &str = "merchant_customer_support";
|
pub const ROLE_ID_MERCHANT_CUSTOMER_SUPPORT: &str = "merchant_customer_support";
|
||||||
pub const INTERNAL_USER_MERCHANT_ID: &str = "juspay000";
|
pub const INTERNAL_USER_MERCHANT_ID: &str = "juspay000";
|
||||||
|
pub const MAX_ROLE_NAME_LENGTH: usize = 64;
|
||||||
|
|||||||
@ -62,6 +62,10 @@ pub enum UserErrors {
|
|||||||
RoleNotFound,
|
RoleNotFound,
|
||||||
#[error("InvalidRoleOperationWithMessage")]
|
#[error("InvalidRoleOperationWithMessage")]
|
||||||
InvalidRoleOperationWithMessage(String),
|
InvalidRoleOperationWithMessage(String),
|
||||||
|
#[error("RoleNameParsingError")]
|
||||||
|
RoleNameParsingError,
|
||||||
|
#[error("RoleNameAlreadyExists")]
|
||||||
|
RoleNameAlreadyExists,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
|
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
|
||||||
@ -159,6 +163,12 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
|||||||
Self::InvalidRoleOperationWithMessage(_) => {
|
Self::InvalidRoleOperationWithMessage(_) => {
|
||||||
AER::BadRequest(ApiError::new(sub_code, 33, self.get_error_message(), None))
|
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::MaxInvitationsError => "Maximum invite count per request exceeded",
|
||||||
Self::RoleNotFound => "Role Not Found",
|
Self::RoleNotFound => "Role Not Found",
|
||||||
Self::InvalidRoleOperationWithMessage(error_message) => error_message,
|
Self::InvalidRoleOperationWithMessage(error_message) => error_message,
|
||||||
|
Self::RoleNameParsingError => "Invalid Role Name",
|
||||||
|
Self::RoleNameAlreadyExists => "Role name already exists",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,8 @@ use crate::{
|
|||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod role;
|
||||||
|
|
||||||
pub async fn get_authorization_info(
|
pub async fn get_authorization_info(
|
||||||
_state: AppState,
|
_state: AppState,
|
||||||
) -> UserResponse<user_role_api::AuthorizationInfoResponse> {
|
) -> UserResponse<user_role_api::AuthorizationInfoResponse> {
|
||||||
@ -30,98 +32,6 @@ pub async fn get_authorization_info(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_invitable_roles(
|
|
||||||
state: AppState,
|
|
||||||
user_from_token: auth::UserFromToken,
|
|
||||||
) -> UserResponse<user_role_api::ListRolesResponse> {
|
|
||||||
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<user_role_api::RoleInfoResponse> {
|
|
||||||
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<Vec<user_role_api::Permission>> {
|
|
||||||
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(
|
pub async fn update_user_role(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
user_from_token: auth::UserFromToken,
|
user_from_token: auth::UserFromToken,
|
||||||
|
|||||||
223
crates/router/src/core/user_role/role.rs
Normal file
223
crates/router/src/core/user_role/role.rs
Normal file
@ -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<Vec<Permission>> {
|
||||||
|
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<role_api::ListRolesResponse> {
|
||||||
|
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<role_api::RoleInfoResponse> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@ -200,29 +200,21 @@ impl RoleInterface for MockDb {
|
|||||||
role_update: storage::RoleUpdate,
|
role_update: storage::RoleUpdate,
|
||||||
) -> CustomResult<storage::Role, errors::StorageError> {
|
) -> CustomResult<storage::Role, errors::StorageError> {
|
||||||
let mut roles = self.roles.lock().await;
|
let mut roles = self.roles.lock().await;
|
||||||
let last_modified_at = common_utils::date_time::now();
|
|
||||||
|
|
||||||
roles
|
roles
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|role| role.role_id == role_id)
|
.find(|role| role.role_id == role_id)
|
||||||
.map(|role| {
|
.map(|role| {
|
||||||
*role = match role_update {
|
*role = match role_update {
|
||||||
storage::RoleUpdate::UpdateGroup {
|
storage::RoleUpdate::UpdateDetails {
|
||||||
groups,
|
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,
|
role_name,
|
||||||
last_modified_at,
|
last_modified_at,
|
||||||
last_modified_by,
|
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()
|
..role.to_owned()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1052,9 +1052,17 @@ impl User {
|
|||||||
// Role information
|
// Role information
|
||||||
route = route.service(
|
route = route.service(
|
||||||
web::scope("/role")
|
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("/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")]
|
#[cfg(feature = "dummy_connector")]
|
||||||
|
|||||||
@ -29,6 +29,7 @@ pub enum ApiIdentifier {
|
|||||||
Forex,
|
Forex,
|
||||||
RustLockerMigration,
|
RustLockerMigration,
|
||||||
Gsm,
|
Gsm,
|
||||||
|
Role,
|
||||||
User,
|
User,
|
||||||
UserRole,
|
UserRole,
|
||||||
ConnectorOnboarding,
|
ConnectorOnboarding,
|
||||||
@ -200,7 +201,9 @@ impl From<Flow> for ApiIdentifier {
|
|||||||
| Flow::GetAuthorizationInfo
|
| Flow::GetAuthorizationInfo
|
||||||
| Flow::AcceptInvitation
|
| Flow::AcceptInvitation
|
||||||
| Flow::DeleteUserRole
|
| Flow::DeleteUserRole
|
||||||
| Flow::TransferOrgOwnership => Self::UserRole,
|
| Flow::TransferOrgOwnership
|
||||||
|
| Flow::CreateRole
|
||||||
|
| Flow::UpdateRole => Self::UserRole,
|
||||||
|
|
||||||
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
|
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
|
||||||
Self::ConnectorOnboarding
|
Self::ConnectorOnboarding
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use crate::{
|
|||||||
core::{api_locking, user_role as user_role_core},
|
core::{api_locking, user_role as user_role_core},
|
||||||
services::{
|
services::{
|
||||||
api,
|
api,
|
||||||
authentication::{self as auth, UserFromToken},
|
authentication::{self as auth},
|
||||||
authorization::permissions::Permission,
|
authorization::permissions::Permission,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -29,6 +29,38 @@ pub async fn get_authorization_info(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_role_from_token(state: web::Data<AppState>, 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<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
json_payload: web::Json<user_role_api::role::CreateRoleRequest>,
|
||||||
|
) -> 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<AppState>, req: HttpRequest) -> HttpResponse {
|
pub async fn list_all_roles(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||||
let flow = Flow::ListRoles;
|
let flow = Flow::ListRoles;
|
||||||
Box::pin(api::server_wrap(
|
Box::pin(api::server_wrap(
|
||||||
@ -36,7 +68,7 @@ pub async fn list_all_roles(state: web::Data<AppState>, req: HttpRequest) -> Htt
|
|||||||
state.clone(),
|
state.clone(),
|
||||||
&req,
|
&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),
|
&auth::JWTAuth(Permission::UsersRead),
|
||||||
api_locking::LockAction::NotApplicable,
|
api_locking::LockAction::NotApplicable,
|
||||||
))
|
))
|
||||||
@ -49,7 +81,7 @@ pub async fn get_role(
|
|||||||
path: web::Path<String>,
|
path: web::Path<String>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let flow = Flow::GetRole;
|
let flow = Flow::GetRole;
|
||||||
let request_payload = user_role_api::GetRoleRequest {
|
let request_payload = user_role_api::role::GetRoleRequest {
|
||||||
role_id: path.into_inner(),
|
role_id: path.into_inner(),
|
||||||
};
|
};
|
||||||
Box::pin(api::server_wrap(
|
Box::pin(api::server_wrap(
|
||||||
@ -57,22 +89,29 @@ pub async fn get_role(
|
|||||||
state.clone(),
|
state.clone(),
|
||||||
&req,
|
&req,
|
||||||
request_payload,
|
request_payload,
|
||||||
user_role_core::get_role,
|
user_role_core::role::get_role,
|
||||||
&auth::JWTAuth(Permission::UsersRead),
|
&auth::JWTAuth(Permission::UsersRead),
|
||||||
api_locking::LockAction::NotApplicable,
|
api_locking::LockAction::NotApplicable,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_role_from_token(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
pub async fn update_role(
|
||||||
let flow = Flow::GetRoleFromToken;
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
json_payload: web::Json<user_role_api::role::UpdateRoleRequest>,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::UpdateRole;
|
||||||
|
let role_id = path.into_inner();
|
||||||
|
|
||||||
Box::pin(api::server_wrap(
|
Box::pin(api::server_wrap(
|
||||||
flow,
|
flow,
|
||||||
state.clone(),
|
state.clone(),
|
||||||
&req,
|
&req,
|
||||||
(),
|
json_payload.into_inner(),
|
||||||
|state, user: UserFromToken, _| user_role_core::get_role_from_token(state, user),
|
|state, user, req| user_role_core::role::update_role(state, user, req, &role_id),
|
||||||
&auth::DashboardNoPermissionAuth,
|
&auth::JWTAuth(Permission::UsersWrite),
|
||||||
api_locking::LockAction::NotApplicable,
|
api_locking::LockAction::NotApplicable,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -926,3 +926,23 @@ impl ForeignFrom<UserStatus> for user_role_api::UserStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RoleName(String);
|
||||||
|
|
||||||
|
impl RoleName {
|
||||||
|
pub fn new(name: String) -> UserResult<Self> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
use api_models::user_role as user_role_api;
|
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<Permission> for user_role_api::Permission {
|
impl From<Permission> for user_role_api::Permission {
|
||||||
fn from(value: Permission) -> Self {
|
fn from(value: Permission) -> Self {
|
||||||
@ -34,3 +39,24 @@ impl From<Permission> 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<String> = 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(())
|
||||||
|
}
|
||||||
|
|||||||
@ -363,6 +363,10 @@ pub enum Flow {
|
|||||||
UpdateUserAccountDetails,
|
UpdateUserAccountDetails,
|
||||||
/// Accept user invitation
|
/// Accept user invitation
|
||||||
AcceptInvitation,
|
AcceptInvitation,
|
||||||
|
/// Create Role
|
||||||
|
CreateRole,
|
||||||
|
/// Update Role
|
||||||
|
UpdateRole,
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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';
|
||||||
Reference in New Issue
Block a user