14 KiB
Dashboards Handlers Authentication Refactor
Overview
This PRD outlines the plan to refactor all dashboard handlers in libs/handlers/src/dashboards/
to accept the complete AuthenticatedUser
object instead of just the user_id
parameter.
Problem Statement
Currently, dashboard handlers accept only a user ID (Uuid
) as the user parameter. This approach has several limitations:
- It lacks rich user context information such as organization memberships and roles
- It requires additional database lookups to fetch user data within handlers
- It doesn't align with the REST endpoints which already use the
AuthenticatedUser
type from middleware
By refactoring these handlers to accept the complete AuthenticatedUser
object, we will:
- Improve code efficiency by reducing redundant database queries
- Enhance security by making permission checks more comprehensive
- Increase consistency across the codebase
- Improve test reliability with standardized test user fixtures
Goals
- Update all dashboard handlers to use
AuthenticatedUser
instead of user ID - Update tests to use the test utilities created in the metrics handlers PRD
- Optimize handler code to use available user context information
- Ensure tests pass with the new parameter format
- Maintain backward compatibility with existing functionality
Non-Goals
- Changing the business logic of the dashboard handlers
- Modifying database schemas
- Adding new features to dashboard handlers
- Changing the API contract between handlers and consumers
Technical Design
Overview
The refactoring will involve updating function signatures across all dashboard handlers to accept &AuthenticatedUser
instead of &Uuid
, and then modifying the internal logic to use user.id
instead of user_id
where appropriate. We'll also leverage additional user information to optimize certain operations.
Components to Modify
Dashboard Core Handlers
create_dashboard_handler.rs
get_dashboard_handler.rs
list_dashboard_handler.rs
update_dashboard_handler.rs
delete_dashboard_handler.rs
Dashboard Collection Management Handlers
add_dashboard_to_collections_handler.rs
remove_dashboard_from_collections_handler.rs
Dashboard Sharing Handlers
sharing/create_sharing_handler.rs
sharing/list_sharing_handler.rs
sharing/update_sharing_handler.rs
sharing/delete_sharing_handler.rs
Example Function Signature Changes
// Before
pub async fn get_dashboard_handler(dashboard_id: &Uuid, user_id: &Uuid) -> Result<DashboardResponse> {
// ...
}
// After
pub async fn get_dashboard_handler(dashboard_id: &Uuid, user: &AuthenticatedUser) -> Result<DashboardResponse> {
// ...
}
Key Implementation Details
For each handler, we'll:
- Update the function signature to accept
&AuthenticatedUser
instead of&Uuid
- Replace all instances of
user_id
withuser.id
in function body - Leverage user attributes and organization info where applicable
- Update database queries to filter by
user.id
instead ofuser_id
- Enhance permission checks using organization and team roles
Example Implementation (for get_dashboard_handler.rs)
// Before
pub async fn get_dashboard_handler(dashboard_id: &Uuid, user_id: &Uuid) -> Result<DashboardResponse> {
let mut conn = get_pg_pool().get().await?;
// Get the dashboard
let dashboard = dashboard_files::table
.filter(dashboard_files::id.eq(dashboard_id))
.filter(dashboard_files::created_by.eq(user_id).or(dashboard_files::publicly_accessible.eq(true)))
.filter(dashboard_files::deleted_at.is_null())
.first::<DashboardFile>(&mut conn)
.await
.map_err(|e| match e {
diesel::result::Error::NotFound =>
anyhow!("Dashboard not found or you don't have access"),
_ => anyhow!("Database error: {}", e),
})?;
// Additional logic...
Ok(DashboardResponse::from(dashboard))
}
// After
pub async fn get_dashboard_handler(dashboard_id: &Uuid, user: &AuthenticatedUser) -> Result<DashboardResponse> {
let mut conn = get_pg_pool().get().await?;
// Get the dashboard without filtering by user initially
let dashboard = dashboard_files::table
.filter(dashboard_files::id.eq(dashboard_id))
.filter(dashboard_files::deleted_at.is_null())
.first::<DashboardFile>(&mut conn)
.await
.map_err(|e| match e {
diesel::result::Error::NotFound =>
anyhow!("Dashboard not found"),
_ => anyhow!("Database error: {}", e),
})?;
// Enhanced permission checking using user context
if dashboard.created_by != user.id && !dashboard.publicly_accessible {
// Check if user is an admin of the dashboard's organization
let is_org_admin = user.organizations.iter()
.any(|org| org.id == dashboard.organization_id &&
(org.role == UserOrganizationRole::Admin || org.role == UserOrganizationRole::Owner));
if !is_org_admin {
// Check for explicit dashboard permissions
let has_permission = check_dashboard_permission(&dashboard_id, &user.id, &mut conn).await?;
if !has_permission {
return Err(anyhow!("You don't have access to this dashboard"));
}
}
}
// Additional logic...
Ok(DashboardResponse::from(dashboard))
}
REST Endpoint Changes
REST endpoints will need minimal changes:
// Before
pub async fn get_dashboard_route(
Path(dashboard_id): Path<Uuid>,
Extension(user): Extension<AuthenticatedUser>
) -> Result<ApiResponse<DashboardResponse>, ApiError> {
match get_dashboard_handler(&dashboard_id, &user.id).await {
// ...
}
}
// After
pub async fn get_dashboard_route(
Path(dashboard_id): Path<Uuid>,
Extension(user): Extension<AuthenticatedUser>
) -> Result<ApiResponse<DashboardResponse>, ApiError> {
match get_dashboard_handler(&dashboard_id, &user).await {
// ...
}
}
Files to Modify
Handler Files
/libs/handlers/src/dashboards/create_dashboard_handler.rs
/libs/handlers/src/dashboards/get_dashboard_handler.rs
/libs/handlers/src/dashboards/list_dashboard_handler.rs
/libs/handlers/src/dashboards/update_dashboard_handler.rs
/libs/handlers/src/dashboards/delete_dashboard_handler.rs
/libs/handlers/src/dashboards/add_dashboard_to_collections_handler.rs
/libs/handlers/src/dashboards/remove_dashboard_from_collections_handler.rs
/libs/handlers/src/dashboards/sharing/create_sharing_handler.rs
/libs/handlers/src/dashboards/sharing/list_sharing_handler.rs
/libs/handlers/src/dashboards/sharing/update_sharing_handler.rs
/libs/handlers/src/dashboards/sharing/delete_sharing_handler.rs
Tests
/libs/handlers/tests/dashboards/list_sharing_test.rs
- Any other existing dashboard handler tests
REST Endpoints
/src/routes/rest/routes/dashboards/create_dashboard.rs
/src/routes/rest/routes/dashboards/get_dashboard.rs
/src/routes/rest/routes/dashboards/list_dashboards.rs
/src/routes/rest/routes/dashboards/update_dashboard.rs
/src/routes/rest/routes/dashboards/delete_dashboard.rs
- Other related REST endpoints for collection management and sharing
Implementation Plan
Phase A: Core Dashboard Handlers (Days 1-3)
- ⏳ Update signatures and implementations of core dashboard handlers
- ⏳ Create/update tests for these handlers
- ⏳ Run tests to verify functionality
- ✅ Success criteria: All core dashboard handlers pass tests with the new parameter format
Phase B: Dashboard Collection Management Handlers (Days 4-5)
- 🔜 Update collection management handler signatures and implementations
- 🔜 Create/update tests for these handlers
- 🔜 Run tests to verify functionality
- ✅ Success criteria: All collection management handlers pass tests with the new parameter format
Phase C: Dashboard Sharing Handlers (Day 6)
- 🔜 Update sharing handler signatures and implementations
- 🔜 Update existing sharing tests
- 🔜 Run tests to verify functionality
- ✅ Success criteria: All sharing handlers pass tests with the new parameter format
Phase D: REST Integration (Day 7)
- 🔜 Update REST endpoints to pass the full user object
- 🔜 Run integration tests
- 🔜 Fix any issues
- ✅ Success criteria: All REST endpoints work correctly with the refactored handlers
Testing Strategy
Unit Tests
- Create tests if they don't exist
- Update existing tests to use the test utilities
- Add tests for different user roles and permissions
- Every handler must have tests that pass before considering the refactoring complete
Example Test Creation/Update
#[tokio::test]
async fn test_get_dashboard_with_owner() -> Result<()> {
// Setup test environment
setup_test_environment().await?;
// Create test organization
let org_id = Uuid::new_v4();
// Create test dashboard
let dashboard_id = Uuid::new_v4();
let creator_id = Uuid::new_v4();
let test_dashboard = create_test_dashboard(dashboard_id, org_id, creator_id).await?;
insert_test_dashboard(&test_dashboard).await?;
// Create test user with the new utilities - as the owner
let user = create_test_user(
Some(creator_id),
None,
None,
None,
None,
None,
Some(vec![
OrganizationMembership {
id: org_id,
role: UserOrganizationRole::Member,
}
]),
None,
None,
None
);
// Test using the full user object
let result = get_dashboard_handler(&dashboard_id, &user).await;
// Assert success
assert!(result.is_ok());
// Additional assertions...
// Cleanup
cleanup_test_dashboard(dashboard_id).await?;
Ok(())
}
#[tokio::test]
async fn test_get_dashboard_as_org_admin() -> Result<()> {
// Setup test environment
setup_test_environment().await?;
// Create test organization
let org_id = Uuid::new_v4();
// Create test dashboard
let dashboard_id = Uuid::new_v4();
let creator_id = Uuid::new_v4(); // Different from admin
let test_dashboard = create_test_dashboard(dashboard_id, org_id, creator_id).await?;
insert_test_dashboard(&test_dashboard).await?;
// Create test admin user
let admin_user = create_test_admin_user(Some(org_id));
// Test using the full user object
let result = get_dashboard_handler(&dashboard_id, &admin_user).await;
// Assert success - admin should be able to access
assert!(result.is_ok());
// Additional assertions...
// Cleanup
cleanup_test_dashboard(dashboard_id).await?;
Ok(())
}
#[tokio::test]
async fn test_get_dashboard_unauthorized() -> Result<()> {
// Setup test environment
setup_test_environment().await?;
// Create test organizations
let org_id = Uuid::new_v4();
let different_org_id = Uuid::new_v4();
// Create test dashboard
let dashboard_id = Uuid::new_v4();
let creator_id = Uuid::new_v4();
let test_dashboard = create_test_dashboard(dashboard_id, org_id, creator_id).await?;
test_dashboard.publicly_accessible = false; // Ensure it's not publicly accessible
insert_test_dashboard(&test_dashboard).await?;
// Create user from different organization
let different_org_user = create_test_regular_user(Some(different_org_id));
// Test using the full user object
let result = get_dashboard_handler(&dashboard_id, &different_org_user).await;
// Assert error - unauthorized
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("access"));
// Cleanup
cleanup_test_dashboard(dashboard_id).await?;
Ok(())
}
Test Cases
-
Owner User Tests
- User accessing their own dashboard
- User attempting to update their own dashboard
-
Admin User Tests
- Admin accessing a dashboard they don't own
- Admin updating a dashboard in their organization
-
Regular User Tests
- User attempting to access a dashboard they don't own
- User in wrong organization attempting access
-
Permission Tests
- User with explicit permissions through sharing
- User without permissions attempting access
-
Collection Management Tests
- Adding dashboard to a collection with permission
- Attempting to add dashboard without permission
- Removing dashboard from a collection with permission
- Attempting to remove dashboard without permission
Special Focus: List_sharing_test.rs
Since there is already an existing test file for dashboard sharing, special care will be taken to update this test file:
// Update existing test for sharing listing
#[tokio::test]
async fn test_list_dashboard_sharing() -> Result<()> {
// Setup test environment
setup_test_environment().await?;
// Create test data
let dashboard_id = Uuid::new_v4();
// [...existing test setup...]
// Update to use new test user utilities
let admin_user = create_test_admin_user(Some(org_id));
// Test sharing listing with the AuthenticatedUser object
let result = list_dashboard_sharing_handler(&dashboard_id, &admin_user).await;
// Rest of the test remains similar
// ...
}
Rollback Plan
If issues arise during implementation:
- Revert affected handlers to original implementation
- Document specific issues for resolution
- Implement a phased approach if needed, starting with less complex handlers
Success Criteria
For this PRD to be considered fully implemented:
- All dashboard handlers successfully accept
AuthenticatedUser
instead of just user ID - All tests are created or updated and pass with the new implementation
- REST endpoints correctly pass the full user object
- No regression in functionality or performance
- Enhanced permission checks work correctly
Dependencies
- Completion of the test utilities created in the metrics handlers PRD
middleware::AuthenticatedUser
struct fromlibs/middleware/src/types.rs
- Existing dashboards handlers implementation
Timeline
Expected completion time: 1 week (7 business days)
- Days 1-3: Core dashboard handlers refactoring and testing
- Days 4-5: Collection management handlers refactoring and testing
- Day 6: Sharing handlers refactoring and testing
- Day 7: REST integration and final validation