mirror of https://github.com/buster-so/buster.git
840 lines
33 KiB
Rust
840 lines
33 KiB
Rust
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, chats};
|
|
use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl, OptionalExtension};
|
|
use diesel_async::RunQueryDsl;
|
|
use middleware::OrganizationMembership;
|
|
use uuid::Uuid;
|
|
use std::cmp::Ordering;
|
|
|
|
/// Computes the effective permission level for a user on an asset by taking the maximum
|
|
/// of their direct permission and workspace sharing permission.
|
|
///
|
|
/// # Arguments
|
|
/// * `direct_permission` - The user's direct permission on the asset (if any)
|
|
/// * `workspace_sharing` - The workspace sharing level for the asset
|
|
/// * `organization_id` - UUID of the organization
|
|
/// * `organization_role_grants` - User's organization memberships
|
|
///
|
|
/// # Returns
|
|
/// * `Option<AssetPermissionRole>` - The highest permission level available to the user
|
|
pub fn compute_effective_permission(
|
|
direct_permission: Option<AssetPermissionRole>,
|
|
workspace_sharing: WorkspaceSharing,
|
|
organization_id: Uuid,
|
|
organization_role_grants: &[OrganizationMembership],
|
|
) -> Option<AssetPermissionRole> {
|
|
// First check if the user has WorkspaceAdmin or DataAdmin role for the organization
|
|
for org in organization_role_grants {
|
|
if org.id == organization_id
|
|
&& (org.role == UserOrganizationRole::WorkspaceAdmin
|
|
|| org.role == UserOrganizationRole::DataAdmin)
|
|
{
|
|
return Some(AssetPermissionRole::Owner);
|
|
}
|
|
}
|
|
|
|
// Compute workspace-granted permission
|
|
let workspace_permission = if workspace_sharing != WorkspaceSharing::None {
|
|
// Check if user is member of the organization
|
|
if organization_role_grants.iter().any(|org| org.id == organization_id) {
|
|
match workspace_sharing {
|
|
WorkspaceSharing::CanView => Some(AssetPermissionRole::CanView),
|
|
WorkspaceSharing::CanEdit => Some(AssetPermissionRole::CanEdit),
|
|
WorkspaceSharing::FullAccess => Some(AssetPermissionRole::FullAccess),
|
|
WorkspaceSharing::None => None,
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Return the highest permission level
|
|
match (direct_permission, workspace_permission) {
|
|
(Some(direct), Some(workspace)) => {
|
|
// Use the max method to get the higher permission
|
|
Some(direct.max(workspace))
|
|
}
|
|
(Some(direct), None) => Some(direct),
|
|
(None, Some(workspace)) => Some(workspace),
|
|
(None, None) => None,
|
|
}
|
|
}
|
|
|
|
/// Checks if a user has sufficient permissions based on organization roles and asset permissions.
|
|
///
|
|
/// # Arguments
|
|
/// * `current_permission_level` - Optional current permission level of the user for the asset
|
|
/// * `required_permission_level` - Required permission level to access the asset
|
|
/// * `organization_id` - UUID of the organization
|
|
/// * `organization_role_grants` - Array of tuples containing (UUID, UserOrganizationRole) for the user
|
|
/// * `workspace_sharing` - Workspace sharing level for the asset
|
|
///
|
|
/// # Returns
|
|
/// * `bool` - True if the user has sufficient permissions, false otherwise
|
|
pub fn check_permission_access(
|
|
current_permission_level: Option<AssetPermissionRole>,
|
|
required_permission_level: &[AssetPermissionRole],
|
|
organization_id: Uuid,
|
|
organization_role_grants: &[OrganizationMembership],
|
|
workspace_sharing: WorkspaceSharing,
|
|
) -> bool {
|
|
// First check if the user has WorkspaceAdmin or DataAdmin role for the organization
|
|
for org in organization_role_grants {
|
|
if org.id == organization_id
|
|
&& (org.role == UserOrganizationRole::WorkspaceAdmin
|
|
|| org.role == UserOrganizationRole::DataAdmin)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Check if user is member of the workspace and asset is shared
|
|
if workspace_sharing != WorkspaceSharing::None {
|
|
for org in organization_role_grants {
|
|
if org.id == organization_id {
|
|
// Map workspace sharing level to permission role
|
|
let workspace_permission = match workspace_sharing {
|
|
WorkspaceSharing::CanView => AssetPermissionRole::CanView,
|
|
WorkspaceSharing::CanEdit => AssetPermissionRole::CanEdit,
|
|
WorkspaceSharing::FullAccess => AssetPermissionRole::FullAccess,
|
|
WorkspaceSharing::None => unreachable!(),
|
|
};
|
|
|
|
if required_permission_level.contains(&workspace_permission) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then check if the user has the required permission level
|
|
if let Some(permission) = current_permission_level {
|
|
if required_permission_level.contains(&permission) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If none of the above conditions are met, return false
|
|
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,
|
|
/// 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
|
|
/// * `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 dashboard containing the metric, false otherwise
|
|
pub async fn check_metric_dashboard_access(
|
|
metric_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()),
|
|
)
|
|
})?;
|
|
|
|
// 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)),
|
|
)
|
|
.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)
|
|
.load::<Uuid>(&mut conn)
|
|
.await?;
|
|
|
|
if dashboard_ids.is_empty() {
|
|
return Ok(false);
|
|
}
|
|
|
|
// 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())
|
|
};
|
|
|
|
// 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())
|
|
};
|
|
|
|
// 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)
|
|
};
|
|
|
|
// 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.
|
|
///
|
|
/// This function is used to implement permission cascading from chats to metrics.
|
|
/// If a user has access to any chat containing the metric (either through direct permissions,
|
|
/// workspace sharing, or if the chat 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
|
|
/// * `user_orgs` - User's organization memberships
|
|
///
|
|
/// # 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,
|
|
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()),
|
|
)
|
|
})?;
|
|
|
|
// 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()?;
|
|
|
|
if has_public_chat_access.is_some() {
|
|
return Ok(true);
|
|
}
|
|
|
|
// Check if metric belongs to any workspace-shared chat
|
|
let workspace_shared_chat = 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::workspace_sharing.ne(WorkspaceSharing::None))
|
|
.select((database::schema::chats::organization_id, database::schema::chats::workspace_sharing))
|
|
.first::<(Uuid, WorkspaceSharing)>(&mut conn)
|
|
.await
|
|
.optional()?;
|
|
|
|
if let Some((org_id, _sharing_level)) = workspace_shared_chat {
|
|
// Check if user is member of that organization
|
|
if user_orgs.iter().any(|org| org.id == org_id) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// 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 (either through direct permissions,
|
|
/// workspace sharing, or if the chat is public), 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
|
|
/// * `user_orgs` - User's organization memberships
|
|
///
|
|
/// # 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,
|
|
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()),
|
|
)
|
|
})?;
|
|
|
|
// 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()?;
|
|
|
|
if has_public_chat_access.is_some() {
|
|
return Ok(true);
|
|
}
|
|
|
|
// Check if dashboard belongs to any workspace-shared chat
|
|
let workspace_shared_chat = 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::workspace_sharing.ne(WorkspaceSharing::None))
|
|
.select((database::schema::chats::organization_id, database::schema::chats::workspace_sharing))
|
|
.first::<(Uuid, WorkspaceSharing)>(&mut conn)
|
|
.await
|
|
.optional()?;
|
|
|
|
if let Some((org_id, _sharing_level)) = workspace_shared_chat {
|
|
// Check if user is member of that organization
|
|
if user_orgs.iter().any(|org| org.id == org_id) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Checks if a user has access to a metric through any associated collection.
|
|
///
|
|
/// This function is used to implement permission cascading from collections to metrics.
|
|
/// If a user has access to any collection containing the metric (either through direct permissions,
|
|
/// workspace sharing, or if the collection 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
|
|
/// * `user_orgs` - User's organization memberships
|
|
///
|
|
/// # Returns
|
|
/// * `Result<bool>` - True if the user has access to any collection containing the metric, false otherwise
|
|
pub async fn check_metric_collection_access(
|
|
metric_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 metric
|
|
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(metric_id))
|
|
.filter(collections_to_assets::asset_type.eq(AssetType::MetricFile))
|
|
.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);
|
|
}
|
|
|
|
// Note: Collections don't have publicly_accessible fields, only workspace_sharing
|
|
|
|
// Check if metric 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(metric_id))
|
|
.filter(collections_to_assets::asset_type.eq(AssetType::MetricFile))
|
|
.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)
|
|
}
|
|
|
|
/// Checks if a user has access to a dashboard through any associated collection.
|
|
///
|
|
/// This function is used to implement permission cascading from collections to dashboards.
|
|
/// If a user has access to any collection containing the dashboard (either through direct permissions,
|
|
/// workspace sharing, or if the collection is public), 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
|
|
/// * `user_orgs` - User's organization memberships
|
|
///
|
|
/// # Returns
|
|
/// * `Result<bool>` - True if the user has access to any collection containing the dashboard, false otherwise
|
|
pub async fn check_dashboard_collection_access(
|
|
dashboard_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 dashboard
|
|
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(dashboard_id))
|
|
.filter(collections_to_assets::asset_type.eq(AssetType::DashboardFile))
|
|
.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);
|
|
}
|
|
|
|
// Note: Collections don't have publicly_accessible fields, only workspace_sharing
|
|
|
|
// Check if dashboard 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(dashboard_id))
|
|
.filter(collections_to_assets::asset_type.eq(AssetType::DashboardFile))
|
|
.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)
|
|
}
|
|
|
|
/// 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::*;
|
|
|
|
#[test]
|
|
fn test_workspace_admin_access() {
|
|
let org_id = Uuid::new_v4();
|
|
let grants = vec![OrganizationMembership {
|
|
id: org_id,
|
|
role: UserOrganizationRole::WorkspaceAdmin
|
|
}];
|
|
|
|
assert!(check_permission_access(
|
|
None,
|
|
&[AssetPermissionRole::Owner],
|
|
org_id,
|
|
&grants,
|
|
WorkspaceSharing::None
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_data_admin_access() {
|
|
let org_id = Uuid::new_v4();
|
|
let grants = vec![OrganizationMembership {
|
|
id: org_id,
|
|
role: UserOrganizationRole::DataAdmin
|
|
}];
|
|
|
|
assert!(check_permission_access(
|
|
None,
|
|
&[AssetPermissionRole::Owner],
|
|
org_id,
|
|
&grants,
|
|
WorkspaceSharing::None
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_matching_permission_level() {
|
|
let org_id = Uuid::new_v4();
|
|
let grants = vec![OrganizationMembership {
|
|
id: org_id,
|
|
role: UserOrganizationRole::Viewer
|
|
}];
|
|
|
|
assert!(check_permission_access(
|
|
Some(AssetPermissionRole::CanEdit),
|
|
&[AssetPermissionRole::CanEdit],
|
|
org_id,
|
|
&grants,
|
|
WorkspaceSharing::None
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_insufficient_permissions() {
|
|
let org_id = Uuid::new_v4();
|
|
let grants = vec![OrganizationMembership {
|
|
id: org_id,
|
|
role: UserOrganizationRole::Viewer
|
|
}];
|
|
|
|
assert!(!check_permission_access(
|
|
Some(AssetPermissionRole::CanView),
|
|
&[AssetPermissionRole::CanEdit],
|
|
org_id,
|
|
&grants,
|
|
WorkspaceSharing::None
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_permissions() {
|
|
let org_id = Uuid::new_v4();
|
|
let grants = vec![OrganizationMembership {
|
|
id: org_id,
|
|
role: UserOrganizationRole::Viewer
|
|
}];
|
|
|
|
assert!(!check_permission_access(
|
|
None,
|
|
&[AssetPermissionRole::CanView],
|
|
org_id,
|
|
&grants,
|
|
WorkspaceSharing::None
|
|
));
|
|
}
|
|
}
|