buster/api/prds/active/api_dashboard_permission_ch...

604 lines
22 KiB
Markdown

# Dashboard Permission Checks
## Problem Statement
The dashboard-related handlers in `@libs/handlers/src/dashboards` currently lack standardized permission checks using the `@libs/sharing` library. While some permission logic may exist, it doesn't consistently use the `check_asset_permission.rs` functions and doesn't properly handle organization admin access to dashboard resources.
Specific issues include:
- Inconsistent permission checks across dashboard handlers
- Hardcoded permission values in responses (e.g., `permission: AssetPermissionRole::Owner`)
- No implementation of the CanFilter role which is specifically relevant for dashboards
- No automatic elevated access for workspace and data admins
- Risk of unauthorized access, modification, or deletion of dashboards
- No clear error handling for permission denied cases
These issues affect the security and consistency of the application and need to be addressed to ensure proper access control across all dashboard resources.
### Current Limitations
- Dashboard handlers may use ad-hoc permission checks or none at all
- No organization admin check for automatic access elevation
- No implementation of the CanFilter permission level which is especially important for dashboards
- Inconsistent error handling for permission failures
- Lack of proper permission filtering for list operations
### Impact
- User Impact: Users may have incorrect access to dashboards (too much or too little)
- System Impact: Security vulnerabilities and inconsistent behavior
- Business Impact: Potential unauthorized access to business-critical dashboards and inability to properly administer them
## Requirements
### Functional Requirements
#### Core Functionality
- Implement permission checks in all dashboard handlers
- Details: Add permission checks at the beginning of each handler function
- Acceptance Criteria: All dashboard handlers properly check permissions before performing operations
- Dependencies: Sharing library, admin check utility
- Enforce correct permission levels for different operations
- Details: Map operations to appropriate permission levels (view, filter, edit, delete)
- Acceptance Criteria: Each operation requires the correct minimum permission level
- Dependencies: `AssetPermissionRole` enum
- Implement proper error handling
- Details: Return appropriate error messages for permission denied cases
- Acceptance Criteria: Consistent, secure error handling across all dashboard handlers
- Dependencies: None
- Support CanFilter permission level
- Details: Properly handle the CanFilter permission which allows filtering but not editing
- Acceptance Criteria: Users with CanFilter can view and apply filters but not modify the dashboard
- Dependencies: None
#### Handler-Specific Requirements
- get_dashboard_handler
- Details: Require at least CanView permission
- Acceptance Criteria: Users with at least CanView permission can access dashboard details
- Dependencies: None
- delete_dashboard_handler
- Details: Require FullAccess or Owner permission
- Acceptance Criteria: Only users with FullAccess or Owner permission can delete dashboards
- Dependencies: None
- update_dashboard_handler
- Details: Require at least CanEdit permission
- Acceptance Criteria: Users with at least CanEdit permission can update dashboard structure
- Dependencies: None
- list_dashboard_handler
- Details: Filter results based on user's permissions
- Acceptance Criteria: Only dashboards the user has at least CanView permission for are returned
- Dependencies: None
- add_asset_to_dashboard_handler
- Details: Require at least CanEdit permission on the dashboard
- Acceptance Criteria: Only users with at least CanEdit permission can add assets to dashboards
- Dependencies: None
- sharing_endpoint_handlers
- Details: Require FullAccess or Owner permission
- Acceptance Criteria: Only users with FullAccess or Owner permission can modify sharing settings
- Dependencies: None
### Non-Functional Requirements
- Performance Requirements
- Permission checks should add minimal overhead to handlers (<10ms)
- Security Requirements
- Permission checks must happen before any data access or modification
- Error messages must not reveal sensitive information
- Maintainability Requirements
- All handlers should use consistent permission checking patterns
- Code should be well-documented for future maintenance
## Technical Design
### System Architecture
```mermaid
graph TD
A[Dashboard Handler] --> B[Permission Check]
B -->|Has Permission| C[Proceed with Handler Logic]
B -->|No Permission| D[Return Permission Error]
C --> E[Execute Database Operations]
E --> F[Return Success Response]
D --> G[Return Error Response]
```
### Core Components
#### Component 1: Permission Check Utility for Dashboard Handlers
```rust
/// Verifies a user has sufficient permissions for a dashboard operation
///
/// # Arguments
/// * `dashboard_id` - The ID of the dashboard to check
/// * `user_id` - The ID of the user requesting access
/// * `required_role` - The minimum role required for the operation
///
/// # Returns
/// * `Result<()>` - Ok if user has permission, Error otherwise
async fn verify_dashboard_permission(
dashboard_id: &Uuid,
user_id: &Uuid,
required_role: AssetPermissionRole,
) -> Result<()> {
// Get the organization ID for this dashboard
let org_id = dashboard_files::table
.filter(dashboard_files::id.eq(dashboard_id))
.filter(dashboard_files::deleted_at.is_null())
.select(dashboard_files::organization_id)
.first::<Uuid>(&mut get_pg_pool().get().await?)
.await
.map_err(|e| anyhow!("Failed to find dashboard: {}", e))?;
// Check if user is an org admin
if is_user_org_admin(user_id, &org_id).await? {
// Admins get everything except Owner permissions
if required_role != AssetPermissionRole::Owner {
return Ok(());
}
}
// Check regular permissions
let has_access = has_permission(
*dashboard_id,
AssetType::DashboardFile,
*user_id,
IdentityType::User,
required_role,
)
.await?;
if has_access {
Ok(())
} else {
Err(anyhow!("Insufficient permissions for dashboard operation"))
}
}
/// Get the actual permission role a user has for a dashboard
///
/// # Arguments
/// * `dashboard_id` - The ID of the dashboard to check
/// * `user_id` - The ID of the user to check permissions for
///
/// # Returns
/// * `Result<AssetPermissionRole>` - The highest permission role the user has
async fn get_dashboard_permission_role(
dashboard_id: &Uuid,
user_id: &Uuid,
) -> Result<AssetPermissionRole> {
// Get the organization ID for this dashboard
let org_id = dashboard_files::table
.filter(dashboard_files::id.eq(dashboard_id))
.filter(dashboard_files::deleted_at.is_null())
.select(dashboard_files::organization_id)
.first::<Uuid>(&mut get_pg_pool().get().await?)
.await
.map_err(|e| anyhow!("Failed to find dashboard: {}", e))?;
// Check if user is an org admin
if is_user_org_admin(user_id, &org_id).await? {
// Admins get FullAccess
return Ok(AssetPermissionRole::FullAccess);
}
// Check regular permissions
let user_role = check_access(
*dashboard_id,
AssetType::DashboardFile,
*user_id,
IdentityType::User,
)
.await?;
// If dashboard was created by the user, they're the owner
let is_owner = dashboard_files::table
.filter(dashboard_files::id.eq(dashboard_id))
.filter(dashboard_files::created_by.eq(user_id))
.first::<QueryableDashboardFile>(&mut get_pg_pool().get().await?)
.await
.is_ok();
if is_owner {
Ok(AssetPermissionRole::Owner)
} else {
Ok(user_role.unwrap_or(AssetPermissionRole::CanView))
}
}
```
#### Component 2: Modified get_dashboard_handler
```rust
pub async fn get_dashboard_handler(
dashboard_id: &Uuid,
user_id: &Uuid,
version_number: Option<i32>,
) -> Result<BusterDashboardResponse> {
// Verify user has at least CanView permission
verify_dashboard_permission(dashboard_id, user_id, AssetPermissionRole::CanView).await?;
// Existing handler logic continues below...
// ...
// Get the actual permission role for accurate response
let permission_role = get_dashboard_permission_role(dashboard_id, user_id).await?;
Ok(BusterDashboardResponse {
access: permission_role, // Use actual role instead of hardcoded Owner
metrics,
dashboard,
permission: permission_role, // Use actual role instead of hardcoded Owner
public_password: None,
collections: vec![],
// Other fields...
individual_permissions,
publicly_accessible: dashboard_file.publicly_accessible,
public_expiry_date: dashboard_file.public_expiry_date,
public_enabled_by: public_enabled_by_user,
versions,
})
}
```
#### Component 3: Modified delete_dashboard_handler
```rust
pub async fn delete_dashboard_handler(
dashboard_id: &Uuid,
user_id: &Uuid,
) -> Result<()> {
// Verify user has FullAccess permission (required for deletion)
verify_dashboard_permission(dashboard_id, user_id, AssetPermissionRole::FullAccess).await?;
// Existing handler logic continues below...
// ...
}
```
#### Component 4: Modified update_dashboard_handler
```rust
pub async fn update_dashboard_handler(
dashboard_id: &Uuid,
user_id: &Uuid,
request: UpdateDashboardRequest,
) -> Result<BusterDashboardResponse> {
// Verify user has at least CanEdit permission
verify_dashboard_permission(dashboard_id, user_id, AssetPermissionRole::CanEdit).await?;
// Existing handler logic continues below...
// ...
// Get the actual permission role for accurate response
let permission_role = get_dashboard_permission_role(dashboard_id, user_id).await?;
// Return with proper permission
Ok(BusterDashboardResponse {
// ... other fields ...
permission: permission_role, // Use actual role instead of hardcoded Owner
// ... other fields ...
})
}
```
#### Component 5: Modified list_dashboard_handler
```rust
pub async fn list_dashboard_handler(user_id: &Uuid) -> Result<Vec<DashboardSummary>> {
// For list operations, we'll filter by permissions rather than block entirely
let mut conn = get_pg_pool().get().await?;
// Get organization ID for this user
let org_id = match get_user_organization_id(user_id).await {
Ok(id) => id,
Err(_) => return Ok(Vec::new()), // No organization, no dashboards
};
// Check if user is an org admin
let is_admin = is_user_org_admin(user_id, &org_id).await?;
// If admin, return all dashboards in organization
if is_admin {
let org_dashboards = dashboard_files::table
.filter(dashboard_files::organization_id.eq(org_id))
.filter(dashboard_files::deleted_at.is_null())
// ... additional query logic ...
.load::<QueryableDashboardFile>(&mut conn)
.await?;
// Transform to summary format
let summaries = org_dashboards
.into_iter()
.map(|db| DashboardSummary {
id: db.id,
name: db.name,
// ... other fields ...
permission: AssetPermissionRole::FullAccess, // Admin gets FullAccess
})
.collect();
return Ok(summaries);
}
// Otherwise, get dashboards based on explicit permissions
// Get all dashboards the user has created (owner by default)
let mut user_dashboards = dashboard_files::table
.filter(dashboard_files::created_by.eq(user_id))
.filter(dashboard_files::deleted_at.is_null())
// ... additional query logic ...
.load::<QueryableDashboardFile>(&mut conn)
.await?;
// Get all dashboards where the user has been granted permissions
let shared_dashboards = asset_permissions::table
.inner_join(dashboard_files::table.on(dashboard_files::id.eq(asset_permissions::asset_id)))
.filter(asset_permissions::identity_id.eq(user_id))
.filter(asset_permissions::identity_type.eq(IdentityType::User))
.filter(asset_permissions::asset_type.eq(AssetType::DashboardFile))
.filter(asset_permissions::deleted_at.is_null())
.filter(dashboard_files::deleted_at.is_null())
// ... additional query logic ...
.select((
dashboard_files::all_columns,
asset_permissions::role,
))
.load::<(QueryableDashboardFile, AssetPermissionRole)>(&mut conn)
.await?;
// Transform user dashboards to summary format
let mut summaries: Vec<DashboardSummary> = user_dashboards
.into_iter()
.map(|db| DashboardSummary {
id: db.id,
name: db.name,
// ... other fields ...
permission: AssetPermissionRole::Owner, // User is owner
})
.collect();
// Add shared dashboards to summaries
let shared_summaries: Vec<DashboardSummary> = shared_dashboards
.into_iter()
.map(|(db, role)| DashboardSummary {
id: db.id,
name: db.name,
// ... other fields ...
permission: role, // Use the actual role
})
.collect();
// Combine and deduplicate
summaries.extend(shared_summaries);
// ... deduplicate logic ...
Ok(summaries)
}
```
### File Changes
#### Modified Files
- `api/libs/handlers/src/dashboards/get_dashboard_handler.rs`
- Changes: Add permission check at start of handler, use accurate permission role in response
- Impact: Ensures user has appropriate view permissions, returns accurate permission info
- Dependencies: Sharing library, admin check utility
- `api/libs/handlers/src/dashboards/delete_dashboard_handler.rs`
- Changes: Add permission check at start of handler
- Impact: Ensures user has appropriate delete permissions
- Dependencies: Sharing library, admin check utility
- `api/libs/handlers/src/dashboards/update_dashboard_handler.rs`
- Changes: Add permission check at start of handler, use accurate permission role in response
- Impact: Ensures user has appropriate edit permissions, returns accurate permission info
- Dependencies: Sharing library, admin check utility
- `api/libs/handlers/src/dashboards/list_dashboard_handler.rs`
- Changes: Modify query to filter by permissions, add admin special handling
- Impact: Ensures user only sees dashboards they have permission to view
- Dependencies: Sharing library, admin check utility
- `api/libs/handlers/src/dashboards/create_dashboard_handler.rs`
- Changes: No permission check needed for creation
- Impact: None (users can create dashboards without special permissions)
- Dependencies: None
## Implementation Plan
### Phase 1: Add Permission Utilities (In Progress)
1. Create dashboard-specific permission utility functions
- [ ] Implement `verify_dashboard_permission` helper function
- [ ] Implement `get_dashboard_permission_role` helper function
- [ ] Add error handling for permission failures
- [ ] Create reusable query for getting dashboard organization ID
2. Add unit tests for permission utilities
- [ ] Test permission verification with various roles
- [ ] Test admin override functionality
- [ ] Test CanFilter permission level
- [ ] Test error handling and edge cases
### Phase 2: Modify Dashboard Handlers (Not Started)
1. Update get_dashboard_handler
- [ ] Add permission check for CanView
- [ ] Replace hardcoded permission with actual user permission
- [ ] Handle CanFilter permission level
- [ ] Ensure proper error handling
- [ ] Update unit tests
2. Update delete_dashboard_handler
- [ ] Add permission check for FullAccess
- [ ] Ensure proper error handling
- [ ] Update unit tests
3. Update update_dashboard_handler
- [ ] Add permission check for CanEdit
- [ ] Replace hardcoded permission with actual user permission
- [ ] Ensure proper error handling
- [ ] Update unit tests
4. Update list_dashboard_handler
- [ ] Modify queries to filter by permission
- [ ] Add logic to include admin-accessible dashboards
- [ ] Include accurate permission levels in responses
- [ ] Update unit tests
### Phase 3: Testing & Documentation (Not Started)
1. Add integration tests
- [ ] Test end-to-end flows with different permission levels
- [ ] Test CanFilter permission level specifically
- [ ] Verify admin access works correctly
- [ ] Test permission denial scenarios
2. Update documentation
- [ ] Document permission requirements for each handler
- [ ] Add examples of correct usage
- [ ] Document error handling behavior
- [ ] Explain CanFilter permission level
## Testing Strategy
### Unit Tests
```rust
#[cfg(test)]
mod tests {
use super::*;
use database::enums::{AssetPermissionRole, AssetType, IdentityType};
use mockall::{predicate::*, *};
// Mock permission checking functions
mock! {
PermissionChecker {}
impl PermissionChecker {
async fn has_permission(
asset_id: Uuid,
asset_type: AssetType,
identity_id: Uuid,
identity_type: IdentityType,
required_role: AssetPermissionRole,
) -> Result<bool>;
async fn is_user_org_admin(
user_id: &Uuid,
org_id: &Uuid,
) -> Result<bool>;
}
}
#[tokio::test]
async fn test_get_dashboard_handler_with_permission() {
// Test that handler succeeds when user has permission
let dashboard_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
// Mock permission check to return true
// [mocking setup here]
let result = get_dashboard_handler(&dashboard_id, &user_id, None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_get_dashboard_handler_without_permission() {
// Test that handler fails when user lacks permission
let dashboard_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
// Mock permission check to return false
// [mocking setup here]
let result = get_dashboard_handler(&dashboard_id, &user_id, None).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Insufficient permissions"));
}
#[tokio::test]
async fn test_get_dashboard_permission_role_admin() {
// Test that admin users get FullAccess role
let dashboard_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let org_id = Uuid::new_v4();
// Mock admin check to return true
// [mocking setup here]
let result = get_dashboard_permission_role(&dashboard_id, &user_id).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), AssetPermissionRole::FullAccess);
}
#[tokio::test]
async fn test_get_dashboard_with_can_filter_permission() {
// Test that CanFilter permission allows viewing but has the correct permission level in response
let dashboard_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
// Mock permission check to return CanFilter
// [mocking setup here]
let result = get_dashboard_handler(&dashboard_id, &user_id, None).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().permission, AssetPermissionRole::CanFilter);
}
}
```
### Integration Tests
#### Scenario 1: Dashboard Access with Various Permission Levels
- Setup: Create test dashboard and users with different permission levels
- Steps:
1. Attempt to view dashboard with CanView, CanFilter, CanEdit, and FullAccess permissions
2. Attempt to edit dashboard with CanView, CanFilter, CanEdit, and FullAccess permissions
3. Attempt to delete dashboard with CanView, CanFilter, CanEdit, and FullAccess permissions
- Expected Results: Operations succeed only with appropriate permission levels
- Validation Criteria: View works with any permission, edit requires CanEdit+, delete requires FullAccess+
#### Scenario 2: CanFilter Permission Level
- Setup: Create test dashboard and user with CanFilter permission
- Steps:
1. User attempts to view dashboard (should succeed)
2. User applies filters to dashboard (should succeed, specific to dashboards)
3. User attempts to edit dashboard structure (should fail)
- Expected Results: User can view and filter but not edit
- Validation Criteria: CanFilter works as expected for dashboard-specific operations
#### Scenario 3: Admin Access to Dashboards
- Setup: Create test dashboard and admin user in same organization
- Steps:
1. Admin attempts to view, edit, and delete dashboard without explicit permissions
2. System checks admin status and permits operations
- Expected Results: Admin can perform all operations except those requiring Owner permission
- Validation Criteria: Operations succeed due to admin status, not explicit permissions
### Security Considerations
- Security Requirement 1: Permission Check Precedence
- Description: Permission checks must happen before any data access or modification
- Implementation: Place permission checks at the beginning of handler functions
- Validation: Code review and tests that verify permission failures prevent data access
- Security Requirement 2: CanFilter Implementation
- Description: Users with CanFilter should only be able to view and filter dashboards
- Implementation: Correctly map dashboard operations to permission levels
- Validation: Tests that verify filtering works with CanFilter but editing doesn't
### Performance Considerations
- Performance Requirement 1: Efficient Permission Checking
- Description: Permission checks should not significantly impact handler performance
- Implementation: Optimize database queries, consider caching for frequent checks
- Validation: Performance benchmarks of handlers with and without permission checks
### References
- [Sharing Library Documentation](mdc:libs/sharing/src/lib.rs)
- [Dashboard Models](mdc:database/src/models.rs)
- [Asset Permission Roles](mdc:database/src/enums.rs)