diff --git a/apps/api/libs/handlers/src/metrics/get_metric_data_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_data_handler.rs index 163fc6fde..47f5268e4 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_data_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_data_handler.rs @@ -1,11 +1,10 @@ use anyhow::{anyhow, Result}; -use chrono::Utc; use database::{ pool::get_pg_pool, - schema::{dashboard_files, metric_files, metric_files_to_dashboard_files}, + schema::metric_files, types::{data_metadata::DataMetadata, MetricYml}, }; -use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use indexmap::IndexMap; use middleware::AuthenticatedUser; @@ -68,41 +67,18 @@ pub async fn get_metric_data_handler( if is_permission_error { tracing::warn!( - "Initial metric access failed due to potential permission issue: {}. Checking public dashboard access.", + "Initial metric access failed due to potential permission issue: {}. Checking dashboard access.", e ); - // --- Step 3: Check if metric belongs to a valid public dashboard --- - let mut conn_check = get_pg_pool().get().await?; - let now = Utc::now(); - - let public_dashboard_exists = match metric_files_to_dashboard_files::table - .inner_join(dashboard_files::table.on( - dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id), - )) - .filter(metric_files_to_dashboard_files::metric_file_id.eq(request.metric_id)) - .filter(dashboard_files::publicly_accessible.eq(true)) - .filter(dashboard_files::deleted_at.is_null()) - .filter( - dashboard_files::public_expiry_date - .is_null() - .or(dashboard_files::public_expiry_date.gt(now)), - ) - .select(dashboard_files::id) // Select any column to check existence - .first::(&mut conn_check) // Try to get the first matching ID + // Check if user has access to ANY dashboard containing this metric (including public dashboards) + let has_dashboard_access = sharing::check_metric_dashboard_access(&request.metric_id, &user.id) .await - { - Ok(id) => Some(id), - Err(diesel::NotFound) => None, - Err(e) => { - tracing::error!("Error checking if public dashboard exists: {}", e); - return Err(anyhow!("Error checking if public dashboard exists: {}", e)); - } - }; + .unwrap_or(false); - if public_dashboard_exists.is_some() { - // --- Step 4: Public dashboard found, fetch metric bypassing permissions --- - tracing::info!("Found associated public dashboard. Fetching metric definition without direct permissions."); + if has_dashboard_access { + // User has access to a dashboard containing this metric + tracing::info!("Found associated dashboard with user access. Fetching metric with dashboard context."); match get_metric_for_dashboard_handler( &request.metric_id, &user, @@ -113,19 +89,19 @@ pub async fn get_metric_data_handler( { Ok(metric_via_dashboard) => { tracing::debug!( - "Successfully retrieved metric via public dashboard association." + "Successfully retrieved metric via dashboard association." ); metric_via_dashboard // Use this metric definition } Err(fetch_err) => { // If fetching via dashboard fails unexpectedly, return that error - tracing::error!("Failed to fetch metric via dashboard context even though public dashboard exists: {}", fetch_err); + tracing::error!("Failed to fetch metric via dashboard context: {}", fetch_err); return Err(fetch_err); } } } else { - // No public dashboard association found, return the original permission error - tracing::warn!("No valid public dashboard association found for metric. Returning original error."); + // No dashboard access, return the original permission error + tracing::warn!("No dashboard association found for metric. Returning original error."); return Err(e); } } else { diff --git a/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs index 88d81460c..27df1131b 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs @@ -157,48 +157,61 @@ pub async fn get_metric_for_dashboard_handler( tracing::debug!(metric_id = %metric_id, user_id = %user.id, ?permission, "Granting access via direct permission."); } } else { - // No sufficient direct/admin permission, check public access rules - tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking public access rules."); - if !metric_file.publicly_accessible { - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, insufficient direct permission)."); - return Err(anyhow!("You don't have permission to view this metric")); - } - tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible."); - - // Check if the public access has expired - if let Some(expiry_date) = metric_file.public_expiry_date { - tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date"); - if expiry_date < chrono::Utc::now() { - tracing::warn!(metric_id = %metric_id, "Public access expired"); - return Err(anyhow!("Public access to this metric has expired")); - } - } - - // Check if a password is required - tracing::debug!(metric_id = %metric_id, has_password = metric_file.public_password.is_some(), "Checking password requirement"); - if let Some(required_password) = &metric_file.public_password { - tracing::debug!(metric_id = %metric_id, "Password required. Checking provided password."); - match password { - Some(provided_password) => { - if provided_password != *required_password { - // Incorrect password provided - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Incorrect public password provided"); - return Err(anyhow!("Incorrect password for public access")); - } - // Correct password provided, grant CanView via public access - tracing::debug!(metric_id = %metric_id, user_id = %user.id, "Correct public password provided. Granting CanView."); - permission = AssetPermissionRole::CanView; - } - None => { - // Password required but none provided - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Public password required but none provided"); - return Err(anyhow!("public_password required for this metric")); - } - } - } else { - // Publicly accessible, not expired, and no password required - tracing::debug!(metric_id = %metric_id, "Public access granted (no password required)."); + // No sufficient direct/admin permission, check if user has access via a dashboard + tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking dashboard access."); + + let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id) + .await + .unwrap_or(false); + + if has_dashboard_access { + // User has access to a dashboard containing this metric, grant CanView + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via dashboard. Granting CanView."); permission = AssetPermissionRole::CanView; + } else { + // No dashboard access, check public access rules + tracing::debug!(metric_id = %metric_id, "No dashboard access. Checking public access rules."); + if !metric_file.publicly_accessible { + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, no dashboard access, insufficient direct permission)."); + return Err(anyhow!("You don't have permission to view this metric")); + } + tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible."); + + // Check if the public access has expired + if let Some(expiry_date) = metric_file.public_expiry_date { + tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date"); + if expiry_date < chrono::Utc::now() { + tracing::warn!(metric_id = %metric_id, "Public access expired"); + return Err(anyhow!("Public access to this metric has expired")); + } + } + + // Check if a password is required + tracing::debug!(metric_id = %metric_id, has_password = metric_file.public_password.is_some(), "Checking password requirement"); + if let Some(required_password) = &metric_file.public_password { + tracing::debug!(metric_id = %metric_id, "Password required. Checking provided password."); + match password { + Some(provided_password) => { + if provided_password != *required_password { + // Incorrect password provided + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Incorrect public password provided"); + return Err(anyhow!("Incorrect password for public access")); + } + // Correct password provided, grant CanView via public access + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "Correct public password provided. Granting CanView."); + permission = AssetPermissionRole::CanView; + } + None => { + // Password required but none provided + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Public password required but none provided"); + return Err(anyhow!("public_password required for this metric")); + } + } + } else { + // Publicly accessible, not expired, and no password required + tracing::debug!(metric_id = %metric_id, "Public access granted (no password required)."); + permission = AssetPermissionRole::CanView; + } } } diff --git a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs index 0b3957475..855a4dd3a 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs @@ -155,48 +155,61 @@ pub async fn get_metric_handler( tracing::debug!(metric_id = %metric_id, user_id = %user.id, ?permission, "Granting access via direct permission."); } } else { - // No sufficient direct/admin permission, check public access rules - tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking public access rules."); - if !metric_file.publicly_accessible { - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, insufficient direct permission)."); - return Err(anyhow!("You don't have permission to view this metric")); - } - tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible."); - - // Check if the public access has expired - if let Some(expiry_date) = metric_file.public_expiry_date { - tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date"); - if expiry_date < chrono::Utc::now() { - tracing::warn!(metric_id = %metric_id, "Public access expired"); - return Err(anyhow!("Public access to this metric has expired")); - } - } - - // Check if a password is required - tracing::debug!(metric_id = %metric_id, has_password = metric_file.public_password.is_some(), "Checking password requirement"); - if let Some(required_password) = &metric_file.public_password { - tracing::debug!(metric_id = %metric_id, "Password required. Checking provided password."); - match password { - Some(provided_password) => { - if provided_password != *required_password { - // Incorrect password provided - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Incorrect public password provided"); - return Err(anyhow!("Incorrect password for public access")); - } - // Correct password provided, grant CanView via public access - tracing::debug!(metric_id = %metric_id, user_id = %user.id, "Correct public password provided. Granting CanView."); - permission = AssetPermissionRole::CanView; - } - None => { - // Password required but none provided - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Public password required but none provided"); - return Err(anyhow!("public_password required for this metric")); - } - } - } else { - // Publicly accessible, not expired, and no password required - tracing::debug!(metric_id = %metric_id, "Public access granted (no password required)."); + // No sufficient direct/admin permission, check if user has access via a dashboard + tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking dashboard access."); + + let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id) + .await + .unwrap_or(false); + + if has_dashboard_access { + // User has access to a dashboard containing this metric, grant CanView + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via dashboard. Granting CanView."); permission = AssetPermissionRole::CanView; + } else { + // No dashboard access, check public access rules + tracing::debug!(metric_id = %metric_id, "No dashboard access. Checking public access rules."); + if !metric_file.publicly_accessible { + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, no dashboard access, insufficient direct permission)."); + return Err(anyhow!("You don't have permission to view this metric")); + } + tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible."); + + // Check if the public access has expired + if let Some(expiry_date) = metric_file.public_expiry_date { + tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date"); + if expiry_date < chrono::Utc::now() { + tracing::warn!(metric_id = %metric_id, "Public access expired"); + return Err(anyhow!("Public access to this metric has expired")); + } + } + + // Check if a password is required + tracing::debug!(metric_id = %metric_id, has_password = metric_file.public_password.is_some(), "Checking password requirement"); + if let Some(required_password) = &metric_file.public_password { + tracing::debug!(metric_id = %metric_id, "Password required. Checking provided password."); + match password { + Some(provided_password) => { + if provided_password != *required_password { + // Incorrect password provided + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Incorrect public password provided"); + return Err(anyhow!("Incorrect password for public access")); + } + // Correct password provided, grant CanView via public access + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "Correct public password provided. Granting CanView."); + permission = AssetPermissionRole::CanView; + } + None => { + // Password required but none provided + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Public password required but none provided"); + return Err(anyhow!("public_password required for this metric")); + } + } + } else { + // Publicly accessible, not expired, and no password required + tracing::debug!(metric_id = %metric_id, "Public access granted (no password required)."); + permission = AssetPermissionRole::CanView; + } } } diff --git a/apps/api/libs/sharing/src/asset_access_checks.rs b/apps/api/libs/sharing/src/asset_access_checks.rs index 5ecdc56cd..a5a662a4c 100644 --- a/apps/api/libs/sharing/src/asset_access_checks.rs +++ b/apps/api/libs/sharing/src/asset_access_checks.rs @@ -1,4 +1,8 @@ -use database::enums::{AssetPermissionRole, UserOrganizationRole}; +use database::enums::{AssetPermissionRole, AssetType, IdentityType, UserOrganizationRole}; +use database::pool::get_pg_pool; +use database::schema::{asset_permissions, dashboard_files, metric_files_to_dashboard_files}; +use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl, OptionalExtension}; +use diesel_async::RunQueryDsl; use middleware::OrganizationMembership; use uuid::Uuid; @@ -39,6 +43,80 @@ pub fn check_permission_access( false } +/// Checks if a user has access to a metric through any associated dashboard. +/// +/// This function is used to implement permission cascading from dashboards to metrics. +/// If a user has access to any dashboard containing the metric (either through direct permissions +/// or if the dashboard is public), they get at least CanView permission. +/// +/// # Arguments +/// * `metric_id` - UUID of the metric to check +/// * `user_id` - UUID of the user to check permissions for +/// +/// # Returns +/// * `Result` - True if the user has access to any dashboard containing the metric, false otherwise +pub async fn check_metric_dashboard_access( + metric_id: &Uuid, + user_id: &Uuid, +) -> Result { + let mut conn = get_pg_pool().get().await.map_err(|e| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UnableToSendCommand, + Box::new(e.to_string()), + ) + })?; + + // First check if user has direct access to any dashboard containing this metric + let has_direct_access = metric_files_to_dashboard_files::table + .inner_join( + dashboard_files::table + .on(dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id)), + ) + .inner_join( + asset_permissions::table.on( + asset_permissions::asset_id.eq(dashboard_files::id) + .and(asset_permissions::asset_type.eq(AssetType::DashboardFile)) + .and(asset_permissions::identity_id.eq(user_id)) + .and(asset_permissions::identity_type.eq(IdentityType::User)) + .and(asset_permissions::deleted_at.is_null()) + ), + ) + .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_id)) + .filter(dashboard_files::deleted_at.is_null()) + .filter(metric_files_to_dashboard_files::deleted_at.is_null()) + .select(dashboard_files::id) + .first::(&mut conn) + .await + .optional()?; + + if has_direct_access.is_some() { + return Ok(true); + } + + // Now check if metric belongs to any PUBLIC dashboard (not expired) + let now = chrono::Utc::now(); + let has_public_access = metric_files_to_dashboard_files::table + .inner_join( + dashboard_files::table + .on(dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id)), + ) + .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_id)) + .filter(dashboard_files::deleted_at.is_null()) + .filter(metric_files_to_dashboard_files::deleted_at.is_null()) + .filter(dashboard_files::publicly_accessible.eq(true)) + .filter( + dashboard_files::public_expiry_date + .is_null() + .or(dashboard_files::public_expiry_date.gt(now)), + ) + .select(dashboard_files::id) + .first::(&mut conn) + .await + .optional()?; + + Ok(has_public_access.is_some()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/apps/api/libs/sharing/src/lib.rs b/apps/api/libs/sharing/src/lib.rs index bb717a629..6658b9ba4 100644 --- a/apps/api/libs/sharing/src/lib.rs +++ b/apps/api/libs/sharing/src/lib.rs @@ -19,4 +19,4 @@ pub use types::{ SerializableAssetPermission, UserInfo, }; pub use user_lookup::find_user_by_email; -pub use asset_access_checks::check_permission_access; +pub use asset_access_checks::{check_permission_access, check_metric_dashboard_access};