feat(user): setup roles table with queries (#3691)

This commit is contained in:
Apoorv Dixit
2024-02-19 17:45:42 +05:30
committed by GitHub
parent df739a302b
commit e0d8bb207e
14 changed files with 543 additions and 2 deletions

View File

@ -17,7 +17,8 @@ pub mod diesel_exports {
DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus,
DbRefundStatus as RefundStatus, DbRefundType as RefundType, DbRefundStatus as RefundStatus, DbRefundType as RefundType,
DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization,
DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbUserStatus as UserStatus, DbRoleScope as RoleScope, DbRoutingAlgorithmKind as RoutingAlgorithmKind,
DbUserStatus as UserStatus,
}; };
} }
pub use common_enums::*; pub use common_enums::*;
@ -500,3 +501,53 @@ pub enum DashboardMetadata {
IsMultipleConfiguration, IsMultipleConfiguration,
IsChangePasswordRequired, IsChangePasswordRequired,
} }
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumString,
frunk::LabelledGeneric,
)]
#[diesel_enum(storage_type = "db_enum")]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum RoleScope {
Merchant,
Organization,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
serde::Serialize,
serde::Deserialize,
strum::Display,
strum::EnumString,
frunk::LabelledGeneric,
)]
#[diesel_enum(storage_type = "text")]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum PermissionGroup {
OperationsView,
OperationsManage,
ConnectorsView,
ConnectorsManage,
WorkflowsView,
WorkflowsManage,
AnalyticsView,
UsersView,
UsersManage,
MerchantDetailsView,
MerchantDetailsManage,
OrganizationManage,
}

View File

@ -39,6 +39,7 @@ pub mod process_tracker;
pub mod query; pub mod query;
pub mod refund; pub mod refund;
pub mod reverse_lookup; pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm; pub mod routing_algorithm;
#[allow(unused_qualifications)] #[allow(unused_qualifications)]
pub mod schema; pub mod schema;

View File

@ -32,6 +32,7 @@ pub mod payouts;
pub mod process_tracker; pub mod process_tracker;
pub mod refund; pub mod refund;
pub mod reverse_lookup; pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm; pub mod routing_algorithm;
pub mod user; pub mod user;
pub mod user_role; pub mod user_role;

View File

@ -0,0 +1,68 @@
use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods};
use router_env::tracing::{self, instrument};
use crate::{
enums::RoleScope, query::generics, role::*, schema::roles::dsl, PgPooledConn, StorageResult,
};
impl RoleNew {
#[instrument(skip(conn))]
pub async fn insert(self, conn: &PgPooledConn) -> StorageResult<Role> {
generics::generic_insert(conn, self).await
}
}
impl Role {
pub async fn find_by_role_id(conn: &PgPooledConn, role_id: &str) -> StorageResult<Self> {
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
conn,
dsl::role_id.eq(role_id.to_owned()),
)
.await
}
pub async fn update_by_role_id(
conn: &PgPooledConn,
role_id: &str,
role_update: RoleUpdate,
) -> StorageResult<Self> {
generics::generic_update_with_unique_predicate_get_result::<
<Self as HasTable>::Table,
_,
_,
_,
>(
conn,
dsl::role_id.eq(role_id.to_owned()),
RoleUpdateInternal::from(role_update),
)
.await
}
pub async fn delete_by_role_id(conn: &PgPooledConn, role_id: &str) -> StorageResult<Self> {
generics::generic_delete_one_with_result::<<Self as HasTable>::Table, _, _>(
conn,
dsl::role_id.eq(role_id.to_owned()),
)
.await
}
pub async fn list_roles(
conn: &PgPooledConn,
merchant_id: &str,
org_id: &str,
) -> StorageResult<Vec<Self>> {
let predicate = dsl::merchant_id.eq(merchant_id.to_owned()).or(dsl::org_id
.eq(org_id.to_owned())
.and(dsl::scope.eq(RoleScope::Organization)));
generics::generic_filter::<<Self as HasTable>::Table, _, _, _>(
conn,
predicate,
None,
None,
Some(dsl::last_modified_at.asc()),
)
.await
}
}

View File

@ -0,0 +1,83 @@
use diesel::{AsChangeset, Identifiable, Insertable, Queryable};
use time::PrimitiveDateTime;
use crate::{enums, schema::roles};
#[derive(Clone, Debug, Identifiable, Queryable)]
#[diesel(table_name = roles)]
pub struct Role {
pub id: i32,
pub role_name: String,
pub role_id: String,
pub merchant_id: String,
pub org_id: String,
#[diesel(deserialize_as = super::DieselArray<enums::PermissionGroup>)]
pub groups: Vec<enums::PermissionGroup>,
pub scope: enums::RoleScope,
pub created_at: PrimitiveDateTime,
pub created_by: String,
pub last_modified_at: PrimitiveDateTime,
pub last_modified_by: String,
}
#[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)]
#[diesel(table_name = roles)]
pub struct RoleNew {
pub role_name: String,
pub role_id: String,
pub merchant_id: String,
pub org_id: String,
#[diesel(deserialize_as = super::DieselArray<enums::PermissionGroup>)]
pub groups: Vec<enums::PermissionGroup>,
pub scope: enums::RoleScope,
pub created_at: PrimitiveDateTime,
pub created_by: String,
pub last_modified_at: PrimitiveDateTime,
pub last_modified_by: String,
}
#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
#[diesel(table_name = roles)]
pub struct RoleUpdateInternal {
groups: Option<Vec<enums::PermissionGroup>>,
role_name: Option<String>,
last_modified_by: String,
last_modified_at: PrimitiveDateTime,
}
pub enum RoleUpdate {
UpdateGroup {
groups: Vec<enums::PermissionGroup>,
last_modified_by: String,
},
UpdateRoleName {
role_name: String,
last_modified_by: String,
},
}
impl From<RoleUpdate> for RoleUpdateInternal {
fn from(value: RoleUpdate) -> Self {
let last_modified_at = common_utils::date_time::now();
match value {
RoleUpdate::UpdateGroup {
groups,
last_modified_by,
} => Self {
groups: Some(groups),
role_name: None,
last_modified_at,
last_modified_by,
},
RoleUpdate::UpdateRoleName {
role_name,
last_modified_by,
} => Self {
groups: None,
role_name: Some(role_name),
last_modified_at,
last_modified_by,
},
}
}
}

View File

@ -994,6 +994,31 @@ diesel::table! {
} }
} }
diesel::table! {
use diesel::sql_types::*;
use crate::enums::diesel_exports::*;
roles (id) {
id -> Int4,
#[max_length = 64]
role_name -> Varchar,
#[max_length = 64]
role_id -> Varchar,
#[max_length = 64]
merchant_id -> Varchar,
#[max_length = 64]
org_id -> Varchar,
groups -> Array<Nullable<Text>>,
scope -> RoleScope,
created_at -> Timestamp,
#[max_length = 64]
created_by -> Varchar,
last_modified_at -> Timestamp,
#[max_length = 64]
last_modified_by -> Varchar,
}
}
diesel::table! { diesel::table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use crate::enums::diesel_exports::*; use crate::enums::diesel_exports::*;
@ -1095,6 +1120,7 @@ diesel::allow_tables_to_appear_in_same_query!(
process_tracker, process_tracker,
refund, refund,
reverse_lookup, reverse_lookup,
roles,
routing_algorithm, routing_algorithm,
user_roles, user_roles,
users, users,

View File

@ -31,6 +31,7 @@ pub mod payout_attempt;
pub mod payouts; pub mod payouts;
pub mod refund; pub mod refund;
pub mod reverse_lookup; pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm; pub mod routing_algorithm;
pub mod user; pub mod user;
pub mod user_role; pub mod user_role;
@ -111,6 +112,7 @@ pub trait StorageInterface:
+ authorization::AuthorizationInterface + authorization::AuthorizationInterface
+ user::sample_data::BatchSampleDataInterface + user::sample_data::BatchSampleDataInterface
+ health_check::HealthCheckDbInterface + health_check::HealthCheckDbInterface
+ role::RoleInterface
+ 'static + 'static
{ {
fn get_scheduler_db(&self) -> Box<dyn scheduler::SchedulerInterface>; fn get_scheduler_db(&self) -> Box<dyn scheduler::SchedulerInterface>;

View File

@ -24,6 +24,7 @@ use time::PrimitiveDateTime;
use super::{ use super::{
dashboard_metadata::DashboardMetadataInterface, dashboard_metadata::DashboardMetadataInterface,
role::RoleInterface,
user::{sample_data::BatchSampleDataInterface, UserInterface}, user::{sample_data::BatchSampleDataInterface, UserInterface},
user_role::UserRoleInterface, user_role::UserRoleInterface,
}; };
@ -2256,3 +2257,45 @@ impl HealthCheckDbInterface for KafkaStore {
self.diesel_store.health_check_db().await self.diesel_store.health_check_db().await
} }
} }
#[async_trait::async_trait]
impl RoleInterface for KafkaStore {
async fn insert_role(
&self,
role: storage::RoleNew,
) -> CustomResult<storage::Role, errors::StorageError> {
self.diesel_store.insert_role(role).await
}
async fn find_role_by_role_id(
&self,
role_id: &str,
) -> CustomResult<storage::Role, errors::StorageError> {
self.diesel_store.find_role_by_role_id(role_id).await
}
async fn update_role_by_role_id(
&self,
role_id: &str,
role_update: storage::RoleUpdate,
) -> CustomResult<storage::Role, errors::StorageError> {
self.diesel_store
.update_role_by_role_id(role_id, role_update)
.await
}
async fn delete_role_by_role_id(
&self,
role_id: &str,
) -> CustomResult<storage::Role, errors::StorageError> {
self.diesel_store.delete_role_by_role_id(role_id).await
}
async fn list_all_roles(
&self,
merchant_id: &str,
org_id: &str,
) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
self.diesel_store.list_all_roles(merchant_id, org_id).await
}
}

View File

@ -0,0 +1,236 @@
use diesel_models::role as storage;
use error_stack::{IntoReport, ResultExt};
use super::MockDb;
use crate::{
connection,
core::errors::{self, CustomResult},
services::Store,
};
#[async_trait::async_trait]
pub trait RoleInterface {
async fn insert_role(
&self,
role: storage::RoleNew,
) -> CustomResult<storage::Role, errors::StorageError>;
async fn find_role_by_role_id(
&self,
role_id: &str,
) -> CustomResult<storage::Role, errors::StorageError>;
async fn update_role_by_role_id(
&self,
role_id: &str,
role_update: storage::RoleUpdate,
) -> CustomResult<storage::Role, errors::StorageError>;
async fn delete_role_by_role_id(
&self,
role_id: &str,
) -> CustomResult<storage::Role, errors::StorageError>;
async fn list_all_roles(
&self,
merchant_id: &str,
org_id: &str,
) -> CustomResult<Vec<storage::Role>, errors::StorageError>;
}
#[async_trait::async_trait]
impl RoleInterface for Store {
async fn insert_role(
&self,
role: storage::RoleNew,
) -> CustomResult<storage::Role, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
role.insert(&conn).await.map_err(Into::into).into_report()
}
async fn find_role_by_role_id(
&self,
role_id: &str,
) -> CustomResult<storage::Role, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::Role::find_by_role_id(&conn, role_id)
.await
.map_err(Into::into)
.into_report()
}
async fn update_role_by_role_id(
&self,
role_id: &str,
role_update: storage::RoleUpdate,
) -> CustomResult<storage::Role, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::Role::update_by_role_id(&conn, role_id, role_update)
.await
.map_err(Into::into)
.into_report()
}
async fn delete_role_by_role_id(
&self,
role_id: &str,
) -> CustomResult<storage::Role, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::Role::delete_by_role_id(&conn, role_id)
.await
.map_err(Into::into)
.into_report()
}
async fn list_all_roles(
&self,
merchant_id: &str,
org_id: &str,
) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::Role::list_roles(&conn, merchant_id, org_id)
.await
.map_err(Into::into)
.into_report()
}
}
#[async_trait::async_trait]
impl RoleInterface for MockDb {
async fn insert_role(
&self,
role: storage::RoleNew,
) -> CustomResult<storage::Role, errors::StorageError> {
let mut roles = self.roles.lock().await;
if roles
.iter()
.any(|role_inner| role_inner.role_id == role.role_id)
{
Err(errors::StorageError::DuplicateValue {
entity: "role_id",
key: None,
})?
}
let role = storage::Role {
id: roles
.len()
.try_into()
.into_report()
.change_context(errors::StorageError::MockDbError)?,
role_name: role.role_name,
role_id: role.role_id,
merchant_id: role.merchant_id,
org_id: role.org_id,
groups: role.groups,
scope: role.scope,
created_by: role.created_by,
created_at: role.created_at,
last_modified_at: role.last_modified_at,
last_modified_by: role.last_modified_by,
};
roles.push(role.clone());
Ok(role)
}
async fn find_role_by_role_id(
&self,
role_id: &str,
) -> CustomResult<storage::Role, errors::StorageError> {
let roles = self.roles.lock().await;
roles
.iter()
.find(|role| role.role_id == role_id)
.cloned()
.ok_or(
errors::StorageError::ValueNotFound(format!(
"No role available role_id = {role_id}"
))
.into(),
)
}
async fn update_role_by_role_id(
&self,
role_id: &str,
role_update: storage::RoleUpdate,
) -> CustomResult<storage::Role, errors::StorageError> {
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 {
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,
..role.to_owned()
},
};
role.to_owned()
})
.ok_or(
errors::StorageError::ValueNotFound(format!(
"No role available for role_id = {role_id}"
))
.into(),
)
}
async fn delete_role_by_role_id(
&self,
role_id: &str,
) -> CustomResult<storage::Role, errors::StorageError> {
let mut roles = self.roles.lock().await;
let role_index = roles
.iter()
.position(|role| role.role_id == role_id)
.ok_or(errors::StorageError::ValueNotFound(format!(
"No role available for role_id = {role_id}"
)))?;
Ok(roles.remove(role_index))
}
async fn list_all_roles(
&self,
merchant_id: &str,
org_id: &str,
) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
let roles = self.roles.lock().await;
let roles_list: Vec<_> = roles
.iter()
.filter(|role| {
role.merchant_id == merchant_id
|| (role.org_id == org_id
&& role.scope == diesel_models::enums::RoleScope::Organization)
})
.cloned()
.collect();
if roles_list.is_empty() {
return Err(errors::StorageError::ValueNotFound(format!(
"No role found for merchant id = {} and org_id = {}",
merchant_id, org_id
))
.into());
}
Ok(roles_list)
}
}

View File

@ -31,6 +31,7 @@ pub mod payout_attempt;
pub mod payouts; pub mod payouts;
pub mod refund; pub mod refund;
pub mod reverse_lookup; pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm; pub mod routing_algorithm;
pub mod user; pub mod user;
pub mod user_role; pub mod user_role;
@ -51,7 +52,8 @@ pub use self::{
dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*, dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*,
gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*,
merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*,
process_tracker::*, refund::*, reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, process_tracker::*, refund::*, reverse_lookup::*, role::*, routing_algorithm::*, user::*,
user_role::*,
}; };
use crate::types::api::routing; use crate::types::api::routing;

View File

@ -0,0 +1 @@
pub use diesel_models::role::*;

View File

@ -45,6 +45,7 @@ pub struct MockDb {
pub user_roles: Arc<Mutex<Vec<store::user_role::UserRole>>>, pub user_roles: Arc<Mutex<Vec<store::user_role::UserRole>>>,
pub authorizations: Arc<Mutex<Vec<store::authorization::Authorization>>>, pub authorizations: Arc<Mutex<Vec<store::authorization::Authorization>>>,
pub dashboard_metadata: Arc<Mutex<Vec<store::user::dashboard_metadata::DashboardMetadata>>>, pub dashboard_metadata: Arc<Mutex<Vec<store::user::dashboard_metadata::DashboardMetadata>>>,
pub roles: Arc<Mutex<Vec<store::role::Role>>>,
} }
impl MockDb { impl MockDb {
@ -82,6 +83,7 @@ impl MockDb {
user_roles: Default::default(), user_roles: Default::default(),
authorizations: Default::default(), authorizations: Default::default(),
dashboard_metadata: Default::default(), dashboard_metadata: Default::default(),
roles: Default::default(),
}) })
} }
} }

View File

@ -0,0 +1,6 @@
-- This file should undo anything in `up.sql`
DROP INDEX IF EXISTS role_id_index;
DROP INDEX IF EXISTS roles_merchant_org_index;
DROP TABLE IF EXISTS roles;
DROP TYPE "RoleScope";

View File

@ -0,0 +1,19 @@
-- Your SQL goes here
CREATE TYPE "RoleScope" AS ENUM ('merchant','organization');
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
role_name VARCHAR(64) NOT NULL,
role_id VARCHAR(64) NOT NULL UNIQUE,
merchant_id VARCHAR(64) NOT NULL,
org_id VARCHAR(64) NOT NULL,
groups TEXT[] NOT NULL,
scope "RoleScope" NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now(),
created_by VARCHAR(64) NOT NULL,
last_modified_at TIMESTAMP NOT NULL DEFAULT now(),
last_modified_by VARCHAR(64) NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS role_id_index ON roles (role_id);
CREATE INDEX roles_merchant_org_index ON roles (merchant_id, org_id);