diff --git a/api/src/database/models.rs b/api/src/database/models.rs index 09f216462..a67464ac3 100644 --- a/api/src/database/models.rs +++ b/api/src/database/models.rs @@ -15,7 +15,23 @@ allow_columns_to_appear_in_same_group_by_clause!( teams::name, permission_groups_to_identities::permission_group_id, teams_to_users::user_id, - teams_to_users::role + teams_to_users::role, + permission_groups::id, + permission_groups::name, + permission_groups_to_identities::identity_id, + permission_groups_to_identities::identity_type, + users::id, + users::name, + users::email, + users_to_organizations::role, + datasets::id, + datasets::name, + datasets::created_at, + datasets::updated_at, + datasets::enabled, + datasets::imported, + data_sources::id, + data_sources::name, ); #[derive(Queryable, Insertable, Identifiable, Associations, Debug)] diff --git a/api/src/routes/rest/routes/users/assets/list_permission_groups.rs b/api/src/routes/rest/routes/users/assets/list_permission_groups.rs index cd3415072..bc3e5b77e 100644 --- a/api/src/routes/rest/routes/users/assets/list_permission_groups.rs +++ b/api/src/routes/rest/routes/users/assets/list_permission_groups.rs @@ -7,9 +7,12 @@ use diesel_async::RunQueryDsl; use serde::Serialize; use uuid::Uuid; +use crate::database::enums::IdentityType; use crate::database::lib::get_pg_pool; -use crate::database::models::{PermissionGroup, User}; -use crate::database::schema::permission_groups; +use crate::database::models::User; +use crate::database::schema::{ + dataset_permissions, permission_groups, permission_groups_to_identities, +}; use crate::routes::rest::ApiResponse; use crate::utils::security::checks::is_user_workspace_admin_or_data_admin; use crate::utils::user::user_info::get_user_organization_id; @@ -18,9 +21,8 @@ use crate::utils::user::user_info::get_user_organization_id; pub struct PermissionGroupInfo { pub id: Uuid, pub name: String, - pub organization_id: Uuid, - pub created_at: DateTime, - pub updated_at: DateTime, + pub dataset_count: i64, + pub assigned: bool, } pub async fn list_permission_groups( @@ -45,24 +47,59 @@ async fn list_permission_groups_handler(user: User) -> Result = permission_groups::table + let groups = permission_groups::table + .left_join( + permission_groups_to_identities::table.on( + permission_groups_to_identities::permission_group_id + .eq(permission_groups::id) + .and(permission_groups_to_identities::deleted_at.is_null()) + .and(permission_groups_to_identities::identity_id.eq(user.id)) + .and(permission_groups_to_identities::identity_type.eq(IdentityType::User)), + ), + ) + .left_join( + dataset_permissions::table.on(dataset_permissions::permission_id + .eq(permission_groups::id) + .and(dataset_permissions::permission_type.eq("permission_group")) + .and(dataset_permissions::deleted_at.is_null()) + .and(dataset_permissions::organization_id.eq(organization_id))), + ) + .select(( + permission_groups::id, + permission_groups::name, + diesel::dsl::sql::( + "COALESCE(count(dataset_permissions.id), 0)", + ), + diesel::dsl::sql::( + "permission_groups_to_identities.identity_id IS NOT NULL", + ), + )) + .group_by(( + permission_groups::id, + permission_groups::name, + dataset_permissions::id, + permission_groups_to_identities::identity_id, + )) .filter(permission_groups::organization_id.eq(organization_id)) .filter(permission_groups::deleted_at.is_null()) .order_by(permission_groups::created_at.desc()) - .load(&mut *conn) + .load::<(Uuid, String, i64, bool)>(&mut *conn) .await?; Ok(groups .into_iter() - .map(|group| PermissionGroupInfo { - id: group.id, - name: group.name, - organization_id: group.organization_id, - created_at: group.created_at, - updated_at: group.updated_at, - }) + .map( + |(id, name, dataset_count, assigned)| PermissionGroupInfo { + id, + name, + dataset_count, + assigned, + }, + ) .collect()) } diff --git a/api/src/routes/rest/routes/users/assets/mod.rs b/api/src/routes/rest/routes/users/assets/mod.rs index cc1759a70..d9e4f0dd8 100644 --- a/api/src/routes/rest/routes/users/assets/mod.rs +++ b/api/src/routes/rest/routes/users/assets/mod.rs @@ -1,10 +1,11 @@ -use axum::{routing::get, Router}; +use axum::{routing::get, routing::put, Router}; pub mod list_attributes; pub mod list_dataset_groups; pub mod list_datasets; pub mod list_permission_groups; pub mod list_teams; +pub mod put_teams; pub fn router() -> Router { Router::new() @@ -19,4 +20,5 @@ pub fn router() -> Router { get(list_permission_groups::list_permission_groups), ) .route("/teams", get(list_teams::list_teams)) + .route("/teams", put(put_teams::put_teams)) } diff --git a/api/src/routes/rest/routes/users/assets/put_teams.rs b/api/src/routes/rest/routes/users/assets/put_teams.rs new file mode 100644 index 000000000..d1e8416be --- /dev/null +++ b/api/src/routes/rest/routes/users/assets/put_teams.rs @@ -0,0 +1,100 @@ +use anyhow::Result; +use axum::extract::Path; +use axum::http::StatusCode; +use axum::{Extension, Json}; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use serde::Serialize; +use tokio::spawn; +use uuid::Uuid; + +use crate::database::lib::get_pg_pool; +use crate::database::models::User; +use crate::database::schema::{teams_to_users}; +use crate::routes::rest::ApiResponse; +use crate::utils::security::checks::is_user_workspace_admin_or_data_admin; +use crate::utils::user::user_info::get_user_organization_id; + +#[derive(Debug, Serialize)] +pub struct TeamAssignment { + pub id: Uuid, + pub assigned: bool, +} + +pub async fn put_teams( + Extension(user): Extension, + Path(id): Path, + Json(assignments): Json>, +) -> Result, (StatusCode, &'static str)> { + match put_teams_handler(user, id, assignments).await { + Ok(_) => (), + Err(e) => { + tracing::error!("Error listing teams: {:?}", e); + return Err((StatusCode::INTERNAL_SERVER_ERROR, "Error listing teams")); + } + }; + + Ok(ApiResponse::NoContent) +} + +async fn put_teams_handler( + user: User, + user_id: Uuid, + assignments: Vec, +) -> Result<()> { + let organization_id = get_user_organization_id(&user_id).await?; + + if !is_user_workspace_admin_or_data_admin(&user, &organization_id).await? { + return Err(anyhow::anyhow!("User is not authorized to list teams")); + }; + + let (to_assign, to_unassign): (Vec<_>, Vec<_>) = + assignments.into_iter().partition(|a| a.assigned); + + let assign_handle = { + let user_id = user_id; + spawn(async move { + if !to_assign.is_empty() { + let mut conn = get_pg_pool().get().await?; + for team in to_assign { + diesel::insert_into(teams_to_users::table) + .values(( + teams_to_users::team_id.eq(team.id), + teams_to_users::user_id.eq(user_id), + )) + .on_conflict((teams_to_users::team_id, teams_to_users::user_id)) + .do_update() + .set(teams_to_users::deleted_at.eq(None::>)) + .execute(&mut *conn) + .await?; + } + } + Ok::<_, anyhow::Error>(()) + }) + }; + + let unassign_handle = { + let user_id = user_id; + spawn(async move { + if !to_unassign.is_empty() { + let mut conn = get_pg_pool().get().await?; + diesel::update(teams_to_users::table) + .filter( + teams_to_users::team_id + .eq_any(to_unassign.iter().map(|a| a.id)) + .and(teams_to_users::user_id.eq(user_id)), + ) + .set(teams_to_users::deleted_at.eq(chrono::Utc::now())) + .execute(&mut *conn) + .await?; + } + Ok::<_, anyhow::Error>(()) + }) + }; + + let (assign_result, unassign_result) = tokio::try_join!(assign_handle, unassign_handle)?; + assign_result?; + unassign_result?; + + Ok(()) +} diff --git a/api/src/routes/ws/datasets/list_datasets.rs b/api/src/routes/ws/datasets/list_datasets.rs index 48e292e26..e807303bc 100644 --- a/api/src/routes/ws/datasets/list_datasets.rs +++ b/api/src/routes/ws/datasets/list_datasets.rs @@ -1,7 +1,6 @@ use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; use diesel::{ - allow_columns_to_appear_in_same_group_by_clause, dsl::sql, sql_types::{Nullable, Timestamptz}, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl, @@ -78,20 +77,6 @@ pub struct ListDatasetObject { pub belongs_to: Option, } -allow_columns_to_appear_in_same_group_by_clause!( - datasets::id, - datasets::name, - datasets::created_at, - datasets::updated_at, - datasets::enabled, - datasets::imported, - users::id, - users::name, - users::email, - data_sources::id, - data_sources::name, -); - pub async fn list_datasets(user: &User, req: ListDatasetsRequest) -> Result<()> { let list_dashboards_res = match list_datasets_handler( &user.id, diff --git a/api/src/routes/ws/permissions/list_permission_groups.rs b/api/src/routes/ws/permissions/list_permission_groups.rs index 3f3051bba..2ae80222e 100644 --- a/api/src/routes/ws/permissions/list_permission_groups.rs +++ b/api/src/routes/ws/permissions/list_permission_groups.rs @@ -1,6 +1,5 @@ use anyhow::{anyhow, Result}; use diesel::{ - allow_columns_to_appear_in_same_group_by_clause, dsl::sql, sql_types::{Array, BigInt, Text, Uuid as DieselUuid}, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl, @@ -65,12 +64,6 @@ pub struct PermissionGroupInfo { pub belongs_to: Option, } -allow_columns_to_appear_in_same_group_by_clause!( - permission_groups::id, - permission_groups::name, - permission_groups_to_identities::identity_id, -); - pub async fn list_permission_groups(user: &User, req: ListPermissionGroupsRequest) -> Result<()> { let permission_groups = match list_permission_groups_handler(user, req.page, req.page_size, req.filters).await { diff --git a/api/src/routes/ws/permissions/list_teams.rs b/api/src/routes/ws/permissions/list_teams.rs index 1b868a053..cef46051b 100644 --- a/api/src/routes/ws/permissions/list_teams.rs +++ b/api/src/routes/ws/permissions/list_teams.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; use diesel::{ - allow_columns_to_appear_in_same_group_by_clause, dsl::sql, sql_types::BigInt, + dsl::sql, sql_types::BigInt, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; diff --git a/api/src/routes/ws/permissions/list_users.rs b/api/src/routes/ws/permissions/list_users.rs index fad9cd9d9..ac2f702a4 100644 --- a/api/src/routes/ws/permissions/list_users.rs +++ b/api/src/routes/ws/permissions/list_users.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; use diesel::{ - alias, allow_columns_to_appear_in_same_group_by_clause, + alias, dsl::sql, sql_types::{Array, BigInt, Text, Uuid as DieselUuid}, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl, @@ -160,14 +160,6 @@ async fn list_all_users( let team_pgi = alias!(permission_groups_to_identities as team_pgi); - allow_columns_to_appear_in_same_group_by_clause!( - users::id, - users::name, - users::email, - users_to_organizations::role, - permission_groups_to_identities::permission_group_id - ); - let user_permissions: Vec<(Uuid, Option, String, i64, i64, UserOrganizationRole)> = match users::table .left_join(teams_to_users::table.on(users::id.eq(teams_to_users::user_id).and(teams_to_users::deleted_at.is_null()))) .inner_join(users_to_organizations::table.on(users::id.eq(users_to_organizations::user_id).and(users_to_organizations::deleted_at.is_null()))) diff --git a/api/src/routes/ws/permissions/permissions_utils.rs b/api/src/routes/ws/permissions/permissions_utils.rs index 58a6a82bb..f521f898e 100644 --- a/api/src/routes/ws/permissions/permissions_utils.rs +++ b/api/src/routes/ws/permissions/permissions_utils.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use diesel::{ - alias, allow_columns_to_appear_in_same_group_by_clause, + alias, deserialize::QueryableByName, dsl::{count, not, sql}, insert_into, @@ -523,12 +523,6 @@ async fn get_user_queries_last_30_days(user_id: Arc) -> Result { async fn get_user_permission_groups(user_id: Arc) -> Result> { let mut conn = get_pg_pool().get().await?; - allow_columns_to_appear_in_same_group_by_clause!( - permission_groups::id, - permission_groups::name, - permission_groups_to_identities::identity_type, - ); - let permission_groups = match permission_groups::table .inner_join( permission_groups_to_identities::table