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,
DbRefundStatus as RefundStatus, DbRefundType as RefundType,
DbRequestIncrementalAuthorization as RequestIncrementalAuthorization,
DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbUserStatus as UserStatus,
DbRoleScope as RoleScope, DbRoutingAlgorithmKind as RoutingAlgorithmKind,
DbUserStatus as UserStatus,
};
}
pub use common_enums::*;
@ -500,3 +501,53 @@ pub enum DashboardMetadata {
IsMultipleConfiguration,
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 refund;
pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm;
#[allow(unused_qualifications)]
pub mod schema;

View File

@ -32,6 +32,7 @@ pub mod payouts;
pub mod process_tracker;
pub mod refund;
pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm;
pub mod user;
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! {
use diesel::sql_types::*;
use crate::enums::diesel_exports::*;
@ -1095,6 +1120,7 @@ diesel::allow_tables_to_appear_in_same_query!(
process_tracker,
refund,
reverse_lookup,
roles,
routing_algorithm,
user_roles,
users,

View File

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

View File

@ -24,6 +24,7 @@ use time::PrimitiveDateTime;
use super::{
dashboard_metadata::DashboardMetadataInterface,
role::RoleInterface,
user::{sample_data::BatchSampleDataInterface, UserInterface},
user_role::UserRoleInterface,
};
@ -2256,3 +2257,45 @@ impl HealthCheckDbInterface for KafkaStore {
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 refund;
pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm;
pub mod user;
pub mod user_role;
@ -51,7 +52,8 @@ pub use self::{
dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*,
gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*,
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;

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 authorizations: Arc<Mutex<Vec<store::authorization::Authorization>>>,
pub dashboard_metadata: Arc<Mutex<Vec<store::user::dashboard_metadata::DashboardMetadata>>>,
pub roles: Arc<Mutex<Vec<store::role::Role>>>,
}
impl MockDb {
@ -82,6 +83,7 @@ impl MockDb {
user_roles: Default::default(),
authorizations: 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);