buster/api/libs/sharing/src/check_asset_permission.rs

341 lines
13 KiB
Rust
Raw Normal View History

2025-03-12 05:09:19 +08:00
use anyhow::{Context, Result};
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType},
pool::get_pg_pool,
schema::{asset_permissions, teams_to_users},
};
use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl};
use diesel_async::RunQueryDsl;
use uuid::Uuid;
2025-03-19 23:41:29 +08:00
use crate::errors::SharingError;
2025-03-12 05:09:19 +08:00
/// Input for checking a single asset permission
#[derive(Debug, Clone)]
pub struct CheckPermissionInput {
pub asset_id: Uuid,
pub asset_type: AssetType,
pub identity_id: Uuid,
pub identity_type: IdentityType,
}
/// Result of a permission check
#[derive(Debug, Clone)]
pub struct AssetPermissionResult {
pub asset_id: Uuid,
pub asset_type: AssetType,
pub role: Option<AssetPermissionRole>,
}
/// Checks if a user has access to a resource and returns their role
pub async fn check_access(
asset_id: Uuid,
asset_type: AssetType,
identity_id: Uuid,
identity_type: IdentityType,
) -> Result<Option<AssetPermissionRole>> {
// Validate asset type is not deprecated
if matches!(asset_type, AssetType::Dashboard | AssetType::Thread) {
2025-03-19 23:41:29 +08:00
return Err(SharingError::DeprecatedAssetType(format!("{:?}", asset_type)).into());
2025-03-12 05:09:19 +08:00
}
let mut conn = get_pg_pool().get().await?;
let permissions = match identity_type {
IdentityType::User => {
// For users, we need to check both direct permissions and team permissions
asset_permissions::table
.left_join(
teams_to_users::table
.on(asset_permissions::identity_id.eq(teams_to_users::team_id)),
)
.select(asset_permissions::role)
.filter(
asset_permissions::identity_id
.eq(&identity_id)
.or(teams_to_users::user_id.eq(&identity_id)),
)
.filter(asset_permissions::asset_id.eq(&asset_id))
.filter(asset_permissions::asset_type.eq(&asset_type))
.filter(asset_permissions::deleted_at.is_null())
.load::<AssetPermissionRole>(&mut conn)
.await
.context("Failed to query asset permissions")?
}
_ => {
// For other identity types, just check direct permissions
asset_permissions::table
.select(asset_permissions::role)
.filter(asset_permissions::identity_id.eq(&identity_id))
.filter(asset_permissions::identity_type.eq(&identity_type))
.filter(asset_permissions::asset_id.eq(&asset_id))
.filter(asset_permissions::asset_type.eq(&asset_type))
.filter(asset_permissions::deleted_at.is_null())
.load::<AssetPermissionRole>(&mut conn)
.await
.context("Failed to query asset permissions")?
}
};
if permissions.is_empty() {
return Ok(None);
}
// Find the highest permission level
let highest_permission = permissions
.into_iter()
.reduce(|acc, role| acc.max(role))
.unwrap();
Ok(Some(highest_permission))
}
2025-03-20 00:03:41 +08:00
/// Checks if a user has the required permission level for an asset
2025-03-19 23:41:29 +08:00
pub async fn has_permission(
asset_id: Uuid,
asset_type: AssetType,
identity_id: Uuid,
identity_type: IdentityType,
required_role: AssetPermissionRole,
) -> Result<bool> {
let user_role = check_access(asset_id, asset_type, identity_id, identity_type).await?;
match user_role {
Some(role) => {
2025-03-20 00:03:41 +08:00
// Check if user's role is sufficient for the required role based on the permission hierarchy
Ok(match (role, required_role) {
// Owner can do anything
(AssetPermissionRole::Owner, _) => true,
// FullAccess can do anything except Owner actions
(AssetPermissionRole::FullAccess, AssetPermissionRole::Owner) => false,
(AssetPermissionRole::FullAccess, _) => true,
// CanEdit can edit and view
(AssetPermissionRole::CanEdit, AssetPermissionRole::Owner | AssetPermissionRole::FullAccess) => false,
(AssetPermissionRole::CanEdit, AssetPermissionRole::CanEdit | AssetPermissionRole::CanFilter | AssetPermissionRole::CanView | AssetPermissionRole::Editor | AssetPermissionRole::Viewer) => true,
2025-03-19 23:41:29 +08:00
// CanFilter can filter and view
2025-03-20 00:03:41 +08:00
(AssetPermissionRole::CanFilter, AssetPermissionRole::Owner | AssetPermissionRole::FullAccess | AssetPermissionRole::CanEdit | AssetPermissionRole::Editor) => false,
(AssetPermissionRole::CanFilter, AssetPermissionRole::CanFilter | AssetPermissionRole::CanView | AssetPermissionRole::Viewer) => true,
2025-03-19 23:41:29 +08:00
// CanView can only view
2025-03-20 00:03:41 +08:00
(AssetPermissionRole::CanView, AssetPermissionRole::CanView | AssetPermissionRole::Viewer) => true,
(AssetPermissionRole::CanView, _) => false,
// Editor (legacy) can edit and view
(AssetPermissionRole::Editor, AssetPermissionRole::Owner | AssetPermissionRole::FullAccess) => false,
(AssetPermissionRole::Editor, AssetPermissionRole::CanEdit | AssetPermissionRole::CanFilter | AssetPermissionRole::CanView | AssetPermissionRole::Editor | AssetPermissionRole::Viewer) => true,
// Viewer (legacy) can only view
(AssetPermissionRole::Viewer, AssetPermissionRole::CanView | AssetPermissionRole::Viewer) => true,
(AssetPermissionRole::Viewer, _) => false,
})
2025-03-19 23:41:29 +08:00
}
2025-03-20 00:03:41 +08:00
None => Ok(false),
2025-03-19 23:41:29 +08:00
}
}
2025-03-20 00:03:41 +08:00
/// Simpler structure for holding permission results when checking in bulk
#[derive(Debug, Clone)]
pub struct AssetPermissionEntry {
pub asset_id: Uuid,
pub asset_type: AssetType,
pub role: Option<AssetPermissionRole>,
}
2025-03-12 05:09:19 +08:00
/// Checks permissions for multiple assets in bulk
pub async fn check_access_bulk(
inputs: Vec<CheckPermissionInput>,
2025-03-20 00:03:41 +08:00
) -> Result<Vec<AssetPermissionEntry>> {
2025-03-12 05:09:19 +08:00
if inputs.is_empty() {
2025-03-19 23:41:29 +08:00
return Ok(Vec::new());
2025-03-12 05:09:19 +08:00
}
// Validate no deprecated asset types
if inputs
.iter()
.any(|input| matches!(input.asset_type, AssetType::Dashboard | AssetType::Thread))
{
2025-03-19 23:41:29 +08:00
return Err(SharingError::DeprecatedAssetType("Cannot check permissions for deprecated asset types".to_string()).into());
2025-03-12 05:09:19 +08:00
}
// Group inputs by identity type to optimize queries
let mut user_inputs = Vec::new();
let mut other_identity_inputs = Vec::new();
for input in inputs {
if input.identity_type == IdentityType::User {
user_inputs.push(input);
} else {
other_identity_inputs.push(input);
}
}
2025-03-19 23:41:29 +08:00
let mut results = Vec::new();
2025-03-12 05:09:19 +08:00
// Process user inputs
if !user_inputs.is_empty() {
let mut conn = get_pg_pool().get().await?;
let user_id = user_inputs[0].identity_id;
2025-03-19 23:41:29 +08:00
2025-03-20 00:03:41 +08:00
// Process each input separately (we could optimize this in the future)
for input in user_inputs {
// For users, we need to check both direct permissions and team permissions
let permissions = asset_permissions::table
2025-03-12 05:09:19 +08:00
.left_join(
teams_to_users::table
.on(asset_permissions::identity_id.eq(teams_to_users::team_id)),
)
2025-03-19 23:41:29 +08:00
.select(asset_permissions::role)
2025-03-12 05:09:19 +08:00
.filter(
asset_permissions::identity_id
.eq(&user_id)
.or(teams_to_users::user_id.eq(&user_id)),
)
2025-03-19 23:41:29 +08:00
.filter(asset_permissions::asset_id.eq(&input.asset_id))
.filter(asset_permissions::asset_type.eq(&input.asset_type))
2025-03-12 05:09:19 +08:00
.filter(asset_permissions::deleted_at.is_null())
2025-03-19 23:41:29 +08:00
.load::<AssetPermissionRole>(&mut conn)
2025-03-12 05:09:19 +08:00
.await
2025-03-20 00:03:41 +08:00
.context("Failed to query asset permissions")?;
2025-03-12 05:09:19 +08:00
2025-03-20 00:03:41 +08:00
let highest_role = if permissions.is_empty() {
None
} else {
Some(
permissions
.into_iter()
.reduce(|acc, role| acc.max(role))
.unwrap(),
)
};
2025-03-12 05:09:19 +08:00
2025-03-20 00:03:41 +08:00
results.push(AssetPermissionEntry {
asset_id: input.asset_id,
asset_type: input.asset_type,
role: highest_role,
});
2025-03-12 05:09:19 +08:00
}
}
// Process other identity inputs
for input in other_identity_inputs {
let mut conn = get_pg_pool().get().await?;
2025-03-20 00:03:41 +08:00
// For other identity types, just check direct permissions
let permissions = asset_permissions::table
2025-03-12 05:09:19 +08:00
.select(asset_permissions::role)
.filter(asset_permissions::identity_id.eq(&input.identity_id))
.filter(asset_permissions::identity_type.eq(&input.identity_type))
.filter(asset_permissions::asset_id.eq(&input.asset_id))
.filter(asset_permissions::asset_type.eq(&input.asset_type))
.filter(asset_permissions::deleted_at.is_null())
.load::<AssetPermissionRole>(&mut conn)
.await
.context("Failed to query asset permissions")?;
2025-03-20 00:03:41 +08:00
let highest_role = if permissions.is_empty() {
None
} else {
Some(
permissions
.into_iter()
.reduce(|acc, role| acc.max(role))
.unwrap(),
)
};
2025-03-12 05:09:19 +08:00
2025-03-20 00:03:41 +08:00
results.push(AssetPermissionEntry {
asset_id: input.asset_id,
asset_type: input.asset_type,
role: highest_role,
});
2025-03-12 05:09:19 +08:00
}
Ok(results)
}
/// Checks permissions for multiple assets and returns a structured result
pub async fn check_permissions(
inputs: Vec<CheckPermissionInput>,
) -> Result<Vec<AssetPermissionResult>> {
2025-03-20 00:03:41 +08:00
let permissions_entries = check_access_bulk(inputs.clone()).await?;
2025-03-12 05:09:19 +08:00
2025-03-20 00:03:41 +08:00
// Convert entries to results
let results = permissions_entries
2025-03-12 05:09:19 +08:00
.into_iter()
2025-03-20 00:03:41 +08:00
.map(|entry| {
2025-03-12 05:09:19 +08:00
AssetPermissionResult {
2025-03-20 00:03:41 +08:00
asset_id: entry.asset_id,
asset_type: entry.asset_type,
role: entry.role,
2025-03-12 05:09:19 +08:00
}
})
.collect();
Ok(results)
}
2025-03-19 23:41:29 +08:00
#[cfg(test)]
mod tests {
use super::*;
use database::enums::{AssetPermissionRole, AssetType, IdentityType};
use uuid::Uuid;
#[tokio::test]
async fn test_has_permission_logic() {
// Test owner can do anything
let has_permission = has_permission_logic(AssetPermissionRole::Owner, AssetPermissionRole::CanView);
assert!(has_permission);
// Test full access can do anything
let has_permission = has_permission_logic(AssetPermissionRole::FullAccess, AssetPermissionRole::CanEdit);
assert!(has_permission);
// Test can_edit can filter and view
let has_permission = has_permission_logic(AssetPermissionRole::CanEdit, AssetPermissionRole::CanFilter);
assert!(has_permission);
// Test can_filter cannot edit
let has_permission = has_permission_logic(AssetPermissionRole::CanFilter, AssetPermissionRole::CanEdit);
assert!(!has_permission);
// Test editor can view
let has_permission = has_permission_logic(AssetPermissionRole::Editor, AssetPermissionRole::Viewer);
assert!(has_permission);
// Test viewer cannot edit
let has_permission = has_permission_logic(AssetPermissionRole::Viewer, AssetPermissionRole::Editor);
assert!(!has_permission);
}
// Helper function to test permission logic without database
fn has_permission_logic(user_role: AssetPermissionRole, required_role: AssetPermissionRole) -> bool {
// Special case for Owner and FullAccess, which can do anything
if user_role == AssetPermissionRole::Owner || user_role == AssetPermissionRole::FullAccess {
return true;
}
// For other roles, we need to compare them
match (user_role, required_role) {
// Owner and FullAccess can do anything (handled above)
// CanEdit can edit, filter and view
(AssetPermissionRole::CanEdit, AssetPermissionRole::CanEdit) |
(AssetPermissionRole::CanEdit, AssetPermissionRole::CanFilter) |
(AssetPermissionRole::CanEdit, AssetPermissionRole::CanView) => true,
// CanFilter can filter and view
(AssetPermissionRole::CanFilter, AssetPermissionRole::CanFilter) |
(AssetPermissionRole::CanFilter, AssetPermissionRole::CanView) => true,
// CanView can only view
(AssetPermissionRole::CanView, AssetPermissionRole::CanView) => true,
// Editor can edit and view
(AssetPermissionRole::Editor, AssetPermissionRole::Editor) |
(AssetPermissionRole::Editor, AssetPermissionRole::Viewer) => true,
// Viewer can only view
(AssetPermissionRole::Viewer, AssetPermissionRole::Viewer) => true,
// All other combinations are not permitted
_ => false,
}
}
}