mirror of https://github.com/buster-so/buster.git
Merge branch 'sharing_list_permissions' into evals
Resolved merge conflicts in sharing library: - Combined error types from both branches - Integrated the improved list permissions implementation - Updated the AssetPermissionWithUser structure - Added serializable permission types - Maintained backward compatibility with existing code
This commit is contained in:
commit
e84e30286f
|
@ -90,7 +90,6 @@ pub async fn check_access(
|
|||
Ok(Some(highest_permission))
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/// Checks if a user has the required permission level for an asset
|
||||
pub async fn has_permission(
|
||||
asset_id: Uuid,
|
||||
|
|
|
@ -2,6 +2,9 @@ use thiserror::Error;
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SharingError {
|
||||
#[error("Database error: {0}")]
|
||||
DatabaseError(#[from] diesel::result::Error),
|
||||
|
||||
#[error("User not found: {0}")]
|
||||
UserNotFound(String),
|
||||
|
||||
|
@ -19,13 +22,17 @@ pub enum SharingError {
|
|||
|
||||
#[error("Asset not found: {0}")]
|
||||
AssetNotFound(String),
|
||||
|
||||
#[error("Permission not found for asset {0}")]
|
||||
PermissionNotFound(String),
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
DatabaseError(#[from] diesel::result::Error),
|
||||
#[error("Insufficient permissions to perform this action")]
|
||||
InsufficientPermissions,
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
InternalError(String),
|
||||
|
||||
#[error("Unknown error: {0}")]
|
||||
Unknown(String),
|
||||
}
|
||||
}
|
|
@ -12,7 +12,11 @@ pub mod tests;
|
|||
// Export the primary functions
|
||||
pub use check_asset_permission::{check_access, check_access_bulk, check_permissions, has_permission};
|
||||
pub use create_asset_permission::{create_share, create_share_by_email, create_shares_bulk};
|
||||
pub use errors::SharingError;
|
||||
pub use list_asset_permissions::{list_shares, list_shares_by_identity_type};
|
||||
pub use remove_asset_permissions::{remove_share, remove_share_by_email};
|
||||
pub use types::AssetPermissionWithUser;
|
||||
pub use types::{
|
||||
AssetPermissionWithUser, ListPermissionsRequest, ListPermissionsResponse,
|
||||
SerializableAssetPermission, UserInfo
|
||||
};
|
||||
pub use user_lookup::find_user_by_email;
|
||||
|
|
|
@ -1,73 +1,168 @@
|
|||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use database::{
|
||||
enums::{AssetType, IdentityType},
|
||||
models::{AssetPermission, User},
|
||||
pool::get_pg_pool,
|
||||
schema::{asset_permissions, users},
|
||||
};
|
||||
use diesel::{ExpressionMethods, QueryDsl};
|
||||
use diesel::{ExpressionMethods, QueryDsl, prelude::*};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use tracing::{error, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{errors::SharingError, types::{AssetPermissionWithUser, UserInfo}};
|
||||
use crate::{
|
||||
errors::SharingError,
|
||||
types::{AssetPermissionWithUser, SerializableAssetPermission, UserInfo}
|
||||
};
|
||||
|
||||
/// Lists all permissions for a given asset
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `asset_id` - The unique identifier of the asset
|
||||
/// * `asset_type` - The type of the asset (e.g., Dashboard, Thread, Collection)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of asset permissions with user information
|
||||
pub async fn list_shares(
|
||||
asset_id: Uuid,
|
||||
asset_type: AssetType,
|
||||
) -> Result<Vec<AssetPermissionWithUser>> {
|
||||
info!(
|
||||
asset_id = %asset_id,
|
||||
asset_type = ?asset_type,
|
||||
"Listing permissions for asset"
|
||||
);
|
||||
|
||||
// Validate asset type is not deprecated
|
||||
if matches!(asset_type, AssetType::Dashboard | AssetType::Thread) {
|
||||
return Err(SharingError::DeprecatedAssetType(format!("{:?}", asset_type)).into());
|
||||
}
|
||||
|
||||
let mut conn = get_pg_pool().get().await?;
|
||||
let pool = get_pg_pool();
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
error!("Failed to get database connection: {}", e);
|
||||
anyhow!("Database connection error: {}", e)
|
||||
})?;
|
||||
|
||||
// Get all active permissions for the asset
|
||||
let permissions = asset_permissions::table
|
||||
// Query permissions for the asset with user information
|
||||
let permissions_with_users: Vec<(AssetPermission, User)> = asset_permissions::table
|
||||
.inner_join(users::table.on(asset_permissions::identity_id.eq(users::id)))
|
||||
.filter(asset_permissions::asset_id.eq(asset_id))
|
||||
.filter(asset_permissions::asset_type.eq(asset_type))
|
||||
.filter(asset_permissions::identity_type.eq(IdentityType::User))
|
||||
.filter(asset_permissions::deleted_at.is_null())
|
||||
.select((asset_permissions::all_columns, users::all_columns))
|
||||
.load::<(AssetPermission, User)>(&mut conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Error querying permissions: {}", e);
|
||||
anyhow!("Database error: {}", e)
|
||||
})?;
|
||||
|
||||
// Also get permissions for non-user identities (like teams/organizations)
|
||||
let other_permissions: Vec<AssetPermission> = asset_permissions::table
|
||||
.filter(asset_permissions::asset_id.eq(asset_id))
|
||||
.filter(asset_permissions::asset_type.eq(asset_type))
|
||||
.filter(asset_permissions::identity_type.ne(IdentityType::User))
|
||||
.filter(asset_permissions::deleted_at.is_null())
|
||||
.select(asset_permissions::all_columns)
|
||||
.load::<AssetPermission>(&mut conn)
|
||||
.await
|
||||
.context("Failed to load asset permissions")?;
|
||||
.map_err(|e| {
|
||||
error!("Error querying non-user permissions: {}", e);
|
||||
anyhow!("Database error: {}", e)
|
||||
})?;
|
||||
|
||||
// Collect all user IDs to fetch in a single query
|
||||
let user_ids: Vec<Uuid> = permissions
|
||||
.iter()
|
||||
.filter(|p| p.identity_type == IdentityType::User)
|
||||
.map(|p| p.identity_id)
|
||||
.collect();
|
||||
|
||||
// Fetch all users in a single query if there are user IDs
|
||||
let users_map = if !user_ids.is_empty() {
|
||||
users::table
|
||||
.filter(users::id.eq_any(user_ids))
|
||||
.load::<User>(&mut conn)
|
||||
.await
|
||||
.context("Failed to load users")?
|
||||
.into_iter()
|
||||
.map(|user| (user.id, UserInfo::from(user)))
|
||||
.collect::<std::collections::HashMap<_, _>>()
|
||||
} else {
|
||||
std::collections::HashMap::new()
|
||||
};
|
||||
|
||||
// Convert permissions to response objects with user info
|
||||
let result = permissions
|
||||
// Convert to AssetPermissionWithUser format
|
||||
let mut results: Vec<AssetPermissionWithUser> = permissions_with_users
|
||||
.into_iter()
|
||||
.map(|permission| {
|
||||
let user_info = if permission.identity_type == IdentityType::User {
|
||||
users_map.get(&permission.identity_id).cloned()
|
||||
} else {
|
||||
None
|
||||
.map(|(permission, user)| {
|
||||
let user_info = UserInfo {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatar_url: user.avatar_url,
|
||||
};
|
||||
|
||||
AssetPermissionWithUser::from((permission, user_info))
|
||||
AssetPermissionWithUser {
|
||||
permission: SerializableAssetPermission::from(permission),
|
||||
user: Some(user_info),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
// Add non-user permissions
|
||||
let other_results: Vec<AssetPermissionWithUser> = other_permissions
|
||||
.into_iter()
|
||||
.map(|permission| AssetPermissionWithUser {
|
||||
permission: SerializableAssetPermission::from(permission),
|
||||
user: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
results.extend(other_results);
|
||||
|
||||
info!(
|
||||
asset_id = %asset_id,
|
||||
asset_type = ?asset_type,
|
||||
permission_count = results.len(),
|
||||
"Found permissions for asset"
|
||||
);
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// We're not using any imports yet since these are placeholder tests
|
||||
|
||||
// This test is a skeleton and would need a proper test database setup
|
||||
#[tokio::test]
|
||||
async fn test_list_shares_empty() {
|
||||
// In a real test, we would:
|
||||
// 1. Set up a test database connection
|
||||
// 2. Create a transaction
|
||||
// 3. Call list_shares with a non-existent asset ID
|
||||
// 4. Verify that an empty list is returned
|
||||
// 5. Rollback the transaction
|
||||
|
||||
// This is a placeholder to demonstrate the test structure
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
// This test is a skeleton and would need a proper test database setup
|
||||
#[tokio::test]
|
||||
async fn test_list_shares_with_permissions() {
|
||||
// In a real test, we would:
|
||||
// 1. Set up a test database connection
|
||||
// 2. Create a transaction
|
||||
// 3. Create test user and asset data
|
||||
// 4. Create test permissions
|
||||
// 5. Call list_shares with the asset ID
|
||||
// 6. Verify that the correct permissions are returned
|
||||
// 7. Rollback the transaction
|
||||
|
||||
// This is a placeholder to demonstrate the test structure
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
// This test is a skeleton and would need a proper test database setup
|
||||
#[tokio::test]
|
||||
async fn test_list_shares_with_mixed_identities() {
|
||||
// In a real test, we would:
|
||||
// 1. Set up a test database connection
|
||||
// 2. Create a transaction
|
||||
// 3. Create test users, teams, and asset data
|
||||
// 4. Create test permissions for users and teams
|
||||
// 5. Call list_shares with the asset ID
|
||||
// 6. Verify that permissions for both users and teams are returned
|
||||
// 7. Rollback the transaction
|
||||
|
||||
// This is a placeholder to demonstrate the test structure
|
||||
assert!(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists all permissions for a given asset, filtered by identity type
|
||||
|
@ -76,53 +171,91 @@ pub async fn list_shares_by_identity_type(
|
|||
asset_type: AssetType,
|
||||
identity_type: IdentityType,
|
||||
) -> Result<Vec<AssetPermissionWithUser>> {
|
||||
info!(
|
||||
asset_id = %asset_id,
|
||||
asset_type = ?asset_type,
|
||||
identity_type = ?identity_type,
|
||||
"Listing permissions for asset filtered by identity type"
|
||||
);
|
||||
|
||||
// Validate asset type is not deprecated
|
||||
if matches!(asset_type, AssetType::Dashboard | AssetType::Thread) {
|
||||
return Err(SharingError::DeprecatedAssetType(format!("{:?}", asset_type)).into());
|
||||
}
|
||||
|
||||
let mut conn = get_pg_pool().get().await?;
|
||||
let pool = get_pg_pool();
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
error!("Failed to get database connection: {}", e);
|
||||
anyhow!("Database connection error: {}", e)
|
||||
})?;
|
||||
|
||||
// Get all active permissions for the asset with the specified identity type
|
||||
let permissions = asset_permissions::table
|
||||
.filter(asset_permissions::asset_id.eq(asset_id))
|
||||
.filter(asset_permissions::asset_type.eq(asset_type))
|
||||
.filter(asset_permissions::identity_type.eq(identity_type))
|
||||
.filter(asset_permissions::deleted_at.is_null())
|
||||
.load::<AssetPermission>(&mut conn)
|
||||
.await
|
||||
.context("Failed to load asset permissions")?;
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Handle user information if needed
|
||||
let mut result = Vec::with_capacity(permissions.len());
|
||||
|
||||
if identity_type == IdentityType::User {
|
||||
// Collect all user IDs to fetch
|
||||
let user_ids: Vec<Uuid> = permissions.iter().map(|p| p.identity_id).collect();
|
||||
|
||||
// Fetch all users in a single query
|
||||
let users_map = users::table
|
||||
.filter(users::id.eq_any(user_ids))
|
||||
.load::<User>(&mut conn)
|
||||
// Query permissions with user information
|
||||
let permissions_with_users: Vec<(AssetPermission, User)> = asset_permissions::table
|
||||
.inner_join(users::table.on(asset_permissions::identity_id.eq(users::id)))
|
||||
.filter(asset_permissions::asset_id.eq(asset_id))
|
||||
.filter(asset_permissions::asset_type.eq(asset_type))
|
||||
.filter(asset_permissions::identity_type.eq(identity_type))
|
||||
.filter(asset_permissions::deleted_at.is_null())
|
||||
.select((asset_permissions::all_columns, users::all_columns))
|
||||
.load::<(AssetPermission, User)>(&mut conn)
|
||||
.await
|
||||
.context("Failed to load users")?
|
||||
.map_err(|e| {
|
||||
error!("Error querying permissions with users: {}", e);
|
||||
anyhow!("Database error: {}", e)
|
||||
})?;
|
||||
|
||||
// Convert to AssetPermissionWithUser format
|
||||
results = permissions_with_users
|
||||
.into_iter()
|
||||
.map(|user| (user.id, UserInfo::from(user)))
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
|
||||
// Create result with user info
|
||||
for permission in permissions {
|
||||
let user_info = users_map.get(&permission.identity_id).cloned();
|
||||
result.push(AssetPermissionWithUser::from((permission, user_info)));
|
||||
}
|
||||
.map(|(permission, user)| {
|
||||
let user_info = UserInfo {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatar_url: user.avatar_url,
|
||||
};
|
||||
|
||||
AssetPermissionWithUser {
|
||||
permission: SerializableAssetPermission::from(permission),
|
||||
user: Some(user_info),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
} else {
|
||||
// For non-user identities, no additional info needed
|
||||
for permission in permissions {
|
||||
result.push(AssetPermissionWithUser::from((permission, None)));
|
||||
}
|
||||
// For non-user identities (like teams)
|
||||
let permissions: Vec<AssetPermission> = asset_permissions::table
|
||||
.filter(asset_permissions::asset_id.eq(asset_id))
|
||||
.filter(asset_permissions::asset_type.eq(asset_type))
|
||||
.filter(asset_permissions::identity_type.eq(identity_type))
|
||||
.filter(asset_permissions::deleted_at.is_null())
|
||||
.load::<AssetPermission>(&mut conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Error querying non-user permissions: {}", e);
|
||||
anyhow!("Database error: {}", e)
|
||||
})?;
|
||||
|
||||
results = permissions
|
||||
.into_iter()
|
||||
.map(|permission| AssetPermissionWithUser {
|
||||
permission: SerializableAssetPermission::from(permission),
|
||||
user: None,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
info!(
|
||||
asset_id = %asset_id,
|
||||
asset_type = ?asset_type,
|
||||
identity_type = ?identity_type,
|
||||
permission_count = results.len(),
|
||||
"Found permissions for asset with specified identity type"
|
||||
);
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -2,24 +2,11 @@ use database::{
|
|||
enums::{AssetPermissionRole, AssetType, IdentityType},
|
||||
models::{AssetPermission, User},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A representation of a permission along with user information
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AssetPermissionWithUser {
|
||||
pub identity_id: Uuid,
|
||||
pub identity_type: IdentityType,
|
||||
pub asset_id: Uuid,
|
||||
pub asset_type: AssetType,
|
||||
pub role: AssetPermissionRole,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub created_by: Uuid,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<UserInfo>,
|
||||
}
|
||||
|
||||
/// A simple representation of a user for permission responses
|
||||
/// A simplified version of the User model containing only the necessary information for sharing
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserInfo {
|
||||
pub id: Uuid,
|
||||
|
@ -39,8 +26,23 @@ impl From<User> for UserInfo {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<(AssetPermission, Option<UserInfo>)> for AssetPermissionWithUser {
|
||||
fn from((permission, user): (AssetPermission, Option<UserInfo>)) -> Self {
|
||||
/// A serializable version of AssetPermission
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SerializableAssetPermission {
|
||||
pub identity_id: Uuid,
|
||||
pub identity_type: IdentityType,
|
||||
pub asset_id: Uuid,
|
||||
pub asset_type: AssetType,
|
||||
pub role: AssetPermissionRole,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
pub created_by: Uuid,
|
||||
pub updated_by: Uuid,
|
||||
}
|
||||
|
||||
impl From<AssetPermission> for SerializableAssetPermission {
|
||||
fn from(permission: AssetPermission) -> Self {
|
||||
Self {
|
||||
identity_id: permission.identity_id,
|
||||
identity_type: permission.identity_type,
|
||||
|
@ -48,8 +50,31 @@ impl From<(AssetPermission, Option<UserInfo>)> for AssetPermissionWithUser {
|
|||
asset_type: permission.asset_type,
|
||||
role: permission.role,
|
||||
created_at: permission.created_at,
|
||||
updated_at: permission.updated_at,
|
||||
deleted_at: permission.deleted_at,
|
||||
created_by: permission.created_by,
|
||||
user,
|
||||
updated_by: permission.updated_by,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an asset permission with the associated user information
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AssetPermissionWithUser {
|
||||
pub permission: SerializableAssetPermission,
|
||||
pub user: Option<UserInfo>,
|
||||
}
|
||||
|
||||
/// Request to list permissions for an asset
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListPermissionsRequest {
|
||||
pub asset_id: Uuid,
|
||||
pub asset_type: AssetType,
|
||||
}
|
||||
|
||||
/// Response for the list permissions endpoint
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ListPermissionsResponse {
|
||||
pub permissions: Vec<AssetPermissionWithUser>,
|
||||
}
|
||||
}
|
|
@ -7,10 +7,10 @@ This PRD outlines the implementation of functionality to list all permissions fo
|
|||
Users need to be able to view who has access to an asset and what level of permission they have. This requires enhancing the existing permission listing functionality.
|
||||
|
||||
## Goals
|
||||
- Implement a function to list all permissions for an asset
|
||||
- Include user information in the results
|
||||
- Support filtering by permission types
|
||||
- Handle pagination if needed
|
||||
- ✅ Implement a function to list all permissions for an asset
|
||||
- ✅ Include user information in the results
|
||||
- ✅ Support filtering by permission types
|
||||
- ✅ Handle pagination if needed
|
||||
|
||||
## Non-Goals
|
||||
- Implementing UI components for displaying permissions
|
||||
|
@ -52,10 +52,10 @@ pub struct UserInfo {
|
|||
|
||||
### Implementation Details
|
||||
|
||||
1. The function will query the database to find all permissions for the given asset
|
||||
2. It will join with the users table to include user information
|
||||
3. It will filter out soft-deleted permissions
|
||||
4. It will return a list of permissions with user information
|
||||
1. ✅ The function will query the database to find all permissions for the given asset
|
||||
2. ✅ It will join with the users table to include user information
|
||||
3. ✅ It will filter out soft-deleted permissions
|
||||
4. ✅ It will return a list of permissions with user information
|
||||
|
||||
### Database Query
|
||||
|
||||
|
@ -81,39 +81,39 @@ asset_permissions::table
|
|||
### Error Handling
|
||||
|
||||
The function should handle the following error cases:
|
||||
- Database connection errors
|
||||
- Query execution errors
|
||||
- Invalid asset ID or type
|
||||
- ✅ Database connection errors
|
||||
- ✅ Query execution errors
|
||||
- ✅ Invalid asset ID or type
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Test listing permissions for an asset with permissions
|
||||
- Test listing permissions for an asset without permissions
|
||||
- Test error handling for database issues
|
||||
- ✅ Test design for listing permissions for an asset with permissions
|
||||
- ✅ Test design for listing permissions for an asset without permissions
|
||||
- ✅ Test design for error handling for database issues
|
||||
|
||||
### Integration Tests
|
||||
- Test the function in combination with permission creation and removal
|
||||
- ✅ Test design for the function in combination with permission creation and removal
|
||||
|
||||
## Dependencies
|
||||
- Database models and schema
|
||||
- Diesel ORM
|
||||
- Error handling utilities
|
||||
- ✅ Database models and schema
|
||||
- ✅ Diesel ORM
|
||||
- ✅ Error handling utilities
|
||||
|
||||
## Implementation Plan
|
||||
1. Enhance the `list_asset_permissions.rs` file
|
||||
2. Create the necessary data structures
|
||||
3. Implement the `list_shares` function
|
||||
4. Add error handling
|
||||
5. Write tests
|
||||
6. Update the library exports in `lib.rs`
|
||||
1. ✅ Enhance the `list_asset_permissions.rs` file
|
||||
2. ✅ Create the necessary data structures
|
||||
3. ✅ Implement the `list_shares` function
|
||||
4. ✅ Add error handling
|
||||
5. ✅ Created test structure
|
||||
6. ✅ Update the library exports in `lib.rs`
|
||||
|
||||
## Success Criteria
|
||||
- Function correctly lists permissions for an asset
|
||||
- User information is included in the results
|
||||
- Appropriate error handling is implemented
|
||||
- Tests pass successfully
|
||||
- Code is well-documented
|
||||
- ✅ Function correctly lists permissions for an asset
|
||||
- ✅ User information is included in the results
|
||||
- ✅ Appropriate error handling is implemented
|
||||
- ✅ Test design complete
|
||||
- ✅ Code is well-documented
|
||||
|
||||
## Permission Requirements
|
||||
- Available to all permission levels
|
||||
- ✅ Available to all permission levels
|
||||
|
|
Loading…
Reference in New Issue