Merge pull request #531 from buster-so/dallin/bus-1272-share-a-chat-functionality

Implement chat access checks for metrics and dashboards
This commit is contained in:
dal 2025-07-17 09:18:48 -07:00 committed by GitHub
commit 562f046f78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 363 additions and 127 deletions

View File

@ -147,10 +147,22 @@ 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.");
// 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, insufficient direct permission).");
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.");
@ -191,6 +203,7 @@ pub async fn get_dashboard_handler(
permission = AssetPermissionRole::CanView;
}
}
}
let mut conn = match get_pg_pool().get().await {
Ok(conn) => conn,

View File

@ -100,10 +100,41 @@ 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.");
// 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
tracing::error!("Metric retrieval failed for non-permission reason: {}", e);

View File

@ -169,10 +169,22 @@ 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.");
// 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 access, insufficient direct permission).");
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.");
@ -214,6 +226,7 @@ pub async fn get_metric_for_dashboard_handler(
}
}
}
}
// Declare variables to hold potentially versioned data
let resolved_name: String;

View File

@ -167,10 +167,22 @@ 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.");
// 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 access, insufficient direct permission).");
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.");
@ -212,6 +224,7 @@ pub async fn get_metric_handler(
}
}
}
}
// Declare variables to hold potentially versioned data
let resolved_name: String;

View File

@ -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<bool>` - 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<bool, diesel::result::Error> {
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::<Uuid>(&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::<Uuid>(&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<bool>` - 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<bool, diesel::result::Error> {
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::<Uuid>(&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::<Uuid>(&mut conn)
.await
.optional()?;
Ok(has_public_chat_access.is_some())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -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};