From 65840319fa75ac1bad30eb071c81428063d74bf7 Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 19 Mar 2025 16:56:54 -0600 Subject: [PATCH] ok get metric has permissions on it now --- .../src/metrics/get_metric_handler.rs | 103 +++++++++++-- api/libs/handlers/src/metrics/types.rs | 13 ++ .../integration/metrics/get_metric_test.rs | 145 ++++++++++++++++++ api/tests/integration/metrics/mod.rs | 1 + 4 files changed, 250 insertions(+), 12 deletions(-) create mode 100644 api/tests/integration/metrics/get_metric_test.rs diff --git a/api/libs/handlers/src/metrics/get_metric_handler.rs b/api/libs/handlers/src/metrics/get_metric_handler.rs index b7ce7de46..94b2fbc27 100644 --- a/api/libs/handlers/src/metrics/get_metric_handler.rs +++ b/api/libs/handlers/src/metrics/get_metric_handler.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; use database::types::VersionHistory; -use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; +use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl, Queryable, Selectable, SelectableHelper}; use diesel_async::RunQueryDsl; use serde_json::Value; use serde_yaml; @@ -9,9 +9,9 @@ use uuid::Uuid; use crate::metrics::types::{ BusterMetric, ColumnMetaData, ColumnType, DataMetadata, Dataset, MinMaxValue, SimpleType, }; -use database::enums::{AssetPermissionRole, Verification}; +use database::enums::{AssetPermissionRole, AssetType, IdentityType, Verification}; use database::pool::get_pg_pool; -use database::schema::{datasets, metric_files}; +use database::schema::{asset_permissions, datasets, metric_files, users}; use database::types::MetricYml; use super::Version; @@ -31,6 +31,9 @@ struct QueryableMetricFile { created_at: chrono::DateTime, updated_at: chrono::DateTime, version_history: VersionHistory, + publicly_accessible: bool, + publicly_enabled_by: Option, + public_expiry_date: Option>, } #[derive(Queryable)] @@ -39,20 +42,34 @@ struct DatasetInfo { name: String, } -#[derive(Queryable)] +#[derive(Queryable, Selectable)] #[diesel(table_name = users)] struct UserInfo { + id: Uuid, + email: String, #[diesel(sql_type = diesel::sql_types::Nullable)] name: Option, #[diesel(sql_type = diesel::sql_types::Nullable)] avatar_url: Option, } +#[derive(Queryable)] +struct AssetPermissionInfo { + identity_id: Uuid, + role: AssetPermissionRole, + email: String, + name: Option, +} + /// Handler to retrieve a metric by ID with optional version number -/// +/// /// If version_number is provided, returns that specific version of the metric. /// If version_number is None, returns the latest version of the metric. -pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number: Option) -> Result { +pub async fn get_metric_handler( + metric_id: &Uuid, + user_id: &Uuid, + version_number: Option, +) -> Result { let mut conn = match get_pg_pool().get().await { Ok(conn) => conn, Err(e) => return Err(anyhow!("Failed to get database connection: {}", e)), @@ -75,6 +92,9 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number metric_files::created_at, metric_files::updated_at, metric_files::version_history, + metric_files::publicly_accessible, + metric_files::publicly_enabled_by, + metric_files::public_expiry_date, )) .first::(&mut conn) .await @@ -99,8 +119,10 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number // Get the specific version if it exists if let Some(v) = metric_file.version_history.get_version(version) { match &v.content { - database::types::VersionContent::MetricYml(content) => (content.clone(), v.version_number), - _ => return Err(anyhow!("Invalid version content type")) + database::types::VersionContent::MetricYml(content) => { + (content.clone(), v.version_number) + } + _ => return Err(anyhow!("Invalid version content type")), } } else { return Err(anyhow!("Version {} not found", version)); @@ -109,8 +131,10 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number // Get the latest version if let Some(v) = metric_file.version_history.get_latest_version() { match &v.content { - database::types::VersionContent::MetricYml(content) => (content.clone(), v.version_number), - _ => return Err(anyhow!("Invalid version content type")) + database::types::VersionContent::MetricYml(content) => { + (content.clone(), v.version_number) + } + _ => return Err(anyhow!("Invalid version content type")), } } else { // Fall back to current content if no version history @@ -189,10 +213,59 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number updated_at: v.updated_at, }) .collect(); - + // Sort versions by version_number in ascending order versions.sort_by(|a, b| a.version_number.cmp(&b.version_number)); - + + // Query individual permissions for this metric + 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(metric_id)) + .filter(asset_permissions::asset_type.eq(AssetType::MetricFile)) + .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::(&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) = metric_file.publicly_enabled_by { + users::table + .filter(users::id.eq(enabled_by_id)) + .select(users::email) + .first::(&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| crate::metrics::types::BusterShareIndividual { + email: p.email, + role: p.role, + name: p.name, + }) + .collect::>(), + ) + } + } + Err(_) => None, + }; + // Construct BusterMetric Ok(BusterMetric { id: metric_file.id, @@ -223,5 +296,11 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number // TODO: get the actual access check permission: AssetPermissionRole::Owner, sql: metric_content.sql, + // New sharing fields + individual_permissions, + publicly_accessible: metric_file.publicly_accessible, + public_expiry_date: metric_file.public_expiry_date, + public_enabled_by: public_enabled_by_user, + public_password: None, // Currently not stored in the database }) } diff --git a/api/libs/handlers/src/metrics/types.rs b/api/libs/handlers/src/metrics/types.rs index bee48a1ca..9f96d0c2b 100644 --- a/api/libs/handlers/src/metrics/types.rs +++ b/api/libs/handlers/src/metrics/types.rs @@ -17,6 +17,13 @@ pub struct Version { pub updated_at: DateTime, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BusterShareIndividual { + pub email: String, + pub role: AssetPermissionRole, + pub name: Option, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BusterMetric { pub id: Uuid, @@ -47,6 +54,12 @@ pub struct BusterMetric { pub versions: Vec, pub permission: AssetPermissionRole, pub sql: String, + // Sharing fields + pub individual_permissions: Option>, + pub public_expiry_date: Option>, + pub public_enabled_by: Option, + pub publicly_accessible: bool, + pub public_password: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/api/tests/integration/metrics/get_metric_test.rs b/api/tests/integration/metrics/get_metric_test.rs new file mode 100644 index 000000000..e4713b045 --- /dev/null +++ b/api/tests/integration/metrics/get_metric_test.rs @@ -0,0 +1,145 @@ +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_metric_with_sharing_info() { + // Setup test environment + let env = create_env().await; + let client = TestClient::new(&env); + + // Create test user and metric + let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + let metric_id = create_test_metric(&env, user_id).await; + + // Add sharing permissions + add_test_permissions(&env, metric_id, user_id).await; + + // Add public sharing + enable_public_sharing(&env, metric_id, user_id).await; + + // Test GET request + let response = client + .get(&format!("/api/v1/metrics/{}", metric_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["id"], metric_id.to_string()); + assert_eq!(data["type"], "metric"); + + // 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_metric(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::(user_id) + .bind::("test@example.com") + .bind::("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::(user2_id) + .bind::("test2@example.com") + .bind::("Test User 2") + .execute(&mut conn) + .await + .unwrap(); + + // Insert test metric + let metric_id = Uuid::parse_str("00000000-0000-0000-0000-000000000010").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::(org_id) + .bind::("Test Organization") + .execute(&mut conn) + .await + .unwrap(); + + // Insert metric + diesel::sql_query(r#" + INSERT INTO metric_files (id, name, file_name, content, verification, organization_id, created_by, version_history) + VALUES ($1, 'Test Metric', 'test.yml', '{"description": "Test description", "time_frame": "daily", "dataset_ids": [], "chart_config": {}, "sql": "SELECT 1;"}', 'notRequested', $2, $3, '{}'::jsonb) + ON CONFLICT DO NOTHING + "#) + .bind::(metric_id) + .bind::(org_id) + .bind::(user_id) + .execute(&mut conn) + .await + .unwrap(); + + metric_id +} + +async fn add_test_permissions(env: &TestEnv, metric_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::(user2_id) + .bind::(IdentityTypeEnum::User.to_string()) + .bind::(metric_id) + .bind::(AssetTypeEnum::MetricFile.to_string()) + .bind::(AssetPermissionRole::CanView.to_string()) + .bind::(user_id) + .execute(&mut conn) + .await + .unwrap(); +} + +async fn enable_public_sharing(env: &TestEnv, metric_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 metric_files + SET publicly_accessible = true, publicly_enabled_by = $1, public_expiry_date = $2 + WHERE id = $3 + "#) + .bind::(user_id) + .bind::(expiry_date) + .bind::(metric_id) + .execute(&mut conn) + .await + .unwrap(); +} \ No newline at end of file diff --git a/api/tests/integration/metrics/mod.rs b/api/tests/integration/metrics/mod.rs index 0dea51762..fb9b7b1ef 100644 --- a/api/tests/integration/metrics/mod.rs +++ b/api/tests/integration/metrics/mod.rs @@ -2,4 +2,5 @@ pub mod update_metric_test; pub mod delete_metric_test; pub mod post_metric_dashboard_test; +pub mod get_metric_test; pub mod sharing; \ No newline at end of file