diff --git a/api/migrations/2025-01-20-221752_add_dataset_gropus_to_permission_groups_and_users/down.sql b/api/migrations/2025-01-20-221752_add_dataset_gropus_to_permission_groups_and_users/down.sql new file mode 100644 index 000000000..b21bff40b --- /dev/null +++ b/api/migrations/2025-01-20-221752_add_dataset_gropus_to_permission_groups_and_users/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` +DROP TRIGGER IF EXISTS update_dataset_groups_permissions_updated_at ON dataset_groups_permissions; +DROP INDEX IF EXISTS dataset_groups_permissions_organization_id_idx; +DROP INDEX IF EXISTS dataset_groups_permissions_permission_id_idx; +DROP INDEX IF EXISTS dataset_groups_permissions_dataset_group_id_idx; +DROP TABLE IF EXISTS dataset_groups_permissions; diff --git a/api/migrations/2025-01-20-221752_add_dataset_gropus_to_permission_groups_and_users/up.sql b/api/migrations/2025-01-20-221752_add_dataset_gropus_to_permission_groups_and_users/up.sql new file mode 100644 index 000000000..d2d538ea8 --- /dev/null +++ b/api/migrations/2025-01-20-221752_add_dataset_gropus_to_permission_groups_and_users/up.sql @@ -0,0 +1,15 @@ +-- Your SQL goes here +CREATE TABLE dataset_groups_permissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + dataset_group_id UUID NOT NULL REFERENCES dataset_groups(id), + permission_id UUID NOT NULL, + permission_type VARCHAR NOT NULL, + organization_id UUID NOT NULL REFERENCES organizations(id), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX dataset_groups_permissions_dataset_group_id_idx ON dataset_groups_permissions(dataset_group_id); +CREATE INDEX dataset_groups_permissions_permission_id_idx ON dataset_groups_permissions(permission_id); +CREATE INDEX dataset_groups_permissions_organization_id_idx ON dataset_groups_permissions(organization_id); \ No newline at end of file diff --git a/api/src/database/models.rs b/api/src/database/models.rs index 109e5b25f..dac2767e8 100644 --- a/api/src/database/models.rs +++ b/api/src/database/models.rs @@ -6,6 +6,13 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use uuid::Uuid; +allow_columns_to_appear_in_same_group_by_clause!( + dataset_groups::id, + dataset_groups::name, + dataset_permissions::id, + dataset_groups_permissions::id, +); + #[derive(Queryable, Insertable, Identifiable, Associations, Debug)] #[diesel(belongs_to(User, foreign_key = owner_id))] #[diesel(table_name = api_keys)] @@ -517,3 +524,15 @@ pub struct DatasetPermission { pub updated_at: DateTime, pub deleted_at: Option>, } + +#[derive(Queryable, Insertable, Debug)] +#[diesel(table_name = dataset_groups_permissions)] +pub struct DatasetGroupPermission { + pub id: Uuid, + pub dataset_group_id: Uuid, + pub permission_id: Uuid, + pub permission_type: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, +} diff --git a/api/src/database/schema.rs b/api/src/database/schema.rs index 1f9314a15..8dac857e8 100644 --- a/api/src/database/schema.rs +++ b/api/src/database/schema.rs @@ -201,6 +201,19 @@ diesel::table! { } } +diesel::table! { + dataset_groups_permissions (id) { + id -> Uuid, + dataset_group_id -> Uuid, + permission_id -> Uuid, + permission_type -> Varchar, + organization_id -> Uuid, + created_at -> Timestamptz, + updated_at -> Timestamptz, + deleted_at -> Nullable, + } +} + diesel::table! { dataset_permissions (id) { id -> Uuid, @@ -506,6 +519,8 @@ diesel::joinable!(dashboard_versions -> dashboards (dashboard_id)); diesel::joinable!(dashboards -> organizations (organization_id)); diesel::joinable!(data_sources -> organizations (organization_id)); diesel::joinable!(dataset_groups -> organizations (organization_id)); +diesel::joinable!(dataset_groups_permissions -> dataset_groups (dataset_group_id)); +diesel::joinable!(dataset_groups_permissions -> organizations (organization_id)); diesel::joinable!(dataset_permissions -> datasets (dataset_id)); diesel::joinable!(dataset_permissions -> organizations (organization_id)); diesel::joinable!(datasets -> data_sources (data_source_id)); @@ -544,6 +559,7 @@ diesel::allow_tables_to_appear_in_same_query!( data_sources, dataset_columns, dataset_groups, + dataset_groups_permissions, dataset_permissions, datasets, datasets_to_dataset_groups, diff --git a/api/src/routes/rest/routes/users/assets/list_attributes.rs b/api/src/routes/rest/routes/users/assets/list_attributes.rs index 92508692e..476a2321d 100644 --- a/api/src/routes/rest/routes/users/assets/list_attributes.rs +++ b/api/src/routes/rest/routes/users/assets/list_attributes.rs @@ -56,7 +56,7 @@ async fn list_attributes_handler(user: User, user_id: Uuid) -> Result return Err(anyhow::anyhow!("User organization id not found")), }; - let auth_user_role = match user.attributes.get("role") { + let auth_user_role = match user.attributes.get("organization_role") { Some(Value::String(role)) => role, Some(_) => return Err(anyhow::anyhow!("User role not found")), None => return Err(anyhow::anyhow!("User role not found")), @@ -86,10 +86,17 @@ async fn list_attributes_handler(user: User, user_id: Uuid) -> Result, - pub updated_at: DateTime, + pub permission_count: i64, + pub assigned: bool, } pub async fn list_dataset_groups( Extension(user): Extension, + Path(id): Path, ) -> Result>, (StatusCode, &'static str)> { - let dataset_groups = match list_dataset_groups_handler(user).await { + let dataset_groups = match list_dataset_groups_handler(user, id).await { Ok(groups) => groups, Err(e) => { tracing::error!("Error listing dataset groups: {:?}", e); @@ -39,25 +39,51 @@ pub async fn list_dataset_groups( Ok(ApiResponse::JsonData(dataset_groups)) } -async fn list_dataset_groups_handler(user: User) -> Result> { +async fn list_dataset_groups_handler(user: User, id: Uuid) -> Result> { let mut conn = get_pg_pool().get().await?; let organization_id = get_user_organization_id(&user.id).await?; - let groups: Vec = dataset_groups::table + let groups = dataset_groups::table + .left_join( + dataset_groups_permissions::table.on(dataset_groups_permissions::dataset_group_id + .eq(dataset_groups::id) + .and(dataset_groups_permissions::permission_type.eq("user")) + .and(dataset_groups_permissions::permission_id.eq(id)) + .and(dataset_groups_permissions::deleted_at.is_null())), + ) + .left_join( + dataset_permissions::table.on(dataset_permissions::permission_id + .eq(dataset_groups::id) + .and(dataset_permissions::permission_type.eq("dataset_group")) + .and(dataset_permissions::deleted_at.is_null()) + .and(dataset_permissions::organization_id.eq(organization_id))), + ) + .select(( + dataset_groups::id, + dataset_groups::name, + diesel::dsl::sql::( + "COALESCE(count(dataset_permissions.id), 0)", + ), + diesel::dsl::sql::("dataset_groups_permissions.id IS NOT NULL"), + )) + .group_by(( + dataset_groups::id, + dataset_groups::name, + dataset_groups_permissions::id, + )) .filter(dataset_groups::organization_id.eq(organization_id)) .filter(dataset_groups::deleted_at.is_null()) .order_by(dataset_groups::created_at.desc()) - .load(&mut *conn) + .load::<(Uuid, String, i64, bool)>(&mut *conn) .await?; Ok(groups .into_iter() - .map(|group| DatasetGroupInfo { - id: group.id, - name: group.name, - organization_id: group.organization_id, - created_at: group.created_at, - updated_at: group.updated_at, + .map(|(id, name, permission_count, assigned)| DatasetGroupInfo { + id, + name, + permission_count, + assigned, }) .collect()) } diff --git a/api/src/routes/rest/routes/users/assets/list_datasets.rs b/api/src/routes/rest/routes/users/assets/list_datasets.rs index 35d0a2c12..e6a3136e5 100644 --- a/api/src/routes/rest/routes/users/assets/list_datasets.rs +++ b/api/src/routes/rest/routes/users/assets/list_datasets.rs @@ -1,15 +1,15 @@ use anyhow::Result; +use axum::extract::Path; use axum::http::StatusCode; use axum::Extension; -use chrono::{DateTime, Utc}; use diesel::prelude::*; use diesel_async::RunQueryDsl; use serde::Serialize; use uuid::Uuid; use crate::database::lib::get_pg_pool; -use crate::database::models::{Dataset, User}; -use crate::database::schema::datasets; +use crate::database::models::User; +use crate::database::schema::{dataset_permissions, datasets}; use crate::routes::rest::ApiResponse; use crate::utils::user::user_info::get_user_organization_id; @@ -17,18 +17,14 @@ use crate::utils::user::user_info::get_user_organization_id; pub struct DatasetInfo { pub id: Uuid, pub name: String, - pub organization_id: Uuid, - pub data_source_id: Uuid, - pub enabled: bool, - pub imported: bool, - pub created_at: DateTime, - pub updated_at: DateTime, + pub assigned: bool, } pub async fn list_datasets( Extension(user): Extension, + Path(id): Path, ) -> Result>, (StatusCode, &'static str)> { - let datasets = match list_datasets_handler(user).await { + let datasets = match list_datasets_handler(user, id).await { Ok(datasets) => datasets, Err(e) => { tracing::error!("Error listing datasets: {:?}", e); @@ -39,28 +35,37 @@ pub async fn list_datasets( Ok(ApiResponse::JsonData(datasets)) } -async fn list_datasets_handler(user: User) -> Result> { +async fn list_datasets_handler(user: User, user_id: Uuid) -> Result> { let mut conn = get_pg_pool().get().await?; let organization_id = get_user_organization_id(&user.id).await?; - let datasets: Vec = datasets::table + let datasets = match datasets::table + .left_join( + dataset_permissions::table.on(dataset_permissions::dataset_id + .eq(datasets::id) + .and(dataset_permissions::permission_type.eq("user")) + .and(dataset_permissions::permission_id.eq(user_id)) + .and(dataset_permissions::deleted_at.is_null())), + ) .filter(datasets::organization_id.eq(organization_id)) .filter(datasets::deleted_at.is_null()) - .order_by(datasets::created_at.desc()) - .load(&mut *conn) - .await?; + .select(( + datasets::id, + datasets::name, + diesel::dsl::sql::("dataset_permissions.id IS NOT NULL"), + )) + .load::<(Uuid, String, bool)>(&mut *conn) + .await + { + Ok(datasets) => datasets, + Err(e) => { + tracing::error!("Error listing datasets: {:?}", e); + return Err(anyhow::anyhow!("Error listing datasets")); + } + }; Ok(datasets .into_iter() - .map(|dataset| DatasetInfo { - id: dataset.id, - name: dataset.name, - organization_id: dataset.organization_id, - data_source_id: dataset.data_source_id, - enabled: dataset.enabled, - imported: dataset.imported, - created_at: dataset.created_at, - updated_at: dataset.updated_at, - }) + .map(|(id, name, assigned)| DatasetInfo { id, name, assigned }) .collect()) } diff --git a/api/src/utils/security/checks.rs b/api/src/utils/security/checks.rs index ea17ddea3..9fe639de0 100644 --- a/api/src/utils/security/checks.rs +++ b/api/src/utils/security/checks.rs @@ -25,7 +25,7 @@ pub async fn is_user_workspace_admin_or_data_admin( None => return Err(anyhow::anyhow!("User organization id not found")), }; - let user_role = match user.attributes.get("role") { + let user_role = match user.attributes.get("organization_role") { Some(Value::String(role)) => role, Some(_) => return Err(anyhow::anyhow!("User role not found")), None => return Err(anyhow::anyhow!("User role not found")),