buster/apps/api/libs/sharing/src/asset_access_checks.rs

370 lines
14 KiB
Rust

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;
/// 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
///
/// # 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],
) -> 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;
}
}
// 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
/// 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<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,
) -> 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 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::<Uuid>(&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::<Uuid>(&mut conn)
.await
.optional()?;
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::*;
#[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
));
}
#[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
));
}
#[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
));
}
#[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
));
}
#[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
));
}
}