diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs index e0df36a334..7b6413b27f 100644 --- a/crates/api_models/src/events/user_role.rs +++ b/crates/api_models/src/events/user_role.rs @@ -2,9 +2,10 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::user_role::{ role::{ - CreateRoleRequest, GetRoleRequest, GroupsAndResources, ListRolesAtEntityLevelRequest, - ListRolesRequest, RoleInfoResponseNew, RoleInfoWithGroupsResponse, RoleInfoWithParents, - UpdateRoleRequest, + CreateRoleRequest, CreateRoleV2Request, GetParentGroupsInfoQueryParams, GetRoleRequest, + GroupsAndResources, ListRolesAtEntityLevelRequest, ListRolesQueryParams, ListRolesResponse, + ParentGroupInfoRequest, RoleInfoResponseNew, RoleInfoResponseWithParentsGroup, + RoleInfoWithGroupsResponse, RoleInfoWithParents, UpdateRoleRequest, }, AuthorizationInfoResponse, DeleteUserRoleRequest, ListUsersInEntityRequest, UpdateUserRoleRequest, @@ -14,17 +15,22 @@ common_utils::impl_api_event_type!( Miscellaneous, ( GetRoleRequest, + GetParentGroupsInfoQueryParams, AuthorizationInfoResponse, UpdateUserRoleRequest, DeleteUserRoleRequest, CreateRoleRequest, + CreateRoleV2Request, UpdateRoleRequest, ListRolesAtEntityLevelRequest, RoleInfoResponseNew, RoleInfoWithGroupsResponse, ListUsersInEntityRequest, - ListRolesRequest, + ListRolesQueryParams, GroupsAndResources, - RoleInfoWithParents + RoleInfoWithParents, + ParentGroupInfoRequest, + RoleInfoResponseWithParentsGroup, + ListRolesResponse ) ); diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs index 6b8736d3e7..0ab853273e 100644 --- a/crates/api_models/src/user_role/role.rs +++ b/crates/api_models/src/user_role/role.rs @@ -10,6 +10,14 @@ pub struct CreateRoleRequest { pub entity_type: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct CreateRoleV2Request { + pub role_name: String, + pub role_scope: RoleScope, + pub entity_type: Option, + pub parent_groups: Vec, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct UpdateRoleRequest { pub groups: Option>, @@ -41,8 +49,15 @@ pub struct ParentGroupInfo { } #[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct ListRolesRequest { +pub struct ParentGroupInfoRequest { + pub name: ParentGroup, + pub scopes: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ListRolesQueryParams { pub entity_type: Option, + pub groups: Option, } #[derive(Debug, serde::Serialize)] @@ -54,6 +69,15 @@ pub struct RoleInfoResponseNew { pub scope: RoleScope, } +#[derive(Debug, serde::Serialize)] +pub struct RoleInfoResponseWithParentsGroup { + pub role_id: String, + pub role_name: String, + pub entity_type: EntityType, + pub parent_groups: Vec, + pub role_scope: RoleScope, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct GetRoleRequest { pub role_id: String, @@ -64,6 +88,11 @@ pub struct ListRolesAtEntityLevelRequest { pub entity_type: EntityType, } +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetParentGroupsInfoQueryParams { + pub entity_type: Option, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub enum RoleCheckType { Invite, @@ -81,3 +110,10 @@ pub struct GroupsAndResources { pub groups: Vec, pub resources: Vec, } + +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum ListRolesResponse { + WithGroups(Vec), + WithParentGroups(Vec), +} diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index bf93c6fa00..22091eca89 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -7708,7 +7708,9 @@ pub enum PermissionGroup { ThemeManage, } -#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash, strum::EnumIter)] +#[derive( + Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, strum::EnumIter, +)] pub enum ParentGroup { Operations, Connectors, @@ -7722,7 +7724,7 @@ pub enum ParentGroup { Theme, } -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum Resource { Payment, @@ -7753,7 +7755,9 @@ pub enum Resource { Theme, } -#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)] +#[derive( + Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize, Hash, +)] #[serde(rename_all = "snake_case")] pub enum PermissionScope { Read = 0, diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index edad36e5aa..1b4870f05e 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -89,6 +89,7 @@ pub async fn get_authorization_info_with_group_tag( pub async fn get_parent_group_info( state: SessionState, user_from_token: auth::UserFromToken, + request: role_api::GetParentGroupsInfoQueryParams, ) -> UserResponse> { let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, @@ -102,24 +103,34 @@ pub async fn get_parent_group_info( .await .to_not_found_response(UserErrors::InvalidRoleId)?; - let parent_groups = ParentGroup::get_descriptions_for_groups( - role_info.get_entity_type(), - PermissionGroup::iter().collect(), - ) - .unwrap_or_default() - .into_iter() - .map(|(parent_group, description)| role_api::ParentGroupInfo { - name: parent_group.clone(), - description, - scopes: PermissionGroup::iter() - .filter_map(|group| (group.parent() == parent_group).then_some(group.scope())) - // TODO: Remove this hashset conversion when merhant access - // and organization access groups are removed - .collect::>() + let entity_type = request + .entity_type + .unwrap_or_else(|| role_info.get_entity_type()); + + if role_info.get_entity_type() < entity_type { + return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!( + "Invalid operation, requestor entity type = {} cannot access entity type = {}", + role_info.get_entity_type(), + entity_type + )); + } + + let parent_groups = + ParentGroup::get_descriptions_for_groups(entity_type, PermissionGroup::iter().collect()) + .unwrap_or_default() .into_iter() - .collect(), - }) - .collect::>(); + .map(|(parent_group, description)| role_api::ParentGroupInfo { + name: parent_group.clone(), + description, + scopes: PermissionGroup::iter() + .filter_map(|group| (group.parent() == parent_group).then_some(group.scope())) + // TODO: Remove this hashset conversion when merchant access + // and organization access groups are removed + .collect::>() + .into_iter() + .collect(), + }) + .collect::>(); Ok(ApplicationResponse::Json(parent_groups)) } diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs index 554c0cd7a2..cbd49a1439 100644 --- a/crates/router/src/core/user_role/role.rs +++ b/crates/router/src/core/user_role/role.rs @@ -157,6 +157,111 @@ pub async fn create_role( )) } +pub async fn create_role_v2( + state: SessionState, + user_from_token: UserFromToken, + req: role_api::CreateRoleV2Request, + _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"); + } + + 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 {role_entity_type} and scope {requestor_entity_from_role_scope}", + )); + } + + 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!( + "{user_entity_type} is trying to create of scope {requestor_entity_from_role_scope} and of type {role_entity_type}", + )); + } + + let role_name = RoleName::new(req.role_name.clone())?; + + let permission_groups = + utils::user_role::parent_group_info_request_to_permission_groups(&req.parent_groups)?; + + utils::user_role::validate_role_groups(&permission_groups)?; + utils::user_role::validate_role_name( + &state, + &role_name, + &user_from_token.merchant_id, + &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + &user_from_token.profile_id, + &role_entity_type, + ) + .await?; + + let (org_id, merchant_id, profile_id) = match role_entity_type { + EntityType::Organization | EntityType::Tenant => (user_from_token.org_id, None, None), + EntityType::Merchant => ( + user_from_token.org_id, + Some(user_from_token.merchant_id), + None, + ), + EntityType::Profile => ( + user_from_token.org_id, + Some(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, + org_id, + groups: permission_groups, + scope: req.role_scope, + 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, + tenant_id: user_from_token.tenant_id.unwrap_or(state.tenant.tenant_id), + }) + .await + .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?; + + let response_parent_groups = + utils::user_role::permission_groups_to_parent_group_info(&role.groups, role.entity_type); + + Ok(ApplicationResponse::Json( + role_api::RoleInfoResponseWithParentsGroup { + role_id: role.role_id, + role_name: role.role_name, + role_scope: role.scope, + entity_type: role.entity_type, + parent_groups: response_parent_groups, + }, + )) +} + pub async fn get_role_with_groups( state: SessionState, user_from_token: UserFromToken, @@ -227,7 +332,7 @@ pub async fn get_parent_info_for_role( .get_permission_groups() .iter() .filter_map(|group| (group.parent() == parent_group).then_some(group.scope())) - // TODO: Remove this hashset conversion when merhant access + // TODO: Remove this hashset conversion when merchant access // and organization access groups are removed .collect::>() .into_iter() @@ -331,8 +436,8 @@ pub async fn update_role( pub async fn list_roles_with_info( state: SessionState, user_from_token: UserFromToken, - request: role_api::ListRolesRequest, -) -> UserResponse> { + request: role_api::ListRolesQueryParams, +) -> UserResponse { let user_role_info = user_from_token .get_role_info_from_db(&state) .await @@ -397,25 +502,61 @@ pub async fn list_roles_with_info( role_info_vec.extend(custom_roles.into_iter().map(roles::RoleInfo::from)); - let list_role_info_response = role_info_vec - .into_iter() - .filter_map(|role_info| { - let is_lower_entity = user_role_entity >= role_info.get_entity_type(); - let request_filter = request.entity_type.map_or(true, |entity_type| { - entity_type == role_info.get_entity_type() - }); + if request.groups == Some(true) { + let list_role_info_response = role_info_vec + .into_iter() + .filter_map(|role_info| { + let is_lower_entity = user_role_entity >= role_info.get_entity_type(); + let request_filter = request.entity_type.map_or(true, |entity_type| { + entity_type == role_info.get_entity_type() + }); - (is_lower_entity && request_filter).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(), + (is_lower_entity && request_filter).then_some({ + let permission_groups = role_info.get_permission_groups(); + let parent_groups = utils::user_role::permission_groups_to_parent_group_info( + &permission_groups, + role_info.get_entity_type(), + ); + + role_api::RoleInfoResponseWithParentsGroup { + role_id: role_info.get_role_id().to_string(), + role_name: role_info.get_role_name().to_string(), + entity_type: role_info.get_entity_type(), + parent_groups, + role_scope: role_info.get_scope(), + } + }) }) - }) - .collect::>(); + .collect::>(); - Ok(ApplicationResponse::Json(list_role_info_response)) + Ok(ApplicationResponse::Json( + role_api::ListRolesResponse::WithParentGroups(list_role_info_response), + )) + } + // TODO: To be deprecated + else { + let list_role_info_response = role_info_vec + .into_iter() + .filter_map(|role_info| { + let is_lower_entity = user_role_entity >= role_info.get_entity_type(); + let request_filter = request.entity_type.map_or(true, |entity_type| { + entity_type == role_info.get_entity_type() + }); + + (is_lower_entity && request_filter).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(), + }) + }) + .collect::>(); + + Ok(ApplicationResponse::Json( + role_api::ListRolesResponse::WithGroups(list_role_info_response), + )) + } } pub async fn list_roles_at_entity_level( diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index b5425e55ad..690a433735 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -2722,45 +2722,51 @@ impl User { } // Role information - route = - route.service( - web::scope("/role") - .service( - web::resource("") - .route(web::get().to(user_role::get_role_from_token)) - .route(web::post().to(user_role::create_role)), - ) - .service(web::resource("/v2").route( - web::get().to(user_role::get_groups_and_resources_for_role_from_token), - )) - // TODO: To be deprecated - .service( - web::resource("/v2/list") - .route(web::get().to(user_role::list_roles_with_info)), - ) - .service( - web::scope("/list") - .service( - web::resource("") - .route(web::get().to(user_role::list_roles_with_info)), - ) - .service(web::resource("/invite").route( + route = route.service( + web::scope("/role") + .service( + web::resource("") + .route(web::get().to(user_role::get_role_from_token)) + // TODO: To be deprecated + .route(web::post().to(user_role::create_role)), + ) + .service( + web::resource("/v2") + .route(web::post().to(user_role::create_role_v2)) + .route( + web::get().to(user_role::get_groups_and_resources_for_role_from_token), + ), + ) + // TODO: To be deprecated + .service( + web::resource("/v2/list").route(web::get().to(user_role::list_roles_with_info)), + ) + .service( + web::scope("/list") + .service( + web::resource("").route(web::get().to(user_role::list_roles_with_info)), + ) + .service( + web::resource("/invite").route( web::get().to(user_role::list_invitable_roles_at_entity_level), - )) - .service(web::resource("/update").route( + ), + ) + .service( + web::resource("/update").route( web::get().to(user_role::list_updatable_roles_at_entity_level), - )), - ) - .service( - web::resource("/{role_id}") - .route(web::get().to(user_role::get_role)) - .route(web::put().to(user_role::update_role)), - ) - .service( - web::resource("/{role_id}/v2") - .route(web::get().to(user_role::get_parent_info_for_role)), - ), - ); + ), + ), + ) + .service( + web::resource("/{role_id}") + .route(web::get().to(user_role::get_role)) + .route(web::put().to(user_role::update_role)), + ) + .service( + web::resource("/{role_id}/v2") + .route(web::get().to(user_role::get_parent_info_for_role)), + ), + ); #[cfg(feature = "dummy_connector")] { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index dcc5cf0752..27b66e5bb5 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -331,6 +331,7 @@ impl From for ApiIdentifier { | Flow::AcceptInvitationsPreAuth | Flow::DeleteUserRole | Flow::CreateRole + | Flow::CreateRoleV2 | Flow::UpdateRole | Flow::UserFromEmail | Flow::ListUsersInLineage => Self::UserRole, diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index 9910cef395..91cc92cb3e 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -74,7 +74,7 @@ pub async fn get_groups_and_resources_for_role_from_token( )) .await } - +// TODO: To be deprecated pub async fn create_role( state: web::Data, req: HttpRequest, @@ -95,6 +95,26 @@ pub async fn create_role( .await } +pub async fn create_role_v2( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::CreateRoleV2; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + role_core::create_role_v2, + &auth::JWTAuth { + permission: Permission::MerchantUserWrite, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn get_role( state: web::Data, req: HttpRequest, @@ -274,6 +294,7 @@ pub async fn get_role_information( pub async fn get_parent_group_info( state: web::Data, http_req: HttpRequest, + query: web::Query, ) -> HttpResponse { let flow = Flow::GetParentGroupInfo; @@ -281,9 +302,9 @@ pub async fn get_parent_group_info( flow, state.clone(), &http_req, - (), - |state, user_from_token, _, _| async move { - user_role_core::get_parent_group_info(state, user_from_token).await + query.into_inner(), + |state, user_from_token, request, _| async move { + user_role_core::get_parent_group_info(state, user_from_token, request).await }, &auth::JWTAuth { permission: Permission::ProfileUserRead, @@ -317,7 +338,7 @@ pub async fn list_users_in_lineage( pub async fn list_roles_with_info( state: web::Data, req: HttpRequest, - query: web::Query, + query: web::Query, ) -> HttpResponse { let flow = Flow::ListRolesV2; diff --git a/crates/router/src/services/authorization/permission_groups.rs b/crates/router/src/services/authorization/permission_groups.rs index c056bdfc22..774368b18d 100644 --- a/crates/router/src/services/authorization/permission_groups.rs +++ b/crates/router/src/services/authorization/permission_groups.rs @@ -119,6 +119,7 @@ pub trait ParentGroupExt { entity_type: EntityType, groups: Vec, ) -> Option>; + fn get_available_scopes(&self) -> Vec; } impl ParentGroupExt for ParentGroup { @@ -143,24 +144,26 @@ impl ParentGroupExt for ParentGroup { ) -> Option> { let descriptions_map = Self::iter() .filter_map(|parent| { - let scopes = groups - .iter() - .filter(|group| group.parent() == parent) - .map(|group| group.scope()) - .max()?; - - let resources = parent + if !groups.iter().any(|group| group.parent() == parent) { + return None; + } + let filtered_resources: Vec<_> = parent .resources() - .iter() + .into_iter() .filter(|res| res.entities().iter().any(|entity| entity <= &entity_type)) + .collect(); + + if filtered_resources.is_empty() { + return None; + } + + let description = filtered_resources + .iter() .map(|res| permissions::get_resource_name(*res, entity_type)) .collect::>>()? .join(", "); - Some(( - parent, - format!("{} {}", permissions::get_scope_name(scopes), resources), - )) + Some((parent, description)) }) .collect::>(); @@ -169,6 +172,13 @@ impl ParentGroupExt for ParentGroup { .not() .then_some(descriptions_map) } + + fn get_available_scopes(&self) -> Vec { + PermissionGroup::iter() + .filter(|group| group.parent() == *self) + .map(|group| group.scope()) + .collect() + } } pub static OPERATIONS: [Resource; 8] = [ diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index c1d9dc10b0..67ed2127ea 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -1,6 +1,10 @@ -use std::{cmp, collections::HashSet}; +use std::{ + cmp, + collections::{HashMap, HashSet}, +}; -use common_enums::{EntityType, PermissionGroup}; +use api_models::user_role::role as role_api; +use common_enums::{EntityType, ParentGroup, PermissionGroup}; use common_utils::id_type; use diesel_models::{ enums::{UserRoleVersion, UserStatus}, @@ -10,6 +14,7 @@ use diesel_models::{ use error_stack::{report, Report, ResultExt}; use router_env::logger; use storage_impl::errors::StorageError; +use strum::IntoEnumIterator; use crate::{ consts, @@ -19,7 +24,11 @@ use crate::{ user_role::{ListUserRolesByOrgIdPayload, ListUserRolesByUserIdPayload}, }, routes::SessionState, - services::authorization::{self as authz, roles}, + services::authorization::{ + self as authz, + permission_groups::{ParentGroupExt, PermissionGroupExt}, + roles, + }, types::domain, }; @@ -508,3 +517,68 @@ pub fn get_min_entity( Ok(cmp::min(user_entity, filter_entity)) } + +pub fn parent_group_info_request_to_permission_groups( + parent_groups: &[role_api::ParentGroupInfoRequest], +) -> Result, UserErrors> { + parent_groups + .iter() + .try_fold(Vec::new(), |mut permission_groups, parent_group| { + let scopes = &parent_group.scopes; + + if scopes.is_empty() { + return Err(UserErrors::InvalidRoleOperation); + } + + let available_scopes = parent_group.name.get_available_scopes(); + + if !scopes.iter().all(|scope| available_scopes.contains(scope)) { + return Err(UserErrors::InvalidRoleOperation); + } + + let groups = PermissionGroup::iter() + .filter(|group| { + group.parent() == parent_group.name && scopes.contains(&group.scope()) + }) + .collect::>(); + permission_groups.extend(groups); + + Ok(permission_groups) + }) +} + +pub fn permission_groups_to_parent_group_info( + permission_groups: &[PermissionGroup], + entity_type: EntityType, +) -> Vec { + let parent_groups_map: HashMap> = + permission_groups + .iter() + .fold(HashMap::new(), |mut acc, group| { + let parent = group.parent(); + let scope = group.scope(); + acc.entry(parent).or_default().push(scope); + acc + }); + + parent_groups_map + .into_iter() + .filter_map(|(name, scopes)| { + let unique_scopes = scopes + .into_iter() + .collect::>() + .into_iter() + .collect(); + + let description = + ParentGroup::get_descriptions_for_groups(entity_type, permission_groups.to_vec()) + .and_then(|descriptions| descriptions.get(&name).cloned())?; + + Some(role_api::ParentGroupInfo { + name, + description, + scopes: unique_scopes, + }) + }) + .collect() +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 722d9bde9a..b758d98f49 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -473,6 +473,8 @@ pub enum Flow { PaymentsAuthorize, /// Create Role CreateRole, + /// Create Role V2 + CreateRoleV2, /// Update Role UpdateRole, /// User email flow start