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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|