diff --git a/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs b/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs index 074b85802..f91211faf 100644 --- a/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs +++ b/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs @@ -147,48 +147,61 @@ pub async fn get_dashboard_handler( tracing::debug!(dashboard_id = %dashboard_id, user_id = %user.id, ?permission, "Granting access via direct permission."); } } else { - // No sufficient direct/admin permission, check public access rules - tracing::debug!(dashboard_id = %dashboard_id, "Insufficient direct/admin permission. Checking public access rules."); - if !dashboard_file.publicly_accessible { - tracing::warn!(dashboard_id = %dashboard_id, user_id = %user.id, "Permission denied (not public, insufficient direct permission)."); - return Err(anyhow!("You don't have permission to view this dashboard")); - } - tracing::debug!(dashboard_id = %dashboard_id, "Dashboard is publicly accessible."); - - // Check if the public access has expired - if let Some(expiry_date) = dashboard_file.public_expiry_date { - tracing::debug!(dashboard_id = %dashboard_id, ?expiry_date, "Checking expiry date"); - if expiry_date < chrono::Utc::now() { - tracing::warn!(dashboard_id = %dashboard_id, "Public access expired"); - return Err(anyhow!("Public access to this dashboard has expired")); - } - } - - // Check if a password is required - tracing::debug!(dashboard_id = %dashboard_id, has_password = dashboard_file.public_password.is_some(), "Checking password requirement"); - if let Some(required_password) = &dashboard_file.public_password { - tracing::debug!(dashboard_id = %dashboard_id, "Password required. Checking provided password."); - match password { - Some(provided_password) => { - if provided_password != *required_password { - // Incorrect password provided - tracing::warn!(dashboard_id = %dashboard_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!(dashboard_id = %dashboard_id, user_id = %user.id, "Correct public password provided. Granting CanView."); - permission = AssetPermissionRole::CanView; - } - None => { - // Password required but none provided - tracing::warn!(dashboard_id = %dashboard_id, user_id = %user.id, "Public password required but none provided"); - return Err(anyhow!("public_password required for this dashboard")); - } - } - } else { - // Publicly accessible, not expired, and no password required - tracing::debug!(dashboard_id = %dashboard_id, "Public access granted (no password required)."); + // No sufficient direct/admin permission, check if user has access via a chat + tracing::debug!(dashboard_id = %dashboard_id, "Insufficient direct/admin permission. Checking chat access."); + + let has_chat_access = sharing::check_dashboard_chat_access(dashboard_id, &user.id) + .await + .unwrap_or(false); + + if has_chat_access { + // User has access to a chat containing this dashboard, grant CanView + tracing::debug!(dashboard_id = %dashboard_id, user_id = %user.id, "User has access via chat. Granting CanView."); permission = AssetPermissionRole::CanView; + } else { + // No chat access either, check public access rules + tracing::debug!(dashboard_id = %dashboard_id, "No chat access. Checking public access rules."); + if !dashboard_file.publicly_accessible { + tracing::warn!(dashboard_id = %dashboard_id, user_id = %user.id, "Permission denied (not public, no chat access, insufficient direct permission)."); + return Err(anyhow!("You don't have permission to view this dashboard")); + } + tracing::debug!(dashboard_id = %dashboard_id, "Dashboard is publicly accessible."); + + // Check if the public access has expired + if let Some(expiry_date) = dashboard_file.public_expiry_date { + tracing::debug!(dashboard_id = %dashboard_id, ?expiry_date, "Checking expiry date"); + if expiry_date < chrono::Utc::now() { + tracing::warn!(dashboard_id = %dashboard_id, "Public access expired"); + return Err(anyhow!("Public access to this dashboard has expired")); + } + } + + // Check if a password is required + tracing::debug!(dashboard_id = %dashboard_id, has_password = dashboard_file.public_password.is_some(), "Checking password requirement"); + if let Some(required_password) = &dashboard_file.public_password { + tracing::debug!(dashboard_id = %dashboard_id, "Password required. Checking provided password."); + match password { + Some(provided_password) => { + if provided_password != *required_password { + // Incorrect password provided + tracing::warn!(dashboard_id = %dashboard_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!(dashboard_id = %dashboard_id, user_id = %user.id, "Correct public password provided. Granting CanView."); + permission = AssetPermissionRole::CanView; + } + None => { + // Password required but none provided + tracing::warn!(dashboard_id = %dashboard_id, user_id = %user.id, "Public password required but none provided"); + return Err(anyhow!("public_password required for this dashboard")); + } + } + } else { + // Publicly accessible, not expired, and no password required + tracing::debug!(dashboard_id = %dashboard_id, "Public access granted (no password required)."); + permission = AssetPermissionRole::CanView; + } } } 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 47f5268e4..f7bb0ecdc 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 @@ -100,9 +100,40 @@ pub async fn get_metric_data_handler( } } } else { - // No dashboard access, return the original permission error - tracing::warn!("No dashboard association found for metric. Returning original error."); - return Err(e); + // No dashboard access, check if user has access via a chat + tracing::info!("No dashboard association found. Checking chat access."); + let has_chat_access = sharing::check_metric_chat_access(&request.metric_id, &user.id) + .await + .unwrap_or(false); + + if has_chat_access { + // User has access to a chat containing this metric + tracing::info!("Found associated chat with user access. Fetching metric with chat context."); + match get_metric_for_dashboard_handler( + &request.metric_id, + &user, + request.version_number, + request.password.clone(), + ) + .await + { + Ok(metric_via_chat) => { + tracing::debug!( + "Successfully retrieved metric via chat association." + ); + metric_via_chat // Use this metric definition + } + Err(fetch_err) => { + // If fetching via chat fails unexpectedly, return that error + tracing::error!("Failed to fetch metric via chat context: {}", fetch_err); + return Err(fetch_err); + } + } + } else { + // No dashboard or chat access, return the original permission error + tracing::warn!("No dashboard or chat association found for metric. Returning original error."); + return Err(e); + } } } else { // Error was not permission-related, return original error 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 27df1131b..abe33cb54 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 @@ -169,48 +169,61 @@ pub async fn get_metric_for_dashboard_handler( 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)."); + // No dashboard access, check if user has access via a chat + tracing::debug!(metric_id = %metric_id, "No dashboard access. Checking chat access."); + + let has_chat_access = sharing::check_metric_chat_access(metric_id, &user.id) + .await + .unwrap_or(false); + + if has_chat_access { + // User has access to a chat containing this metric, grant CanView + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via chat. Granting CanView."); permission = AssetPermissionRole::CanView; + } else { + // No chat access either, check public access rules + tracing::debug!(metric_id = %metric_id, "No chat 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/chat 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 855a4dd3a..41c7ca42c 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs @@ -167,48 +167,61 @@ pub async fn get_metric_handler( 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)."); + // No dashboard access, check if user has access via a chat + tracing::debug!(metric_id = %metric_id, "No dashboard access. Checking chat access."); + + let has_chat_access = sharing::check_metric_chat_access(metric_id, &user.id) + .await + .unwrap_or(false); + + if has_chat_access { + // User has access to a chat containing this metric, grant CanView + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via chat. Granting CanView."); permission = AssetPermissionRole::CanView; + } else { + // No chat access either, check public access rules + tracing::debug!(metric_id = %metric_id, "No chat 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/chat 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 a5a662a4c..2d34ab0e2 100644 --- a/apps/api/libs/sharing/src/asset_access_checks.rs +++ b/apps/api/libs/sharing/src/asset_access_checks.rs @@ -117,6 +117,172 @@ pub async fn check_metric_dashboard_access( Ok(has_public_access.is_some()) } +/// Checks if a user has access to a metric through any associated chat. +/// +/// This function is used to implement permission cascading from chats to metrics. +/// If a user has access to any chat containing the metric, 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 chat containing the metric, false otherwise +pub async fn check_metric_chat_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()), + ) + })?; + + // Check if user has access to any chat containing this metric + let has_chat_access = database::schema::messages_to_files::table + .inner_join( + database::schema::messages::table + .on(database::schema::messages::id.eq(database::schema::messages_to_files::message_id)), + ) + .inner_join( + database::schema::chats::table + .on(database::schema::chats::id.eq(database::schema::messages::chat_id)), + ) + .inner_join( + asset_permissions::table.on( + asset_permissions::asset_id.eq(database::schema::chats::id) + .and(asset_permissions::asset_type.eq(AssetType::Chat)) + .and(asset_permissions::identity_id.eq(user_id)) + .and(asset_permissions::identity_type.eq(IdentityType::User)) + .and(asset_permissions::deleted_at.is_null()) + ), + ) + .filter(database::schema::messages_to_files::file_id.eq(metric_id)) + .filter(database::schema::messages_to_files::deleted_at.is_null()) + .filter(database::schema::messages::deleted_at.is_null()) + .filter(database::schema::chats::deleted_at.is_null()) + .select(database::schema::chats::id) + .first::(&mut conn) + .await + .optional()?; + + if has_chat_access.is_some() { + return Ok(true); + } + + // Now check if metric belongs to any PUBLIC chat + let now = chrono::Utc::now(); + let has_public_chat_access = database::schema::messages_to_files::table + .inner_join( + database::schema::messages::table + .on(database::schema::messages::id.eq(database::schema::messages_to_files::message_id)), + ) + .inner_join( + database::schema::chats::table + .on(database::schema::chats::id.eq(database::schema::messages::chat_id)), + ) + .filter(database::schema::messages_to_files::file_id.eq(metric_id)) + .filter(database::schema::messages_to_files::deleted_at.is_null()) + .filter(database::schema::messages::deleted_at.is_null()) + .filter(database::schema::chats::deleted_at.is_null()) + .filter(database::schema::chats::publicly_accessible.eq(true)) + .filter( + database::schema::chats::public_expiry_date + .is_null() + .or(database::schema::chats::public_expiry_date.gt(now)), + ) + .select(database::schema::chats::id) + .first::(&mut conn) + .await + .optional()?; + + Ok(has_public_chat_access.is_some()) +} + +/// Checks if a user has access to a dashboard through any associated chat. +/// +/// This function is used to implement permission cascading from chats to dashboards. +/// If a user has access to any chat containing the dashboard, they get at least CanView permission. +/// +/// # Arguments +/// * `dashboard_id` - UUID of the dashboard to check +/// * `user_id` - UUID of the user to check permissions for +/// +/// # Returns +/// * `Result` - True if the user has access to any chat containing the dashboard, false otherwise +pub async fn check_dashboard_chat_access( + dashboard_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()), + ) + })?; + + // Check if user has access to any chat containing this dashboard + let has_chat_access = database::schema::messages_to_files::table + .inner_join( + database::schema::messages::table + .on(database::schema::messages::id.eq(database::schema::messages_to_files::message_id)), + ) + .inner_join( + database::schema::chats::table + .on(database::schema::chats::id.eq(database::schema::messages::chat_id)), + ) + .inner_join( + asset_permissions::table.on( + asset_permissions::asset_id.eq(database::schema::chats::id) + .and(asset_permissions::asset_type.eq(AssetType::Chat)) + .and(asset_permissions::identity_id.eq(user_id)) + .and(asset_permissions::identity_type.eq(IdentityType::User)) + .and(asset_permissions::deleted_at.is_null()) + ), + ) + .filter(database::schema::messages_to_files::file_id.eq(dashboard_id)) + .filter(database::schema::messages_to_files::deleted_at.is_null()) + .filter(database::schema::messages::deleted_at.is_null()) + .filter(database::schema::chats::deleted_at.is_null()) + .select(database::schema::chats::id) + .first::(&mut conn) + .await + .optional()?; + + if has_chat_access.is_some() { + return Ok(true); + } + + // Now check if dashboard belongs to any PUBLIC chat + let now = chrono::Utc::now(); + let has_public_chat_access = database::schema::messages_to_files::table + .inner_join( + database::schema::messages::table + .on(database::schema::messages::id.eq(database::schema::messages_to_files::message_id)), + ) + .inner_join( + database::schema::chats::table + .on(database::schema::chats::id.eq(database::schema::messages::chat_id)), + ) + .filter(database::schema::messages_to_files::file_id.eq(dashboard_id)) + .filter(database::schema::messages_to_files::deleted_at.is_null()) + .filter(database::schema::messages::deleted_at.is_null()) + .filter(database::schema::chats::deleted_at.is_null()) + .filter(database::schema::chats::publicly_accessible.eq(true)) + .filter( + database::schema::chats::public_expiry_date + .is_null() + .or(database::schema::chats::public_expiry_date.gt(now)), + ) + .select(database::schema::chats::id) + .first::(&mut conn) + .await + .optional()?; + + Ok(has_public_chat_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 6658b9ba4..ec086d512 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, check_metric_dashboard_access}; +pub use asset_access_checks::{check_permission_access, check_metric_dashboard_access, check_metric_chat_access, check_dashboard_chat_access};