feat(user_role): Insert V2 user_roles (#5607)

This commit is contained in:
Mani Chandra
2024-08-29 20:12:04 +05:30
committed by GitHub
parent 35666f57bf
commit 6c266b5df4
7 changed files with 474 additions and 141 deletions

View File

@ -1,4 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::{
collections::{HashMap, HashSet},
ops::Not,
};
use api_models::{
payments::RedirectionResponse,
@ -13,7 +16,6 @@ use diesel_models::{
organization::OrganizationBridge,
user as storage_user,
user_authentication_method::{UserAuthenticationMethodNew, UserAuthenticationMethodUpdate},
user_role::UserRoleNew,
};
use error_stack::{report, ResultExt};
#[cfg(feature = "email")]
@ -60,10 +62,11 @@ pub async fn signup_with_merchant_id(
.await?;
let user_role = new_user
.insert_user_role_in_db(
.insert_org_level_user_role_in_db(
state.clone(),
common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
UserStatus::Active,
None,
)
.await?;
@ -132,10 +135,11 @@ pub async fn signup(
.insert_user_and_merchant_in_db(state.clone())
.await?;
let user_role = new_user
.insert_user_role_in_db(
.insert_org_level_user_role_in_db(
state.clone(),
common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
UserStatus::Active,
None,
)
.await?;
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
@ -163,10 +167,11 @@ pub async fn signup_token_only_flow(
.insert_user_and_merchant_in_db(state.clone())
.await?;
let user_role = new_user
.insert_user_role_in_db(
.insert_org_level_user_role_in_db(
state.clone(),
common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
UserStatus::Active,
None,
)
.await?;
@ -314,10 +319,11 @@ pub async fn connect_account(
.insert_user_and_merchant_in_db(state.clone())
.await?;
let user_role = new_user
.insert_user_role_in_db(
.insert_org_level_user_role_in_db(
state.clone(),
common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
UserStatus::Active,
None,
)
.await?;
@ -773,37 +779,60 @@ async fn handle_existing_user_invitation(
auth_id: &Option<String>,
) -> UserResult<InviteMultipleUserResponse> {
let now = common_utils::date_time::now();
state
if state
.store
.insert_user_role(UserRoleNew {
user_id: invitee_user_from_db.get_user_id().to_owned(),
merchant_id: Some(user_from_token.merchant_id.clone()),
role_id: request.role_id.clone(),
org_id: Some(user_from_token.org_id.clone()),
status: {
if cfg!(feature = "email") {
UserStatus::InvitationSent
} else {
UserStatus::Active
}
},
created_by: user_from_token.user_id.clone(),
last_modified_by: user_from_token.user_id.clone(),
created_at: now,
last_modified: now,
profile_id: None,
entity_id: None,
entity_type: None,
version: UserRoleVersion::V1,
})
.find_user_role_by_user_id_and_lineage(
invitee_user_from_db.get_user_id(),
&user_from_token.org_id,
&user_from_token.merchant_id,
user_from_token.profile_id.as_ref(),
UserRoleVersion::V1,
)
.await
.map_err(|e| {
if e.current_context().is_db_unique_violation() {
e.change_context(UserErrors::UserExists)
.is_err_and(|err| err.current_context().is_db_not_found())
.not()
{
return Err(UserErrors::UserExists.into());
}
if state
.store
.find_user_role_by_user_id_and_lineage(
invitee_user_from_db.get_user_id(),
&user_from_token.org_id,
&user_from_token.merchant_id,
user_from_token.profile_id.as_ref(),
UserRoleVersion::V2,
)
.await
.is_err_and(|err| err.current_context().is_db_not_found())
.not()
{
return Err(UserErrors::UserExists.into());
}
let user_role = domain::NewUserRole {
user_id: invitee_user_from_db.get_user_id().to_owned(),
role_id: request.role_id.clone(),
status: {
if cfg!(feature = "email") {
UserStatus::InvitationSent
} else {
e.change_context(UserErrors::InternalServerError)
UserStatus::Active
}
})?;
},
created_by: user_from_token.user_id.clone(),
last_modified_by: user_from_token.user_id.clone(),
created_at: now,
last_modified: now,
entity: domain::MerchantLevel {
org_id: user_from_token.org_id.clone(),
merchant_id: user_from_token.merchant_id.clone(),
},
}
.insert_in_v1_and_v2(state)
.await?;
let is_email_sent;
#[cfg(feature = "email")]
@ -865,31 +894,22 @@ async fn handle_new_user_invitation(
};
let now = common_utils::date_time::now();
state
.store
.insert_user_role(UserRoleNew {
user_id: new_user.get_user_id().to_owned(),
merchant_id: Some(user_from_token.merchant_id.clone()),
role_id: request.role_id.clone(),
org_id: Some(user_from_token.org_id.clone()),
status: invitation_status,
created_by: user_from_token.user_id.clone(),
last_modified_by: user_from_token.user_id.clone(),
created_at: now,
last_modified: now,
profile_id: None,
entity_id: None,
entity_type: None,
version: UserRoleVersion::V1,
})
.await
.map_err(|e| {
if e.current_context().is_db_unique_violation() {
e.change_context(UserErrors::UserExists)
} else {
e.change_context(UserErrors::InternalServerError)
}
})?;
let user_role = domain::NewUserRole {
user_id: new_user.get_user_id().to_owned(),
role_id: request.role_id.clone(),
status: invitation_status,
created_by: user_from_token.user_id.clone(),
last_modified_by: user_from_token.user_id.clone(),
created_at: now,
last_modified: now,
entity: domain::MerchantLevel {
merchant_id: user_from_token.merchant_id.clone(),
org_id: user_from_token.org_id.clone(),
},
}
.insert_in_v1_and_v2(state)
.await?;
let is_email_sent;
// TODO: Adding this to avoid clippy lints, remove this once the token only flow is being used
@ -1289,7 +1309,7 @@ pub async fn create_internal_user(
}
})?;
let new_user = domain::NewUser::try_from((request, internal_merchant.organization_id))?;
let new_user = domain::NewUser::try_from((request, internal_merchant.organization_id.clone()))?;
let mut store_user: storage_user::UserNew = new_user.clone().try_into()?;
store_user.set_is_verified(true);
@ -1308,12 +1328,16 @@ pub async fn create_internal_user(
.map(domain::user::UserFromStorage::from)?;
new_user
.insert_user_role_in_db(
state,
.get_no_level_user_role(
common_utils::consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(),
UserStatus::Active,
)
.await?;
.add_entity(domain::InternalLevel {
org_id: internal_merchant.organization_id,
})
.insert_in_v1_and_v2(&state)
.await
.change_context(UserErrors::InternalServerError)?;
Ok(ApplicationResponse::StatusOk)
}
@ -1448,10 +1472,11 @@ pub async fn create_merchant_account(
.await?;
let role_insertion_res = new_user
.insert_user_role_in_db(
.insert_org_level_user_role_in_db(
state.clone(),
common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
UserStatus::Active,
Some(UserRoleVersion::V1),
)
.await;
if let Err(e) = role_insertion_res {

View File

@ -35,7 +35,7 @@ use super::{
user::{sample_data::BatchSampleDataInterface, UserInterface},
user_authentication_method::UserAuthenticationMethodInterface,
user_key_store::UserKeyStoreInterface,
user_role::{ListUserRolesByOrgIdPayload, UserRoleInterface},
user_role::{InsertUserRolePayload, ListUserRolesByOrgIdPayload, UserRoleInterface},
};
#[cfg(feature = "payouts")]
use crate::services::kafka::payout::KafkaPayout;
@ -2767,8 +2767,8 @@ impl RedisConnInterface for KafkaStore {
impl UserRoleInterface for KafkaStore {
async fn insert_user_role(
&self,
user_role: user_storage::UserRoleNew,
) -> CustomResult<user_storage::UserRole, errors::StorageError> {
user_role: InsertUserRolePayload,
) -> CustomResult<Vec<user_storage::UserRole>, errors::StorageError> {
self.diesel_store.insert_user_role(user_role).await
}

View File

@ -10,12 +10,35 @@ use crate::{
services::Store,
};
pub enum InsertUserRolePayload {
OnlyV1(storage::UserRoleNew),
OnlyV2(storage::UserRoleNew),
V1AndV2(Box<[storage::UserRoleNew; 2]>),
}
impl InsertUserRolePayload {
fn convert_to_vec(self) -> Vec<storage::UserRoleNew> {
match self {
Self::OnlyV1(user_role) | Self::OnlyV2(user_role) => vec![user_role],
Self::V1AndV2(user_roles) => user_roles.to_vec(),
}
}
}
pub struct ListUserRolesByOrgIdPayload<'a> {
pub user_id: Option<&'a String>,
pub org_id: &'a id_type::OrganizationId,
pub merchant_id: Option<&'a id_type::MerchantId>,
pub profile_id: Option<&'a id_type::ProfileId>,
pub version: Option<enums::UserRoleVersion>,
}
#[async_trait::async_trait]
pub trait UserRoleInterface {
async fn insert_user_role(
&self,
user_role: storage::UserRoleNew,
) -> CustomResult<storage::UserRole, errors::StorageError>;
user_role: InsertUserRolePayload,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError>;
async fn find_user_role_by_user_id(
&self,
@ -86,24 +109,16 @@ pub trait UserRoleInterface {
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError>;
}
pub struct ListUserRolesByOrgIdPayload<'a> {
pub user_id: Option<&'a String>,
pub org_id: &'a id_type::OrganizationId,
pub merchant_id: Option<&'a id_type::MerchantId>,
pub profile_id: Option<&'a id_type::ProfileId>,
pub version: Option<enums::UserRoleVersion>,
}
#[async_trait::async_trait]
impl UserRoleInterface for Store {
#[instrument(skip_all)]
async fn insert_user_role(
&self,
user_role: storage::UserRoleNew,
) -> CustomResult<storage::UserRole, errors::StorageError> {
user_role: InsertUserRolePayload,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
user_role
.insert(&conn)
storage::UserRole::insert_multiple_user_roles(&conn, user_role.convert_to_vec())
.await
.map_err(|error| report!(errors::StorageError::from(error)))
}
@ -138,7 +153,6 @@ impl UserRoleInterface for Store {
.map_err(|error| report!(errors::StorageError::from(error)))
}
#[instrument(skip_all)]
async fn list_user_roles_by_user_id_and_version(
&self,
user_id: &str,
@ -275,37 +289,44 @@ impl UserRoleInterface for Store {
impl UserRoleInterface for MockDb {
async fn insert_user_role(
&self,
user_role: storage::UserRoleNew,
) -> CustomResult<storage::UserRole, errors::StorageError> {
let mut user_roles = self.user_roles.lock().await;
if user_roles
.iter()
.any(|user_role_inner| user_role_inner.user_id == user_role.user_id)
{
Err(errors::StorageError::DuplicateValue {
entity: "user_id",
key: None,
})?
}
let user_role = storage::UserRole {
id: i32::try_from(user_roles.len())
.change_context(errors::StorageError::MockDbError)?,
user_id: user_role.user_id,
merchant_id: user_role.merchant_id,
role_id: user_role.role_id,
status: user_role.status,
created_by: user_role.created_by,
created_at: user_role.created_at,
last_modified: user_role.last_modified,
last_modified_by: user_role.last_modified_by,
org_id: user_role.org_id,
profile_id: None,
entity_id: None,
entity_type: None,
version: enums::UserRoleVersion::V1,
};
user_roles.push(user_role.clone());
Ok(user_role)
user_role: InsertUserRolePayload,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
let mut db_user_roles = self.user_roles.lock().await;
user_role
.convert_to_vec()
.into_iter()
.map(|user_role| {
if db_user_roles
.iter()
.any(|user_role_inner| user_role_inner.user_id == user_role.user_id)
{
Err(errors::StorageError::DuplicateValue {
entity: "user_id",
key: None,
})?
}
let user_role = storage::UserRole {
id: i32::try_from(db_user_roles.len())
.change_context(errors::StorageError::MockDbError)?,
user_id: user_role.user_id,
merchant_id: user_role.merchant_id,
role_id: user_role.role_id,
status: user_role.status,
created_by: user_role.created_by,
created_at: user_role.created_at,
last_modified: user_role.last_modified,
last_modified_by: user_role.last_modified_by,
org_id: user_role.org_id,
profile_id: None,
entity_id: None,
entity_type: None,
version: enums::UserRoleVersion::V1,
};
db_user_roles.push(user_role.clone());
Ok(user_role)
})
.collect::<Result<Vec<_>, _>>()
}
async fn find_user_role_by_user_id(

View File

@ -3,7 +3,7 @@ use std::{collections::HashSet, ops, str::FromStr};
use api_models::{
admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api,
};
use common_enums::TokenPurpose;
use common_enums::{EntityType, TokenPurpose};
use common_utils::{
crypto::Encryptable, errors::CustomResult, id_type, new_type::MerchantName, pii, type_name,
types::keymanager::Identifier,
@ -19,6 +19,7 @@ use masking::{ExposeInterface, PeekInterface, Secret};
use once_cell::sync::Lazy;
use rand::distributions::{Alphanumeric, DistString};
use router_env::env;
use time::PrimitiveDateTime;
use unicode_segmentation::UnicodeSegmentation;
#[cfg(feature = "keymanager_create")]
use {base64::Engine, common_utils::types::keymanager::EncryptionTransferRequest};
@ -29,9 +30,13 @@ use crate::{
admin,
errors::{self, UserErrors, UserResult},
},
db::GlobalStorageInterface,
db::{user_role::InsertUserRolePayload, GlobalStorageInterface},
routes::SessionState,
services::{self, authentication as auth, authentication::UserFromToken, authorization::info},
services::{
self,
authentication::{self as auth, UserFromToken},
authorization::info,
},
types::transformers::ForeignFrom,
utils::{self, user::password},
};
@ -638,38 +643,51 @@ impl NewUser {
created_user
}
pub async fn insert_user_role_in_db(
pub fn get_no_level_user_role(
self,
role_id: String,
user_status: UserStatus,
) -> NewUserRole<NoLevel> {
let now = common_utils::date_time::now();
let user_id = self.get_user_id();
NewUserRole {
status: user_status,
created_by: user_id.clone(),
last_modified_by: user_id.clone(),
user_id,
role_id,
created_at: now,
last_modified: now,
entity: NoLevel,
}
}
pub async fn insert_org_level_user_role_in_db(
self,
state: SessionState,
role_id: String,
user_status: UserStatus,
version: Option<UserRoleVersion>,
) -> UserResult<UserRole> {
let now = common_utils::date_time::now();
let user_id = self.get_user_id();
let org_id = self
.get_new_merchant()
.get_new_organization()
.get_organization_id();
let merchant_id = self.get_new_merchant().get_merchant_id();
state
.store
.insert_user_role(UserRoleNew {
merchant_id: Some(self.get_new_merchant().get_merchant_id()),
status: user_status,
created_by: user_id.clone(),
last_modified_by: user_id.clone(),
user_id,
role_id,
created_at: now,
last_modified: now,
org_id: Some(
self.get_new_merchant()
.get_new_organization()
.get_organization_id(),
),
profile_id: None,
entity_id: None,
entity_type: None,
version: UserRoleVersion::V1,
})
.await
.change_context(UserErrors::InternalServerError)
let org_user_role = self
.get_no_level_user_role(role_id, user_status)
.add_entity(OrganizationLevel {
org_id,
merchant_id,
});
match version {
Some(UserRoleVersion::V1) => org_user_role.insert_in_v1(&state).await,
Some(UserRoleVersion::V2) => org_user_role.insert_in_v2(&state).await,
None => org_user_role.insert_in_v1_and_v2(&state).await,
}
}
}
@ -1282,3 +1300,252 @@ impl RecoveryCodes {
self.0
}
}
// This is for easier construction
#[derive(Clone)]
pub struct NoLevel;
#[derive(Clone)]
pub struct OrganizationLevel {
pub org_id: id_type::OrganizationId,
// Keeping this to allow insertion of org_admins in V1
pub merchant_id: id_type::MerchantId,
}
#[derive(Clone)]
pub struct MerchantLevel {
pub org_id: id_type::OrganizationId,
pub merchant_id: id_type::MerchantId,
}
#[derive(Clone)]
pub struct ProfileLevel {
pub org_id: id_type::OrganizationId,
pub merchant_id: id_type::MerchantId,
pub profile_id: id_type::ProfileId,
}
#[derive(Clone)]
pub struct InternalLevel {
pub org_id: id_type::OrganizationId,
}
#[derive(Clone)]
pub struct NewUserRole<E: Clone> {
pub user_id: String,
pub role_id: String,
pub status: UserStatus,
pub created_by: String,
pub last_modified_by: String,
pub created_at: PrimitiveDateTime,
pub last_modified: PrimitiveDateTime,
pub entity: E,
}
impl NewUserRole<NoLevel> {
pub fn add_entity<T>(self, entity: T) -> NewUserRole<T>
where
T: Clone,
{
NewUserRole {
entity,
user_id: self.user_id,
role_id: self.role_id,
status: self.status,
created_by: self.created_by,
last_modified_by: self.last_modified_by,
created_at: self.created_at,
last_modified: self.last_modified,
}
}
}
pub struct EntityInfo {
org_id: id_type::OrganizationId,
merchant_id: Option<id_type::MerchantId>,
profile_id: Option<id_type::ProfileId>,
entity_id: String,
entity_type: EntityType,
}
impl<E> NewUserRole<E>
where
E: Clone,
{
fn convert_to_new_v1_role(
self,
org_id: id_type::OrganizationId,
merchant_id: id_type::MerchantId,
) -> UserRoleNew {
UserRoleNew {
user_id: self.user_id,
role_id: self.role_id,
status: self.status,
created_by: self.created_by,
last_modified_by: self.last_modified_by,
created_at: self.created_at,
last_modified: self.last_modified,
org_id: Some(org_id),
merchant_id: Some(merchant_id),
profile_id: None,
entity_id: None,
entity_type: None,
version: UserRoleVersion::V1,
}
}
fn convert_to_new_v2_role(self, entity: EntityInfo) -> UserRoleNew {
UserRoleNew {
user_id: self.user_id,
role_id: self.role_id,
status: self.status,
created_by: self.created_by,
last_modified_by: self.last_modified_by,
created_at: self.created_at,
last_modified: self.last_modified,
org_id: Some(entity.org_id),
merchant_id: entity.merchant_id,
profile_id: entity.profile_id,
entity_id: Some(entity.entity_id),
entity_type: Some(entity.entity_type),
version: UserRoleVersion::V2,
}
}
async fn insert_v1_and_v2_in_db_and_get_v1(
state: &SessionState,
v1_role: UserRoleNew,
v2_role: UserRoleNew,
) -> UserResult<UserRole> {
let inserted_roles = state
.store
.insert_user_role(InsertUserRolePayload::V1AndV2(Box::new([v1_role, v2_role])))
.await
.change_context(UserErrors::InternalServerError)?;
// Returning v1 role so other code which was not migrated doesn't break
inserted_roles
.into_iter()
.find(|role| role.version == UserRoleVersion::V1)
.ok_or(report!(UserErrors::InternalServerError))
}
}
impl NewUserRole<OrganizationLevel> {
pub async fn insert_in_v1(self, state: &SessionState) -> UserResult<UserRole> {
let entity = self.entity.clone();
let new_v1_role = self
.clone()
.convert_to_new_v1_role(entity.org_id.clone(), entity.merchant_id.clone());
state
.store
.insert_user_role(InsertUserRolePayload::OnlyV1(new_v1_role))
.await
.change_context(UserErrors::InternalServerError)?
.pop()
.ok_or(report!(UserErrors::InternalServerError))
}
pub async fn insert_in_v2(self, state: &SessionState) -> UserResult<UserRole> {
let entity = self.entity.clone();
let new_v2_role = self.convert_to_new_v2_role(EntityInfo {
org_id: entity.org_id.clone(),
merchant_id: None,
profile_id: None,
entity_id: entity.org_id.get_string_repr().to_owned(),
entity_type: EntityType::Organization,
});
state
.store
.insert_user_role(InsertUserRolePayload::OnlyV2(new_v2_role))
.await
.change_context(UserErrors::InternalServerError)?
.pop()
.ok_or(report!(UserErrors::InternalServerError))
}
pub async fn insert_in_v1_and_v2(self, state: &SessionState) -> UserResult<UserRole> {
let entity = self.entity.clone();
let new_v1_role = self
.clone()
.convert_to_new_v1_role(entity.org_id.clone(), entity.merchant_id.clone());
let new_v2_role = self.clone().convert_to_new_v2_role(EntityInfo {
org_id: entity.org_id.clone(),
merchant_id: None,
profile_id: None,
entity_id: entity.org_id.get_string_repr().to_owned(),
entity_type: EntityType::Organization,
});
Self::insert_v1_and_v2_in_db_and_get_v1(state, new_v1_role, new_v2_role).await
}
}
impl NewUserRole<MerchantLevel> {
pub async fn insert_in_v1_and_v2(self, state: &SessionState) -> UserResult<UserRole> {
let entity = self.entity.clone();
let new_v1_role = self
.clone()
.convert_to_new_v1_role(entity.org_id.clone(), entity.merchant_id.clone());
let new_v2_role = self.clone().convert_to_new_v2_role(EntityInfo {
org_id: entity.org_id.clone(),
merchant_id: Some(entity.merchant_id.clone()),
profile_id: None,
entity_id: entity.merchant_id.get_string_repr().to_owned(),
entity_type: EntityType::Merchant,
});
Self::insert_v1_and_v2_in_db_and_get_v1(state, new_v1_role, new_v2_role).await
}
}
impl NewUserRole<InternalLevel> {
pub async fn insert_in_v1_and_v2(self, state: &SessionState) -> UserResult<UserRole> {
let entity = self.entity.clone();
let internal_merchant_id = id_type::MerchantId::get_internal_user_merchant_id(
consts::user_role::INTERNAL_USER_MERCHANT_ID,
);
let new_v1_role = self
.clone()
.convert_to_new_v1_role(entity.org_id.clone(), internal_merchant_id.clone());
let new_v2_role = self.convert_to_new_v2_role(EntityInfo {
org_id: entity.org_id.clone(),
merchant_id: Some(internal_merchant_id.clone()),
profile_id: None,
entity_id: internal_merchant_id.get_string_repr().to_owned(),
entity_type: EntityType::Internal,
});
Self::insert_v1_and_v2_in_db_and_get_v1(state, new_v1_role, new_v2_role).await
}
}
impl NewUserRole<ProfileLevel> {
pub async fn insert_in_v2(self, state: &SessionState) -> UserResult<UserRole> {
let entity = self.entity.clone();
let new_v2_role = self.convert_to_new_v2_role(EntityInfo {
org_id: entity.org_id.clone(),
merchant_id: Some(entity.merchant_id.clone()),
profile_id: Some(entity.profile_id.clone()),
entity_id: entity.profile_id.get_string_repr().to_owned(),
entity_type: EntityType::Profile,
});
state
.store
.insert_user_role(InsertUserRolePayload::OnlyV2(new_v2_role))
.await
.change_context(UserErrors::InternalServerError)?
.pop()
.ok_or(report!(UserErrors::InternalServerError))
}
}