mirror of https://github.com/buster-so/buster.git
add same individual permissions logic on dashboards
This commit is contained in:
parent
65840319fa
commit
0e6cf53606
|
@ -1,7 +1,7 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable};
|
||||
use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl, Queryable, Selectable, SelectableHelper};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use serde_json::{json, Value};
|
||||
use uuid::Uuid;
|
||||
|
@ -9,11 +9,12 @@ use futures::future::{try_join_all, join_all};
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde_yaml;
|
||||
|
||||
|
||||
use crate::dashboards::types::BusterShareIndividual;
|
||||
use crate::metrics::{get_metric_handler, BusterMetric};
|
||||
use database::enums::{AssetPermissionRole, Verification};
|
||||
use database::enums::{AssetPermissionRole, AssetType, IdentityType, Verification};
|
||||
use database::pool::get_pg_pool;
|
||||
use database::schema::dashboard_files;
|
||||
use database::schema::{asset_permissions, dashboard_files, users};
|
||||
use database::types::VersionHistory;
|
||||
|
||||
use super::{BusterDashboard, BusterDashboardResponse, DashboardConfig, DashboardRow, DashboardRowItem};
|
||||
|
||||
|
@ -29,6 +30,18 @@ struct QueryableDashboardFile {
|
|||
created_by: Uuid,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
publicly_accessible: bool,
|
||||
publicly_enabled_by: Option<Uuid>,
|
||||
public_expiry_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
version_history: VersionHistory,
|
||||
}
|
||||
|
||||
#[derive(Queryable)]
|
||||
struct AssetPermissionInfo {
|
||||
identity_id: Uuid,
|
||||
role: AssetPermissionRole,
|
||||
email: String,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_dashboard_handler(dashboard_id: &Uuid, user_id: &Uuid) -> Result<BusterDashboardResponse> {
|
||||
|
@ -51,11 +64,15 @@ pub async fn get_dashboard_handler(dashboard_id: &Uuid, user_id: &Uuid) -> Resul
|
|||
dashboard_files::created_by,
|
||||
dashboard_files::created_at,
|
||||
dashboard_files::updated_at,
|
||||
dashboard_files::publicly_accessible,
|
||||
dashboard_files::publicly_enabled_by,
|
||||
dashboard_files::public_expiry_date,
|
||||
dashboard_files::version_history,
|
||||
))
|
||||
.first::<QueryableDashboardFile>(&mut conn)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
diesel::result::Error::NotFound => anyhow!("Dashboard file not found"),
|
||||
diesel::result::Error::NotFound => anyhow!("Dashboard file not found or unauthorized"),
|
||||
_ => anyhow!("Database error: {}", e),
|
||||
})?;
|
||||
|
||||
|
@ -102,6 +119,55 @@ pub async fn get_dashboard_handler(dashboard_id: &Uuid, user_id: &Uuid) -> Resul
|
|||
.map(|metric| (metric.id, metric))
|
||||
.collect();
|
||||
|
||||
// Query individual permissions for this dashboard
|
||||
let individual_permissions_query = asset_permissions::table
|
||||
.inner_join(users::table.on(users::id.eq(asset_permissions::identity_id)))
|
||||
.filter(asset_permissions::asset_id.eq(dashboard_id))
|
||||
.filter(asset_permissions::asset_type.eq(AssetType::DashboardFile))
|
||||
.filter(asset_permissions::identity_type.eq(IdentityType::User))
|
||||
.filter(asset_permissions::deleted_at.is_null())
|
||||
.select((
|
||||
asset_permissions::identity_id,
|
||||
asset_permissions::role,
|
||||
users::email,
|
||||
users::name,
|
||||
))
|
||||
.load::<AssetPermissionInfo>(&mut conn)
|
||||
.await;
|
||||
|
||||
// Get the user info for publicly_enabled_by if it exists
|
||||
let public_enabled_by_user = if let Some(enabled_by_id) = dashboard_file.publicly_enabled_by {
|
||||
users::table
|
||||
.filter(users::id.eq(enabled_by_id))
|
||||
.select(users::email)
|
||||
.first::<String>(&mut conn)
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Convert AssetPermissionInfo to BusterShareIndividual
|
||||
let individual_permissions = match individual_permissions_query {
|
||||
Ok(permissions) => {
|
||||
if permissions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
permissions
|
||||
.into_iter()
|
||||
.map(|p| BusterShareIndividual {
|
||||
email: p.email,
|
||||
role: p.role,
|
||||
name: p.name,
|
||||
})
|
||||
.collect::<Vec<BusterShareIndividual>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
// Construct the dashboard using content values where available
|
||||
let dashboard = BusterDashboard {
|
||||
config,
|
||||
|
@ -128,6 +194,11 @@ pub async fn get_dashboard_handler(dashboard_id: &Uuid, user_id: &Uuid) -> Resul
|
|||
permission: AssetPermissionRole::Owner,
|
||||
public_password: None,
|
||||
collections: vec![],
|
||||
// New sharing fields
|
||||
individual_permissions,
|
||||
publicly_accessible: dashboard_file.publicly_accessible,
|
||||
public_expiry_date: dashboard_file.public_expiry_date,
|
||||
public_enabled_by: public_enabled_by_user,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,13 @@ pub struct DashboardMember {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BusterShareIndividual {
|
||||
pub email: String,
|
||||
pub role: AssetPermissionRole,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
// Note: This extends BusterShare which needs to be defined
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BusterDashboardResponse {
|
||||
|
@ -35,6 +42,11 @@ pub struct BusterDashboardResponse {
|
|||
pub permission: AssetPermissionRole,
|
||||
pub public_password: Option<String>,
|
||||
pub collections: Vec<Collection>,
|
||||
// New sharing fields
|
||||
pub individual_permissions: Option<Vec<BusterShareIndividual>>,
|
||||
pub publicly_accessible: bool,
|
||||
pub public_expiry_date: Option<DateTime<Utc>>,
|
||||
pub public_enabled_by: Option<String>,
|
||||
}
|
||||
|
||||
// Note: This extends BusterShare but omits certain fields
|
||||
|
|
|
@ -10,10 +10,21 @@ pub async fn get_dashboard_rest_handler(
|
|||
Extension(user): Extension<AuthenticatedUser>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<ApiResponse<BusterDashboardResponse>, (StatusCode, &'static str)> {
|
||||
tracing::info!(
|
||||
"Processing GET request for dashboard with ID: {}, user_id: {}",
|
||||
id,
|
||||
user.id
|
||||
);
|
||||
|
||||
let dashboard = match get_dashboard_handler(&id, &user.id).await {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting dashboard: {}", e);
|
||||
let error_message = e.to_string();
|
||||
// Return 404 if not found or unauthorized
|
||||
if error_message.contains("not found") || error_message.contains("unauthorized") {
|
||||
return Err((StatusCode::NOT_FOUND, "Dashboard not found or unauthorized"));
|
||||
}
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to get dashboard"));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
use uuid::Uuid;
|
||||
use crate::common::{
|
||||
env::{create_env, TestEnv},
|
||||
http::client::TestClient,
|
||||
assertions::response::assert_api_ok,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use database::enums::{AssetPermissionRole, AssetTypeEnum, IdentityTypeEnum};
|
||||
use diesel::{ExpressionMethods, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_dashboard_with_sharing_info() {
|
||||
// Setup test environment
|
||||
let env = create_env().await;
|
||||
let client = TestClient::new(&env);
|
||||
|
||||
// Create test user and dashboard
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
|
||||
let dashboard_id = create_test_dashboard(&env, user_id).await;
|
||||
|
||||
// Add sharing permissions
|
||||
add_test_permissions(&env, dashboard_id, user_id).await;
|
||||
|
||||
// Add public sharing
|
||||
enable_public_sharing(&env, dashboard_id, user_id).await;
|
||||
|
||||
// Test GET request
|
||||
let response = client
|
||||
.get(&format!("/api/v1/dashboards/{}", dashboard_id))
|
||||
.header("X-User-Id", user_id.to_string())
|
||||
.send()
|
||||
.await;
|
||||
|
||||
// Assert success and verify response
|
||||
let data = assert_api_ok(response).await;
|
||||
|
||||
// Check fields
|
||||
assert_eq!(data["dashboard"]["id"], dashboard_id.to_string());
|
||||
|
||||
// Check sharing fields
|
||||
assert_eq!(data["publicly_accessible"], true);
|
||||
assert!(data["public_expiry_date"].is_string());
|
||||
assert_eq!(data["public_enabled_by"], "test@example.com");
|
||||
assert_eq!(data["individual_permissions"].as_array().unwrap().len(), 1);
|
||||
|
||||
let permission = &data["individual_permissions"][0];
|
||||
assert_eq!(permission["email"], "test2@example.com");
|
||||
assert_eq!(permission["role"], "viewer");
|
||||
assert_eq!(permission["name"], "Test User 2");
|
||||
}
|
||||
|
||||
// Helper functions to set up the test data
|
||||
async fn create_test_dashboard(env: &TestEnv, user_id: Uuid) -> Uuid {
|
||||
let mut conn = env.db_pool.get().await.unwrap();
|
||||
|
||||
// Insert test user
|
||||
diesel::sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING")
|
||||
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||
.bind::<diesel::sql_types::Text, _>("test@example.com")
|
||||
.bind::<diesel::sql_types::Text, _>("Test User")
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert another test user
|
||||
let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap();
|
||||
diesel::sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING")
|
||||
.bind::<diesel::sql_types::Uuid, _>(user2_id)
|
||||
.bind::<diesel::sql_types::Text, _>("test2@example.com")
|
||||
.bind::<diesel::sql_types::Text, _>("Test User 2")
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert test dashboard
|
||||
let dashboard_id = Uuid::parse_str("00000000-0000-0000-0000-000000000020").unwrap();
|
||||
let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap();
|
||||
|
||||
// Insert test organization if needed
|
||||
diesel::sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING")
|
||||
.bind::<diesel::sql_types::Uuid, _>(org_id)
|
||||
.bind::<diesel::sql_types::Text, _>("Test Organization")
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert dashboard
|
||||
diesel::sql_query(r#"
|
||||
INSERT INTO dashboard_files (id, name, file_name, content, organization_id, created_by, version_history)
|
||||
VALUES ($1, 'Test Dashboard', 'test_dashboard.json', '{"rows": [{"items": [{"id": "00000000-0000-0000-0000-000000000010"}]}], "name": "Test Dashboard", "description": "Test description"}', $2, $3, '{}'::jsonb)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#)
|
||||
.bind::<diesel::sql_types::Uuid, _>(dashboard_id)
|
||||
.bind::<diesel::sql_types::Uuid, _>(org_id)
|
||||
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
dashboard_id
|
||||
}
|
||||
|
||||
async fn add_test_permissions(env: &TestEnv, dashboard_id: Uuid, user_id: Uuid) {
|
||||
let mut conn = env.db_pool.get().await.unwrap();
|
||||
|
||||
// Get the second user
|
||||
let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap();
|
||||
|
||||
// Add permission for user2 as viewer
|
||||
diesel::sql_query(r#"
|
||||
INSERT INTO asset_permissions (identity_id, identity_type, asset_id, asset_type, role, created_by, updated_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $6)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#)
|
||||
.bind::<diesel::sql_types::Uuid, _>(user2_id)
|
||||
.bind::<diesel::sql_types::Text, _>(IdentityTypeEnum::User.to_string())
|
||||
.bind::<diesel::sql_types::Uuid, _>(dashboard_id)
|
||||
.bind::<diesel::sql_types::Text, _>(AssetTypeEnum::DashboardFile.to_string())
|
||||
.bind::<diesel::sql_types::Text, _>(AssetPermissionRole::CanView.to_string())
|
||||
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn enable_public_sharing(env: &TestEnv, dashboard_id: Uuid, user_id: Uuid) {
|
||||
let mut conn = env.db_pool.get().await.unwrap();
|
||||
|
||||
// Set public access
|
||||
let expiry_date = Utc::now() + chrono::Duration::days(7);
|
||||
|
||||
diesel::sql_query(r#"
|
||||
UPDATE dashboard_files
|
||||
SET publicly_accessible = true, publicly_enabled_by = $1, public_expiry_date = $2
|
||||
WHERE id = $3
|
||||
"#)
|
||||
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||
.bind::<diesel::sql_types::Timestamptz, _>(expiry_date)
|
||||
.bind::<diesel::sql_types::Uuid, _>(dashboard_id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod sharing;
|
||||
pub mod sharing;
|
||||
pub mod get_dashboard_test;
|
Loading…
Reference in New Issue