mirror of https://github.com/buster-so/buster.git
Implement collection access checks for chats and dashboards
- Updated `get_chat_handler` to check for collection access when a user lacks direct permission. - Modified `get_dashboard_handler` to first verify collection access before checking chat access. - Added `check_chat_collection_access` function to validate user access to chats via collections. - Enhanced `check_metric_dashboard_access` to include collection access checks concurrently with other permission checks. This change improves the permission model by allowing access to chats and dashboards through associated collections, enhancing user experience and security.
This commit is contained in:
parent
955aab3232
commit
1e705c9828
|
@ -17,7 +17,7 @@ use database::{
|
|||
helpers::chats::fetch_chat_with_permission,
|
||||
pool::get_pg_pool,
|
||||
};
|
||||
use sharing::check_permission_access;
|
||||
use sharing::{check_permission_access, compute_effective_permission};
|
||||
|
||||
#[derive(Queryable)]
|
||||
pub struct ChatWithUser {
|
||||
|
@ -100,7 +100,14 @@ pub async fn get_chat_handler(
|
|||
let is_creator = chat_with_permission.chat.created_by == user.id;
|
||||
|
||||
if !has_permission && !is_creator {
|
||||
return Err(anyhow!("You don't have permission to view this chat"));
|
||||
// Check if user has access via a collection
|
||||
let has_collection_access = sharing::check_chat_collection_access(chat_id, &user.id, &user.organizations)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
if !has_collection_access {
|
||||
return Err(anyhow!("You don't have permission to view this chat"));
|
||||
}
|
||||
}
|
||||
|
||||
// Run messages query
|
||||
|
|
|
@ -149,25 +149,37 @@ pub async fn get_dashboard_handler(
|
|||
"Granting access with effective permission (max of direct and workspace sharing)."
|
||||
);
|
||||
} else {
|
||||
// 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.");
|
||||
// No sufficient direct/admin permission, check if user has access via a collection
|
||||
tracing::debug!(dashboard_id = %dashboard_id, "Insufficient direct/admin permission. Checking collection access.");
|
||||
|
||||
let has_chat_access = sharing::check_dashboard_chat_access(dashboard_id, &user.id, &user.organizations)
|
||||
let has_collection_access = sharing::check_dashboard_collection_access(dashboard_id, &user.id, &user.organizations)
|
||||
.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.");
|
||||
if has_collection_access {
|
||||
// User has access to a collection containing this dashboard, grant CanView
|
||||
tracing::debug!(dashboard_id = %dashboard_id, user_id = %user.id, "User has access via collection. 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.");
|
||||
// No collection access, check if user has access via a chat
|
||||
tracing::debug!(dashboard_id = %dashboard_id, "No collection access. Checking chat access.");
|
||||
|
||||
let has_chat_access = sharing::check_dashboard_chat_access(dashboard_id, &user.id, &user.organizations)
|
||||
.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 collection/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 {
|
||||
|
@ -204,6 +216,7 @@ pub async fn get_dashboard_handler(
|
|||
tracing::debug!(dashboard_id = %dashboard_id, "Public access granted (no password required).");
|
||||
permission = AssetPermissionRole::CanView;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use database::enums::{AssetPermissionRole, AssetType, IdentityType, UserOrganizationRole, WorkspaceSharing};
|
||||
use database::pool::get_pg_pool;
|
||||
use database::schema::{asset_permissions, dashboard_files, metric_files_to_dashboard_files, collections, collections_to_assets};
|
||||
use database::schema::{asset_permissions, dashboard_files, metric_files_to_dashboard_files, collections, collections_to_assets, chats};
|
||||
use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl, OptionalExtension};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use middleware::OrganizationMembership;
|
||||
|
@ -126,6 +126,7 @@ pub fn check_permission_access(
|
|||
/// 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,
|
||||
/// workspace sharing, or if the dashboard is public), they get at least CanView permission.
|
||||
/// This also checks if the dashboard itself is accessible via collections (transitive cascading).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `metric_id` - UUID of the metric to check
|
||||
|
@ -146,81 +147,130 @@ pub async fn check_metric_dashboard_access(
|
|||
)
|
||||
})?;
|
||||
|
||||
// First check if user has direct access to any dashboard containing this metric
|
||||
let has_direct_access = metric_files_to_dashboard_files::table
|
||||
// Get all dashboards containing this metric
|
||||
let dashboard_ids: Vec<Uuid> = 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::<Uuid>(&mut conn)
|
||||
.await
|
||||
.optional()?;
|
||||
.load::<Uuid>(&mut conn)
|
||||
.await?;
|
||||
|
||||
if has_direct_access.is_some() {
|
||||
return Ok(true);
|
||||
if dashboard_ids.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 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::<Uuid>(&mut conn)
|
||||
.await
|
||||
.optional()?;
|
||||
// Check multiple access paths concurrently
|
||||
let user_id_clone = *user_id;
|
||||
let user_orgs_clone = user_orgs.to_vec();
|
||||
let dashboard_ids_clone = dashboard_ids.clone();
|
||||
|
||||
// 1. Check direct access to dashboards
|
||||
let direct_access_future = async move {
|
||||
let mut conn = get_pg_pool().get().await.map_err(|e| {
|
||||
diesel::result::Error::DatabaseError(
|
||||
diesel::result::DatabaseErrorKind::UnableToSendCommand,
|
||||
Box::new(e.to_string()),
|
||||
)
|
||||
})?;
|
||||
|
||||
let has_direct = asset_permissions::table
|
||||
.filter(asset_permissions::asset_id.eq_any(&dashboard_ids_clone))
|
||||
.filter(asset_permissions::asset_type.eq(AssetType::DashboardFile))
|
||||
.filter(asset_permissions::identity_id.eq(user_id_clone))
|
||||
.filter(asset_permissions::identity_type.eq(IdentityType::User))
|
||||
.filter(asset_permissions::deleted_at.is_null())
|
||||
.select(asset_permissions::asset_id)
|
||||
.first::<Uuid>(&mut conn)
|
||||
.await
|
||||
.optional()?;
|
||||
|
||||
Ok::<bool, diesel::result::Error>(has_direct.is_some())
|
||||
};
|
||||
|
||||
if has_public_access.is_some() {
|
||||
return Ok(true);
|
||||
}
|
||||
// 2. Check public access to dashboards
|
||||
let dashboard_ids_public = dashboard_ids.clone();
|
||||
let public_access_future = async move {
|
||||
let mut conn = get_pg_pool().get().await.map_err(|e| {
|
||||
diesel::result::Error::DatabaseError(
|
||||
diesel::result::DatabaseErrorKind::UnableToSendCommand,
|
||||
Box::new(e.to_string()),
|
||||
)
|
||||
})?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let has_public = dashboard_files::table
|
||||
.filter(dashboard_files::id.eq_any(&dashboard_ids_public))
|
||||
.filter(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::<Uuid>(&mut conn)
|
||||
.await
|
||||
.optional()?;
|
||||
|
||||
Ok::<bool, diesel::result::Error>(has_public.is_some())
|
||||
};
|
||||
|
||||
// Check if metric belongs to any workspace-shared dashboard
|
||||
let workspace_shared_dashboard = 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::workspace_sharing.ne(WorkspaceSharing::None))
|
||||
.select((dashboard_files::organization_id, dashboard_files::workspace_sharing))
|
||||
.first::<(Uuid, WorkspaceSharing)>(&mut conn)
|
||||
.await
|
||||
.optional()?;
|
||||
|
||||
if let Some((org_id, _sharing_level)) = workspace_shared_dashboard {
|
||||
// Check if user is member of that organization
|
||||
if user_orgs.iter().any(|org| org.id == org_id) {
|
||||
return Ok(true);
|
||||
// 3. Check workspace sharing on dashboards
|
||||
let dashboard_ids_ws = dashboard_ids.clone();
|
||||
let user_orgs_ws = user_orgs.to_vec();
|
||||
let workspace_access_future = async move {
|
||||
let mut conn = get_pg_pool().get().await.map_err(|e| {
|
||||
diesel::result::Error::DatabaseError(
|
||||
diesel::result::DatabaseErrorKind::UnableToSendCommand,
|
||||
Box::new(e.to_string()),
|
||||
)
|
||||
})?;
|
||||
|
||||
let workspace_dashboards = dashboard_files::table
|
||||
.filter(dashboard_files::id.eq_any(&dashboard_ids_ws))
|
||||
.filter(dashboard_files::deleted_at.is_null())
|
||||
.filter(dashboard_files::workspace_sharing.ne(WorkspaceSharing::None))
|
||||
.select((dashboard_files::organization_id, dashboard_files::workspace_sharing))
|
||||
.load::<(Uuid, WorkspaceSharing)>(&mut conn)
|
||||
.await?;
|
||||
|
||||
for (org_id, _) in workspace_dashboards {
|
||||
if user_orgs_ws.iter().any(|org| org.id == org_id) {
|
||||
return Ok::<bool, diesel::result::Error>(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<bool, diesel::result::Error>(false)
|
||||
};
|
||||
|
||||
Ok(false)
|
||||
// 4. Check if dashboards are accessible via collections
|
||||
let dashboard_ids_coll = dashboard_ids.clone();
|
||||
let user_id_coll = *user_id;
|
||||
let user_orgs_coll = user_orgs.to_vec();
|
||||
let collection_access_future = async move {
|
||||
for dashboard_id in &dashboard_ids_coll {
|
||||
if check_dashboard_collection_access(&dashboard_id, &user_id_coll, &user_orgs_coll).await? {
|
||||
return Ok::<bool, diesel::result::Error>(true);
|
||||
}
|
||||
}
|
||||
Ok::<bool, diesel::result::Error>(false)
|
||||
};
|
||||
|
||||
// Execute all checks concurrently
|
||||
let (direct_result, public_result, workspace_result, collection_result) = tokio::join!(
|
||||
direct_access_future,
|
||||
public_access_future,
|
||||
workspace_access_future,
|
||||
collection_access_future
|
||||
);
|
||||
|
||||
// Return true if any check succeeds
|
||||
Ok(direct_result? || public_result? || workspace_result? || collection_result?)
|
||||
}
|
||||
|
||||
/// Checks if a user has access to a metric through any associated chat.
|
||||
|
@ -619,6 +669,85 @@ pub async fn check_dashboard_collection_access(
|
|||
Ok(false)
|
||||
}
|
||||
|
||||
/// Checks if a user has access to a chat through any associated collection.
|
||||
///
|
||||
/// This function is used to implement permission cascading from collections to chats.
|
||||
/// If a user has access to any collection containing the chat (either through direct permissions
|
||||
/// or workspace sharing), they get at least CanView permission.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `chat_id` - UUID of the chat to check
|
||||
/// * `user_id` - UUID of the user to check permissions for
|
||||
/// * `user_orgs` - User's organization memberships
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<bool>` - True if the user has access to any collection containing the chat, false otherwise
|
||||
pub async fn check_chat_collection_access(
|
||||
chat_id: &Uuid,
|
||||
user_id: &Uuid,
|
||||
user_orgs: &[OrganizationMembership],
|
||||
) -> 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()),
|
||||
)
|
||||
})?;
|
||||
|
||||
// First check if user has direct access to any collection containing this chat
|
||||
let has_direct_access = collections_to_assets::table
|
||||
.inner_join(
|
||||
collections::table
|
||||
.on(collections::id.eq(collections_to_assets::collection_id)),
|
||||
)
|
||||
.inner_join(
|
||||
asset_permissions::table.on(
|
||||
asset_permissions::asset_id.eq(collections::id)
|
||||
.and(asset_permissions::asset_type.eq(AssetType::Collection))
|
||||
.and(asset_permissions::identity_id.eq(user_id))
|
||||
.and(asset_permissions::identity_type.eq(IdentityType::User))
|
||||
.and(asset_permissions::deleted_at.is_null())
|
||||
),
|
||||
)
|
||||
.filter(collections_to_assets::asset_id.eq(chat_id))
|
||||
.filter(collections_to_assets::asset_type.eq(AssetType::Chat))
|
||||
.filter(collections::deleted_at.is_null())
|
||||
.filter(collections_to_assets::deleted_at.is_null())
|
||||
.select(collections::id)
|
||||
.first::<Uuid>(&mut conn)
|
||||
.await
|
||||
.optional()?;
|
||||
|
||||
if has_direct_access.is_some() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Check if chat belongs to any workspace-shared collection
|
||||
let workspace_shared_collection = collections_to_assets::table
|
||||
.inner_join(
|
||||
collections::table
|
||||
.on(collections::id.eq(collections_to_assets::collection_id)),
|
||||
)
|
||||
.filter(collections_to_assets::asset_id.eq(chat_id))
|
||||
.filter(collections_to_assets::asset_type.eq(AssetType::Chat))
|
||||
.filter(collections::deleted_at.is_null())
|
||||
.filter(collections_to_assets::deleted_at.is_null())
|
||||
.filter(collections::workspace_sharing.ne(WorkspaceSharing::None))
|
||||
.select((collections::organization_id, collections::workspace_sharing))
|
||||
.first::<(Uuid, WorkspaceSharing)>(&mut conn)
|
||||
.await
|
||||
.optional()?;
|
||||
|
||||
if let Some((org_id, _sharing_level)) = workspace_shared_collection {
|
||||
// Check if user is member of that organization
|
||||
if user_orgs.iter().any(|org| org.id == org_id) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -22,5 +22,5 @@ pub use user_lookup::find_user_by_email;
|
|||
pub use asset_access_checks::{
|
||||
check_permission_access, check_metric_dashboard_access, check_metric_chat_access,
|
||||
check_dashboard_chat_access, check_metric_collection_access, check_dashboard_collection_access,
|
||||
compute_effective_permission
|
||||
check_chat_collection_access, compute_effective_permission
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue