From fcf02f27dc92dd74b751e78eda9d8a840694f1ac Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 19 Mar 2025 12:51:37 -0600 Subject: [PATCH] commit up all the prds --- api/prds/active/api_chats_sharing_create.md | 206 +++++++ api/prds/active/api_chats_sharing_delete.md | 191 +++++++ api/prds/active/api_chats_sharing_list.md | 174 ++++++ api/prds/active/api_chats_sharing_summary.md | 104 ++++ api/prds/active/api_chats_sharing_update.md | 205 +++++++ .../active/api_collections_sharing_create.md | 206 +++++++ .../active/api_collections_sharing_delete.md | 191 +++++++ .../active/api_collections_sharing_list.md | 174 ++++++ .../active/api_collections_sharing_summary.md | 104 ++++ .../active/api_collections_sharing_update.md | 205 +++++++ .../active/api_dashboards_sharing_create.md | 206 +++++++ .../active/api_dashboards_sharing_delete.md | 191 +++++++ .../active/api_dashboards_sharing_list.md | 174 ++++++ .../active/api_dashboards_sharing_summary.md | 104 ++++ .../active/api_dashboards_sharing_update.md | 205 +++++++ api/prds/active/api_metrics_rest_endpoints.md | 181 ------- api/prds/active/api_metrics_sharing_create.md | 206 +++++++ api/prds/active/api_metrics_sharing_delete.md | 260 +++++++++ .../active/api_metrics_sharing_endpoints.md | 251 --------- api/prds/active/api_metrics_sharing_list.md | 174 ++++++ .../active/api_metrics_sharing_summary.md | 104 ++++ api/prds/active/api_metrics_sharing_update.md | 228 ++++++++ .../api_sharing_endpoints_master_summary.md | 136 +++++ ...ent_streaming_library_agent_integration.md | 506 ------------------ api/prds/active/sharing_access_controls.md | 180 ------- .../active/sharing_access_controls_summary.md | 119 ---- api/prds/active/sharing_check_permissions.md | 136 ----- api/prds/active/sharing_create_permissions.md | 93 ---- api/prds/active/sharing_list_permissions.md | 119 ---- api/prds/active/sharing_remove_permissions.md | 104 ---- api/prds/active/sharing_user_lookup.md | 85 --- 31 files changed, 3748 insertions(+), 1774 deletions(-) create mode 100644 api/prds/active/api_chats_sharing_create.md create mode 100644 api/prds/active/api_chats_sharing_delete.md create mode 100644 api/prds/active/api_chats_sharing_list.md create mode 100644 api/prds/active/api_chats_sharing_summary.md create mode 100644 api/prds/active/api_chats_sharing_update.md create mode 100644 api/prds/active/api_collections_sharing_create.md create mode 100644 api/prds/active/api_collections_sharing_delete.md create mode 100644 api/prds/active/api_collections_sharing_list.md create mode 100644 api/prds/active/api_collections_sharing_summary.md create mode 100644 api/prds/active/api_collections_sharing_update.md create mode 100644 api/prds/active/api_dashboards_sharing_create.md create mode 100644 api/prds/active/api_dashboards_sharing_delete.md create mode 100644 api/prds/active/api_dashboards_sharing_list.md create mode 100644 api/prds/active/api_dashboards_sharing_summary.md create mode 100644 api/prds/active/api_dashboards_sharing_update.md delete mode 100644 api/prds/active/api_metrics_rest_endpoints.md create mode 100644 api/prds/active/api_metrics_sharing_create.md create mode 100644 api/prds/active/api_metrics_sharing_delete.md delete mode 100644 api/prds/active/api_metrics_sharing_endpoints.md create mode 100644 api/prds/active/api_metrics_sharing_list.md create mode 100644 api/prds/active/api_metrics_sharing_summary.md create mode 100644 api/prds/active/api_metrics_sharing_update.md create mode 100644 api/prds/active/api_sharing_endpoints_master_summary.md delete mode 100644 api/prds/active/enhancement_streaming_library_agent_integration.md delete mode 100644 api/prds/active/sharing_access_controls.md delete mode 100644 api/prds/active/sharing_access_controls_summary.md delete mode 100644 api/prds/active/sharing_check_permissions.md delete mode 100644 api/prds/active/sharing_create_permissions.md delete mode 100644 api/prds/active/sharing_list_permissions.md delete mode 100644 api/prds/active/sharing_remove_permissions.md delete mode 100644 api/prds/active/sharing_user_lookup.md diff --git a/api/prds/active/api_chats_sharing_create.md b/api/prds/active/api_chats_sharing_create.md new file mode 100644 index 000000000..c034a0ad0 --- /dev/null +++ b/api/prds/active/api_chats_sharing_create.md @@ -0,0 +1,206 @@ +# API Chats Sharing - Create Endpoint PRD + +## Problem Statement +Users need the ability to share chats with other users via a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: POST +- **Path**: /chats/:id/sharing +- **Description**: Shares a chat with specified users +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the chat + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct SharingRequest { + pub emails: Vec, + pub role: AssetPermissionRole, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/chats/sharing/create_sharing.rs` - REST handler for creating sharing permissions +2. `/libs/handlers/src/chats/sharing/create_sharing_handler.rs` - Business logic for creating sharing permissions + +#### REST Handler Implementation +```rust +// create_sharing.rs +pub async fn create_chat_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing POST request for chat sharing with ID: {}, user_id: {}", id, user.id); + + match create_chat_sharing_handler(&id, &user.id, request.emails, request.role).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions created successfully".to_string())), + Err(e) => { + tracing::error!("Error creating sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Chat not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// create_sharing_handler.rs +pub async fn create_chat_sharing_handler( + chat_id: &Uuid, + user_id: &Uuid, + emails: Vec, + role: AssetPermissionRole, +) -> Result<()> { + // 1. Validate the chat exists + let chat = match get_chat_by_id(chat_id).await { + Ok(Some(chat)) => chat, + Ok(None) => return Err(anyhow!("Chat not found")), + Err(e) => return Err(anyhow!("Error fetching chat: {}", e)), + }; + + // 2. Check if user has permission to share the chat (Owner or FullAccess) + let has_permission = has_permission( + *chat_id, + AssetType::Chat, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to share this chat")); + } + + // 3. Process each email and create sharing permissions + for email in emails { + // Create or update the permission using create_share_by_email + match create_share_by_email( + &email, + *chat_id, + AssetType::Chat, + role, + *user_id, + ).await { + Ok(_) => { + tracing::info!("Created sharing permission for email: {} on chat: {}", email, chat_id); + }, + Err(e) => { + tracing::error!("Failed to create sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to create sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to share the chat. + +2. `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs`: +```rust +pub async fn create_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + role: AssetPermissionRole, + created_by: Uuid, +) -> Result +``` +This function creates or updates an asset permission for a user identified by email. It handles: +- Email validation +- User lookup by email +- Permission creation or update +- Error handling for invalid emails or non-existent users + +3. `find_user_by_email` from `@[api/libs/sharing/src]/user_lookup.rs` (used internally by `create_share_by_email`): +```rust +pub async fn find_user_by_email(email: &str) -> Result> +``` +This function looks up a user by their email address, which is necessary for resolving email addresses to user IDs. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the chat doesn't exist +- 403 Forbidden - If the user doesn't have permission to share the chat +- 400 Bad Request - For invalid email addresses or roles +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- Roles must be valid AssetPermissionRole values +- The chat ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent chats +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing creation + +#### Integration Tests +- Test POST /chats/:id/sharing with valid ID, authorized user, and valid emails +- Test POST /chats/:id/sharing with valid ID, unauthorized user +- Test POST /chats/:id/sharing with non-existent chat ID +- Test POST /chats/:id/sharing with invalid email formats +- Test POST /chats/:id/sharing with non-existent user emails +- Test POST /chats/:id/sharing with invalid roles + +#### Test Cases +1. Should create sharing permissions for valid emails and roles +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when chat doesn't exist +4. Should return 400 when email is invalid +5. Should return 400 when role is invalid + +### Performance Considerations +- For bulk sharing with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of sharing operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can share chats +- Validate email addresses to prevent injection attacks +- Validate roles to prevent privilege escalation +- Implement rate limiting to prevent abuse + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing operations by user for audit purposes diff --git a/api/prds/active/api_chats_sharing_delete.md b/api/prds/active/api_chats_sharing_delete.md new file mode 100644 index 000000000..9bc1c7980 --- /dev/null +++ b/api/prds/active/api_chats_sharing_delete.md @@ -0,0 +1,191 @@ +# API Chats Sharing - Delete Endpoint PRD + +## Problem Statement +Users need the ability to remove sharing permissions for chats through a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: DELETE +- **Path**: /chats/:id/sharing +- **Description**: Removes sharing permissions for a chat +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the chat + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct DeleteSharingRequest { + pub emails: Vec, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/chats/sharing/delete_sharing.rs` - REST handler for deleting sharing permissions +2. `/libs/handlers/src/chats/sharing/delete_sharing_handler.rs` - Business logic for deleting sharing permissions + +#### REST Handler Implementation +```rust +// delete_sharing.rs +pub async fn delete_chat_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing DELETE request for chat sharing with ID: {}, user_id: {}", id, user.id); + + match delete_chat_sharing_handler(&id, &user.id, request.emails).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions removed successfully".to_string())), + Err(e) => { + tracing::error!("Error removing sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Chat not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to remove sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// delete_sharing_handler.rs +pub async fn delete_chat_sharing_handler( + chat_id: &Uuid, + user_id: &Uuid, + emails: Vec, +) -> Result<()> { + // 1. Validate the chat exists + let chat = match get_chat_by_id(chat_id).await { + Ok(Some(chat)) => chat, + Ok(None) => return Err(anyhow!("Chat not found")), + Err(e) => return Err(anyhow!("Error fetching chat: {}", e)), + }; + + // 2. Check if user has permission to remove sharing for the chat (Owner or FullAccess) + let has_permission = has_permission( + *chat_id, + AssetType::Chat, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to remove sharing for this chat")); + } + + // 3. Process each email and remove sharing permissions + for email in emails { + match remove_share_by_email( + &email, + *chat_id, + AssetType::Chat, + ).await { + Ok(_) => { + tracing::info!("Removed sharing permission for email: {} on chat: {}", email, chat_id); + }, + Err(e) => { + tracing::error!("Failed to remove sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to remove sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to remove sharing for the chat. + +2. `remove_share_by_email` from `@[api/libs/sharing/src]/remove_asset_permissions.rs`: +```rust +pub async fn remove_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, +) -> Result<()> +``` +This function removes sharing permissions for a user identified by email. It handles: +- Email validation +- User lookup by email +- Permission removal +- Error handling for invalid emails or non-existent users + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the chat doesn't exist +- 403 Forbidden - If the user doesn't have permission to remove sharing for the chat +- 400 Bad Request - For invalid email addresses +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- The chat ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent chats +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing removal + +#### Integration Tests +- Test DELETE /chats/:id/sharing with valid ID, authorized user, and valid emails +- Test DELETE /chats/:id/sharing with valid ID, unauthorized user +- Test DELETE /chats/:id/sharing with non-existent chat ID +- Test DELETE /chats/:id/sharing with invalid email formats +- Test DELETE /chats/:id/sharing with non-existent user emails + +#### Test Cases +1. Should remove sharing permissions for valid emails +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when chat doesn't exist +4. Should return 400 when email is invalid +5. Should handle gracefully when trying to remove sharing for a user that doesn't have access + +### Performance Considerations +- For bulk removal with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of removal operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can remove sharing +- Validate email addresses to prevent injection attacks +- Implement rate limiting to prevent abuse +- Prevent removal of the owner's own access + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing removal operations by user for audit purposes diff --git a/api/prds/active/api_chats_sharing_list.md b/api/prds/active/api_chats_sharing_list.md new file mode 100644 index 000000000..3837ca197 --- /dev/null +++ b/api/prds/active/api_chats_sharing_list.md @@ -0,0 +1,174 @@ +# API Chats Sharing - List Endpoint PRD + +## Problem Statement +Users need the ability to view all sharing permissions for a chat via a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: GET +- **Path**: /chats/:id/sharing +- **Description**: Lists all sharing permissions for a specific chat +- **Authentication**: Required +- **Authorization**: User must have at least ReadOnly access to the chat + +### Response Structure +```rust +#[derive(Debug, Serialize)] +pub struct SharingResponse { + pub permissions: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SharingPermission { + pub user_id: Uuid, + pub email: String, + pub name: Option, + pub avatar_url: Option, + pub role: AssetPermissionRole, +} +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/chats/sharing/list_sharing.rs` - REST handler for listing sharing permissions +2. `/libs/handlers/src/chats/sharing/list_sharing_handler.rs` - Business logic for listing sharing permissions + +#### REST Handler Implementation +```rust +// list_sharing.rs +pub async fn list_chat_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing GET request for chat sharing with ID: {}, user_id: {}", id, user.id); + + match list_chat_sharing_handler(&id, &user.id).await { + Ok(permissions) => { + let response = SharingResponse { + permissions: permissions.into_iter().map(|p| SharingPermission { + user_id: p.user.as_ref().map(|u| u.id).unwrap_or_default(), + email: p.user.as_ref().map(|u| u.email.clone()).unwrap_or_default(), + name: p.user.as_ref().and_then(|u| u.name.clone()), + avatar_url: p.user.as_ref().and_then(|u| u.avatar_url.clone()), + role: p.permission.role, + }).collect(), + }; + Ok(ApiResponse::Success(response)) + }, + Err(e) => { + tracing::error!("Error listing sharing permissions: {}", e); + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to list sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// list_sharing_handler.rs +pub async fn list_chat_sharing_handler( + chat_id: &Uuid, + user_id: &Uuid, +) -> Result> { + // 1. Validate the chat exists + let chat = match get_chat_by_id(chat_id).await { + Ok(Some(chat)) => chat, + Ok(None) => return Err(anyhow!("Chat not found")), + Err(e) => return Err(anyhow!("Error fetching chat: {}", e)), + }; + + // 2. Check if user has permission to view the chat + let user_role = check_access( + *chat_id, + AssetType::Chat, + *user_id, + IdentityType::User, + ).await?; + + if user_role.is_none() { + return Err(anyhow!("User does not have permission to view this chat")); + } + + // 3. Get all permissions for the chat + let permissions = list_shares( + *chat_id, + AssetType::Chat, + ).await?; + + Ok(permissions) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn check_access( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, +) -> Result> +``` +This function is used to verify that the user has permission to view the chat. It returns the user's role for the asset, or None if they don't have access. + +2. `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs`: +```rust +pub async fn list_shares( + asset_id: Uuid, + asset_type: AssetType, +) -> Result> +``` +This function retrieves all permissions for a specified asset, including user information. It filters out soft-deleted permissions and returns a structured response. + +3. The `AssetPermissionWithUser` type from `@[api/libs/sharing/src]/types.rs`: +```rust +pub struct AssetPermissionWithUser { + pub permission: SerializableAssetPermission, + pub user: Option, +} +``` +This type combines permission data with user information for a comprehensive response. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the chat doesn't exist +- 403 Forbidden - If the user doesn't have permission to view the chat +- 500 Internal Server Error - For database errors or other unexpected issues + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent chats +- Test error handling for unauthorized users +- Test mapping from `AssetPermissionWithUser` to `SharingPermission` + +#### Integration Tests +- Test GET /chats/:id/sharing with valid ID and authorized user +- Test GET /chats/:id/sharing with valid ID and unauthorized user +- Test GET /chats/:id/sharing with non-existent chat ID +- Test GET /chats/:id/sharing with chat that has no sharing permissions + +#### Test Cases +1. Should return all sharing permissions for a chat when user has access +2. Should return 403 when user doesn't have access to the chat +3. Should return 404 when chat doesn't exist +4. Should return empty array when no sharing permissions exist + +### Performance Considerations +- The `list_shares` function performs a database join between asset_permissions and users tables +- For chats with many sharing permissions, consider pagination in a future enhancement + +### Security Considerations +- Ensure that only users with at least ReadOnly access can view sharing permissions +- Validate the chat ID to prevent injection attacks +- Do not expose sensitive user information beyond what's needed + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types diff --git a/api/prds/active/api_chats_sharing_summary.md b/api/prds/active/api_chats_sharing_summary.md new file mode 100644 index 000000000..79db121a2 --- /dev/null +++ b/api/prds/active/api_chats_sharing_summary.md @@ -0,0 +1,104 @@ +# API Chats Sharing Endpoints - Summary PRD + +## Overview +This document provides a high-level summary of the API chats sharing endpoints implementation and outlines the development sequence for the individual components. + +## Problem Statement +Currently, there is no way to manage sharing permissions for chats through REST endpoints. Users need to be able to share chats with other users, update sharing permissions, and remove sharing permissions through REST endpoints. + +## Implementation Components +The implementation is broken down into the following components, each with its own detailed PRD: + +1. **List Chats Sharing Endpoint** - GET /chats/:id/sharing + - PRD: [api_chats_sharing_list.md](/Users/dallin/buster/buster/api/prds/active/api_chats_sharing_list.md) + +2. **Create Chats Sharing Endpoint** - POST /chats/:id/sharing + - PRD: [api_chats_sharing_create.md](/Users/dallin/buster/buster/api/prds/active/api_chats_sharing_create.md) + +3. **Update Chats Sharing Endpoint** - PUT /chats/:id/sharing + - PRD: [api_chats_sharing_update.md](/Users/dallin/buster/buster/api/prds/active/api_chats_sharing_update.md) + +4. **Delete Chats Sharing Endpoint** - DELETE /chats/:id/sharing + - PRD: [api_chats_sharing_delete.md](/Users/dallin/buster/buster/api/prds/active/api_chats_sharing_delete.md) + +## PRD Development Sequence and Parallelization + +### PRD Development Order +The PRDs can be developed in the following order, with opportunities for parallel work: + +1. **First: List Chats Sharing Endpoint PRD** (api_chats_sharing_list.md) + - This PRD should be completed first as it establishes the basic data structures and permission checking patterns that other PRDs will build upon. + - It introduces the core response types and error handling approaches. + +2. **Second (Can be done in parallel):** + - **Create Chats Sharing Endpoint PRD** (api_chats_sharing_create.md) + - **Delete Chats Sharing Endpoint PRD** (api_chats_sharing_delete.md) + - These PRDs can be worked on simultaneously by different team members after the List PRD is complete. + - They use different sharing library functions and have minimal dependencies on each other. + +3. **Third: Update Chats Sharing Endpoint PRD** (api_chats_sharing_update.md) + - This PRD should be completed after the Create PRD since it builds on similar concepts and uses the same underlying sharing library functions. + - The update endpoint reuses many patterns from the create endpoint with slight modifications. + +### Dependencies Between PRDs +- The List PRD establishes patterns for permission checking and response structures. +- The Create and Delete PRDs are independent of each other but depend on patterns from the List PRD. +- The Update PRD builds upon the Create PRD's approach to modifying permissions. + +## Implementation Sequence and Parallelization + +### Phase 1: Foundation (Sequential) +1. Set up the directory structure for sharing handlers and endpoints + - Create `/src/routes/rest/routes/chats/sharing/mod.rs` + - Create `/libs/handlers/src/chats/sharing/mod.rs` + - Update `/src/routes/rest/routes/chats/mod.rs` to include the sharing router + - Update `/libs/handlers/src/chats/mod.rs` to export the sharing module + +### Phase 2: Core Endpoints (Can be Parallelized) +After Phase 1 is complete, the following components can be implemented in parallel by different developers: + +- **List Sharing Endpoint** + - Uses `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs` + - Uses `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Create Sharing Endpoint** + - Uses `find_user_by_email` from `@[api/libs/sharing/src]/user_lookup.rs` + - Uses `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Update Sharing Endpoint** + - Uses `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Delete Sharing Endpoint** + - Uses `remove_share_by_email` from `@[api/libs/sharing/src]/remove_asset_permissions.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +### Phase 3: Integration and Testing (Sequential) +1. Integration testing of all endpoints together +2. Manual testing with Postman/curl +3. Performance testing +4. Documentation updates + +## Dependencies +All components depend on the sharing library at `@[api/libs/sharing/src]`, which provides the core functionality for managing asset permissions. The specific dependencies are: + +- `/api/libs/sharing/src/user_lookup.rs` - User lookup by email +- `/api/libs/sharing/src/create_asset_permission.rs` - Create/update permissions +- `/api/libs/sharing/src/remove_asset_permissions.rs` - Remove permissions +- `/api/libs/sharing/src/list_asset_permissions.rs` - List permissions +- `/api/libs/sharing/src/check_asset_permission.rs` - Check permissions +- `/api/libs/sharing/src/types.rs` - Shared types for permissions + +## Security Considerations +- All endpoints require authentication +- Only users with appropriate permissions can manage sharing +- Input validation must be thorough to prevent security issues +- Email addresses must be properly validated and resolved to user IDs +- Permission checks must be enforced for all operations + +## Rollback Plan +If issues are discovered: +1. Revert the changes to the affected files +2. Deploy the previous version +3. Investigate and fix the issues in a new PR diff --git a/api/prds/active/api_chats_sharing_update.md b/api/prds/active/api_chats_sharing_update.md new file mode 100644 index 000000000..c3fbc4cc3 --- /dev/null +++ b/api/prds/active/api_chats_sharing_update.md @@ -0,0 +1,205 @@ +# API Chats Sharing - Update Endpoint PRD + +## Problem Statement +Users need the ability to update sharing permissions for chats through a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: PUT +- **Path**: /chats/:id/sharing +- **Description**: Updates sharing permissions for a chat +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the chat + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct SharingRequest { + pub emails: Vec, + pub role: AssetPermissionRole, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/chats/sharing/update_sharing.rs` - REST handler for updating sharing permissions +2. `/libs/handlers/src/chats/sharing/update_sharing_handler.rs` - Business logic for updating sharing permissions + +#### REST Handler Implementation +```rust +// update_sharing.rs +pub async fn update_chat_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing PUT request for chat sharing with ID: {}, user_id: {}", id, user.id); + + match update_chat_sharing_handler(&id, &user.id, request.emails, request.role).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions updated successfully".to_string())), + Err(e) => { + tracing::error!("Error updating sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Chat not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to update sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// update_sharing_handler.rs +pub async fn update_chat_sharing_handler( + chat_id: &Uuid, + user_id: &Uuid, + emails: Vec, + role: AssetPermissionRole, +) -> Result<()> { + // 1. Validate the chat exists + let chat = match get_chat_by_id(chat_id).await { + Ok(Some(chat)) => chat, + Ok(None) => return Err(anyhow!("Chat not found")), + Err(e) => return Err(anyhow!("Error fetching chat: {}", e)), + }; + + // 2. Check if user has permission to update sharing for the chat (Owner or FullAccess) + let has_permission = has_permission( + *chat_id, + AssetType::Chat, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to update sharing for this chat")); + } + + // 3. Process each email and update sharing permissions + for email in emails { + // The create_share_by_email function handles both creation and updates + // It performs an upsert operation in the database + match create_share_by_email( + &email, + *chat_id, + AssetType::Chat, + role, + *user_id, + ).await { + Ok(_) => { + tracing::info!("Updated sharing permission for email: {} on chat: {}", email, chat_id); + }, + Err(e) => { + tracing::error!("Failed to update sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to update sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to update sharing for the chat. + +2. `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs`: +```rust +pub async fn create_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + role: AssetPermissionRole, + created_by: Uuid, +) -> Result +``` +This function is used for both creating and updating permissions. It performs an upsert operation in the database. + +### Key Differences from Create Endpoint +While the update endpoint uses the same `create_share_by_email` function as the create endpoint, there are some key differences in its usage: + +1. **Semantic Difference**: The PUT method indicates an update operation, while POST indicates creation. +2. **Expected Behavior**: The update endpoint is expected to modify existing permissions, while the create endpoint is expected to add new ones. +3. **Error Handling**: The update endpoint might handle "permission not found" differently than the create endpoint. +4. **Documentation**: The API documentation will describe these endpoints differently to users. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the chat doesn't exist +- 403 Forbidden - If the user doesn't have permission to update sharing for the chat +- 400 Bad Request - For invalid email addresses or roles +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- Roles must be valid AssetPermissionRole values +- The chat ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent chats +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing updates + +#### Integration Tests +- Test PUT /chats/:id/sharing with valid ID, authorized user, and valid emails +- Test PUT /chats/:id/sharing with valid ID, unauthorized user +- Test PUT /chats/:id/sharing with non-existent chat ID +- Test PUT /chats/:id/sharing with invalid email formats +- Test PUT /chats/:id/sharing with non-existent user emails +- Test PUT /chats/:id/sharing with invalid roles + +#### Test Cases +1. Should update sharing permissions for valid emails and roles +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when chat doesn't exist +4. Should return 400 when email is invalid +5. Should return 400 when role is invalid + +### Performance Considerations +- For bulk updates with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of update operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can update sharing +- Validate email addresses to prevent injection attacks +- Validate roles to prevent privilege escalation +- Implement rate limiting to prevent abuse + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing update operations by user for audit purposes diff --git a/api/prds/active/api_collections_sharing_create.md b/api/prds/active/api_collections_sharing_create.md new file mode 100644 index 000000000..1a88ea9eb --- /dev/null +++ b/api/prds/active/api_collections_sharing_create.md @@ -0,0 +1,206 @@ +# API Collections Sharing - Create Endpoint PRD + +## Problem Statement +Users need the ability to share collections with other users via a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: POST +- **Path**: /collections/:id/sharing +- **Description**: Shares a collection with specified users +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the collection + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct SharingRequest { + pub emails: Vec, + pub role: AssetPermissionRole, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/collections/sharing/create_sharing.rs` - REST handler for creating sharing permissions +2. `/libs/handlers/src/collections/sharing/create_sharing_handler.rs` - Business logic for creating sharing permissions + +#### REST Handler Implementation +```rust +// create_sharing.rs +pub async fn create_collection_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing POST request for collection sharing with ID: {}, user_id: {}", id, user.id); + + match create_collection_sharing_handler(&id, &user.id, request.emails, request.role).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions created successfully".to_string())), + Err(e) => { + tracing::error!("Error creating sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Collection not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// create_sharing_handler.rs +pub async fn create_collection_sharing_handler( + collection_id: &Uuid, + user_id: &Uuid, + emails: Vec, + role: AssetPermissionRole, +) -> Result<()> { + // 1. Validate the collection exists + let collection = match get_collection_by_id(collection_id).await { + Ok(Some(collection)) => collection, + Ok(None) => return Err(anyhow!("Collection not found")), + Err(e) => return Err(anyhow!("Error fetching collection: {}", e)), + }; + + // 2. Check if user has permission to share the collection (Owner or FullAccess) + let has_permission = has_permission( + *collection_id, + AssetType::Collection, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to share this collection")); + } + + // 3. Process each email and create sharing permissions + for email in emails { + // Create or update the permission using create_share_by_email + match create_share_by_email( + &email, + *collection_id, + AssetType::Collection, + role, + *user_id, + ).await { + Ok(_) => { + tracing::info!("Created sharing permission for email: {} on collection: {}", email, collection_id); + }, + Err(e) => { + tracing::error!("Failed to create sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to create sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to share the collection. + +2. `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs`: +```rust +pub async fn create_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + role: AssetPermissionRole, + created_by: Uuid, +) -> Result +``` +This function creates or updates an asset permission for a user identified by email. It handles: +- Email validation +- User lookup by email +- Permission creation or update +- Error handling for invalid emails or non-existent users + +3. `find_user_by_email` from `@[api/libs/sharing/src]/user_lookup.rs` (used internally by `create_share_by_email`): +```rust +pub async fn find_user_by_email(email: &str) -> Result> +``` +This function looks up a user by their email address, which is necessary for resolving email addresses to user IDs. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the collection doesn't exist +- 403 Forbidden - If the user doesn't have permission to share the collection +- 400 Bad Request - For invalid email addresses or roles +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- Roles must be valid AssetPermissionRole values +- The collection ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent collections +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing creation + +#### Integration Tests +- Test POST /collections/:id/sharing with valid ID, authorized user, and valid emails +- Test POST /collections/:id/sharing with valid ID, unauthorized user +- Test POST /collections/:id/sharing with non-existent collection ID +- Test POST /collections/:id/sharing with invalid email formats +- Test POST /collections/:id/sharing with non-existent user emails +- Test POST /collections/:id/sharing with invalid roles + +#### Test Cases +1. Should create sharing permissions for valid emails and roles +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when collection doesn't exist +4. Should return 400 when email is invalid +5. Should return 400 when role is invalid + +### Performance Considerations +- For bulk sharing with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of sharing operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can share collections +- Validate email addresses to prevent injection attacks +- Validate roles to prevent privilege escalation +- Implement rate limiting to prevent abuse + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing operations by user for audit purposes diff --git a/api/prds/active/api_collections_sharing_delete.md b/api/prds/active/api_collections_sharing_delete.md new file mode 100644 index 000000000..760cfce05 --- /dev/null +++ b/api/prds/active/api_collections_sharing_delete.md @@ -0,0 +1,191 @@ +# API Collections Sharing - Delete Endpoint PRD + +## Problem Statement +Users need the ability to remove sharing permissions for collections through a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: DELETE +- **Path**: /collections/:id/sharing +- **Description**: Removes sharing permissions for a collection +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the collection + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct DeleteSharingRequest { + pub emails: Vec, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/collections/sharing/delete_sharing.rs` - REST handler for deleting sharing permissions +2. `/libs/handlers/src/collections/sharing/delete_sharing_handler.rs` - Business logic for deleting sharing permissions + +#### REST Handler Implementation +```rust +// delete_sharing.rs +pub async fn delete_collection_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing DELETE request for collection sharing with ID: {}, user_id: {}", id, user.id); + + match delete_collection_sharing_handler(&id, &user.id, request.emails).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions removed successfully".to_string())), + Err(e) => { + tracing::error!("Error removing sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Collection not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to remove sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// delete_sharing_handler.rs +pub async fn delete_collection_sharing_handler( + collection_id: &Uuid, + user_id: &Uuid, + emails: Vec, +) -> Result<()> { + // 1. Validate the collection exists + let collection = match get_collection_by_id(collection_id).await { + Ok(Some(collection)) => collection, + Ok(None) => return Err(anyhow!("Collection not found")), + Err(e) => return Err(anyhow!("Error fetching collection: {}", e)), + }; + + // 2. Check if user has permission to remove sharing for the collection (Owner or FullAccess) + let has_permission = has_permission( + *collection_id, + AssetType::Collection, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to remove sharing for this collection")); + } + + // 3. Process each email and remove sharing permissions + for email in emails { + match remove_share_by_email( + &email, + *collection_id, + AssetType::Collection, + ).await { + Ok(_) => { + tracing::info!("Removed sharing permission for email: {} on collection: {}", email, collection_id); + }, + Err(e) => { + tracing::error!("Failed to remove sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to remove sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to remove sharing for the collection. + +2. `remove_share_by_email` from `@[api/libs/sharing/src]/remove_asset_permissions.rs`: +```rust +pub async fn remove_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, +) -> Result<()> +``` +This function removes sharing permissions for a user identified by email. It handles: +- Email validation +- User lookup by email +- Permission removal +- Error handling for invalid emails or non-existent users + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the collection doesn't exist +- 403 Forbidden - If the user doesn't have permission to remove sharing for the collection +- 400 Bad Request - For invalid email addresses +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- The collection ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent collections +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing removal + +#### Integration Tests +- Test DELETE /collections/:id/sharing with valid ID, authorized user, and valid emails +- Test DELETE /collections/:id/sharing with valid ID, unauthorized user +- Test DELETE /collections/:id/sharing with non-existent collection ID +- Test DELETE /collections/:id/sharing with invalid email formats +- Test DELETE /collections/:id/sharing with non-existent user emails + +#### Test Cases +1. Should remove sharing permissions for valid emails +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when collection doesn't exist +4. Should return 400 when email is invalid +5. Should handle gracefully when trying to remove sharing for a user that doesn't have access + +### Performance Considerations +- For bulk removal with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of removal operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can remove sharing +- Validate email addresses to prevent injection attacks +- Implement rate limiting to prevent abuse +- Prevent removal of the owner's own access + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing removal operations by user for audit purposes diff --git a/api/prds/active/api_collections_sharing_list.md b/api/prds/active/api_collections_sharing_list.md new file mode 100644 index 000000000..1dce8f461 --- /dev/null +++ b/api/prds/active/api_collections_sharing_list.md @@ -0,0 +1,174 @@ +# API Collections Sharing - List Endpoint PRD + +## Problem Statement +Users need the ability to view all sharing permissions for a collection via a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: GET +- **Path**: /collections/:id/sharing +- **Description**: Lists all sharing permissions for a specific collection +- **Authentication**: Required +- **Authorization**: User must have at least ReadOnly access to the collection + +### Response Structure +```rust +#[derive(Debug, Serialize)] +pub struct SharingResponse { + pub permissions: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SharingPermission { + pub user_id: Uuid, + pub email: String, + pub name: Option, + pub avatar_url: Option, + pub role: AssetPermissionRole, +} +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/collections/sharing/list_sharing.rs` - REST handler for listing sharing permissions +2. `/libs/handlers/src/collections/sharing/list_sharing_handler.rs` - Business logic for listing sharing permissions + +#### REST Handler Implementation +```rust +// list_sharing.rs +pub async fn list_collection_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing GET request for collection sharing with ID: {}, user_id: {}", id, user.id); + + match list_collection_sharing_handler(&id, &user.id).await { + Ok(permissions) => { + let response = SharingResponse { + permissions: permissions.into_iter().map(|p| SharingPermission { + user_id: p.user.as_ref().map(|u| u.id).unwrap_or_default(), + email: p.user.as_ref().map(|u| u.email.clone()).unwrap_or_default(), + name: p.user.as_ref().and_then(|u| u.name.clone()), + avatar_url: p.user.as_ref().and_then(|u| u.avatar_url.clone()), + role: p.permission.role, + }).collect(), + }; + Ok(ApiResponse::Success(response)) + }, + Err(e) => { + tracing::error!("Error listing sharing permissions: {}", e); + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to list sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// list_sharing_handler.rs +pub async fn list_collection_sharing_handler( + collection_id: &Uuid, + user_id: &Uuid, +) -> Result> { + // 1. Validate the collection exists + let collection = match get_collection_by_id(collection_id).await { + Ok(Some(collection)) => collection, + Ok(None) => return Err(anyhow!("Collection not found")), + Err(e) => return Err(anyhow!("Error fetching collection: {}", e)), + }; + + // 2. Check if user has permission to view the collection + let user_role = check_access( + *collection_id, + AssetType::Collection, + *user_id, + IdentityType::User, + ).await?; + + if user_role.is_none() { + return Err(anyhow!("User does not have permission to view this collection")); + } + + // 3. Get all permissions for the collection + let permissions = list_shares( + *collection_id, + AssetType::Collection, + ).await?; + + Ok(permissions) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn check_access( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, +) -> Result> +``` +This function is used to verify that the user has permission to view the collection. It returns the user's role for the asset, or None if they don't have access. + +2. `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs`: +```rust +pub async fn list_shares( + asset_id: Uuid, + asset_type: AssetType, +) -> Result> +``` +This function retrieves all permissions for a specified asset, including user information. It filters out soft-deleted permissions and returns a structured response. + +3. The `AssetPermissionWithUser` type from `@[api/libs/sharing/src]/types.rs`: +```rust +pub struct AssetPermissionWithUser { + pub permission: SerializableAssetPermission, + pub user: Option, +} +``` +This type combines permission data with user information for a comprehensive response. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the collection doesn't exist +- 403 Forbidden - If the user doesn't have permission to view the collection +- 500 Internal Server Error - For database errors or other unexpected issues + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent collections +- Test error handling for unauthorized users +- Test mapping from `AssetPermissionWithUser` to `SharingPermission` + +#### Integration Tests +- Test GET /collections/:id/sharing with valid ID and authorized user +- Test GET /collections/:id/sharing with valid ID and unauthorized user +- Test GET /collections/:id/sharing with non-existent collection ID +- Test GET /collections/:id/sharing with collection that has no sharing permissions + +#### Test Cases +1. Should return all sharing permissions for a collection when user has access +2. Should return 403 when user doesn't have access to the collection +3. Should return 404 when collection doesn't exist +4. Should return empty array when no sharing permissions exist + +### Performance Considerations +- The `list_shares` function performs a database join between asset_permissions and users tables +- For collections with many sharing permissions, consider pagination in a future enhancement + +### Security Considerations +- Ensure that only users with at least ReadOnly access can view sharing permissions +- Validate the collection ID to prevent injection attacks +- Do not expose sensitive user information beyond what's needed + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types diff --git a/api/prds/active/api_collections_sharing_summary.md b/api/prds/active/api_collections_sharing_summary.md new file mode 100644 index 000000000..4e888f90b --- /dev/null +++ b/api/prds/active/api_collections_sharing_summary.md @@ -0,0 +1,104 @@ +# API Collections Sharing Endpoints - Summary PRD + +## Overview +This document provides a high-level summary of the API collections sharing endpoints implementation and outlines the development sequence for the individual components. + +## Problem Statement +Currently, there is no way to manage sharing permissions for collections through REST endpoints. Users need to be able to share collections with other users, update sharing permissions, and remove sharing permissions through REST endpoints. + +## Implementation Components +The implementation is broken down into the following components, each with its own detailed PRD: + +1. **List Collections Sharing Endpoint** - GET /collections/:id/sharing + - PRD: [api_collections_sharing_list.md](/Users/dallin/buster/buster/api/prds/active/api_collections_sharing_list.md) + +2. **Create Collections Sharing Endpoint** - POST /collections/:id/sharing + - PRD: [api_collections_sharing_create.md](/Users/dallin/buster/buster/api/prds/active/api_collections_sharing_create.md) + +3. **Update Collections Sharing Endpoint** - PUT /collections/:id/sharing + - PRD: [api_collections_sharing_update.md](/Users/dallin/buster/buster/api/prds/active/api_collections_sharing_update.md) + +4. **Delete Collections Sharing Endpoint** - DELETE /collections/:id/sharing + - PRD: [api_collections_sharing_delete.md](/Users/dallin/buster/buster/api/prds/active/api_collections_sharing_delete.md) + +## PRD Development Sequence and Parallelization + +### PRD Development Order +The PRDs can be developed in the following order, with opportunities for parallel work: + +1. **First: List Collections Sharing Endpoint PRD** (api_collections_sharing_list.md) + - This PRD should be completed first as it establishes the basic data structures and permission checking patterns that other PRDs will build upon. + - It introduces the core response types and error handling approaches. + +2. **Second (Can be done in parallel):** + - **Create Collections Sharing Endpoint PRD** (api_collections_sharing_create.md) + - **Delete Collections Sharing Endpoint PRD** (api_collections_sharing_delete.md) + - These PRDs can be worked on simultaneously by different team members after the List PRD is complete. + - They use different sharing library functions and have minimal dependencies on each other. + +3. **Third: Update Collections Sharing Endpoint PRD** (api_collections_sharing_update.md) + - This PRD should be completed after the Create PRD since it builds on similar concepts and uses the same underlying sharing library functions. + - The update endpoint reuses many patterns from the create endpoint with slight modifications. + +### Dependencies Between PRDs +- The List PRD establishes patterns for permission checking and response structures. +- The Create and Delete PRDs are independent of each other but depend on patterns from the List PRD. +- The Update PRD builds upon the Create PRD's approach to modifying permissions. + +## Implementation Sequence and Parallelization + +### Phase 1: Foundation (Sequential) +1. Set up the directory structure for sharing handlers and endpoints + - Create `/src/routes/rest/routes/collections/sharing/mod.rs` + - Create `/libs/handlers/src/collections/sharing/mod.rs` + - Update `/src/routes/rest/routes/collections/mod.rs` to include the sharing router + - Update `/libs/handlers/src/collections/mod.rs` to export the sharing module + +### Phase 2: Core Endpoints (Can be Parallelized) +After Phase 1 is complete, the following components can be implemented in parallel by different developers: + +- **List Sharing Endpoint** + - Uses `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs` + - Uses `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Create Sharing Endpoint** + - Uses `find_user_by_email` from `@[api/libs/sharing/src]/user_lookup.rs` + - Uses `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Update Sharing Endpoint** + - Uses `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Delete Sharing Endpoint** + - Uses `remove_share_by_email` from `@[api/libs/sharing/src]/remove_asset_permissions.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +### Phase 3: Integration and Testing (Sequential) +1. Integration testing of all endpoints together +2. Manual testing with Postman/curl +3. Performance testing +4. Documentation updates + +## Dependencies +All components depend on the sharing library at `@[api/libs/sharing/src]`, which provides the core functionality for managing asset permissions. The specific dependencies are: + +- `/api/libs/sharing/src/user_lookup.rs` - User lookup by email +- `/api/libs/sharing/src/create_asset_permission.rs` - Create/update permissions +- `/api/libs/sharing/src/remove_asset_permissions.rs` - Remove permissions +- `/api/libs/sharing/src/list_asset_permissions.rs` - List permissions +- `/api/libs/sharing/src/check_asset_permission.rs` - Check permissions +- `/api/libs/sharing/src/types.rs` - Shared types for permissions + +## Security Considerations +- All endpoints require authentication +- Only users with appropriate permissions can manage sharing +- Input validation must be thorough to prevent security issues +- Email addresses must be properly validated and resolved to user IDs +- Permission checks must be enforced for all operations + +## Rollback Plan +If issues are discovered: +1. Revert the changes to the affected files +2. Deploy the previous version +3. Investigate and fix the issues in a new PR diff --git a/api/prds/active/api_collections_sharing_update.md b/api/prds/active/api_collections_sharing_update.md new file mode 100644 index 000000000..0d0691201 --- /dev/null +++ b/api/prds/active/api_collections_sharing_update.md @@ -0,0 +1,205 @@ +# API Collections Sharing - Update Endpoint PRD + +## Problem Statement +Users need the ability to update sharing permissions for collections through a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: PUT +- **Path**: /collections/:id/sharing +- **Description**: Updates sharing permissions for a collection +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the collection + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct SharingRequest { + pub emails: Vec, + pub role: AssetPermissionRole, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/collections/sharing/update_sharing.rs` - REST handler for updating sharing permissions +2. `/libs/handlers/src/collections/sharing/update_sharing_handler.rs` - Business logic for updating sharing permissions + +#### REST Handler Implementation +```rust +// update_sharing.rs +pub async fn update_collection_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing PUT request for collection sharing with ID: {}, user_id: {}", id, user.id); + + match update_collection_sharing_handler(&id, &user.id, request.emails, request.role).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions updated successfully".to_string())), + Err(e) => { + tracing::error!("Error updating sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Collection not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to update sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// update_sharing_handler.rs +pub async fn update_collection_sharing_handler( + collection_id: &Uuid, + user_id: &Uuid, + emails: Vec, + role: AssetPermissionRole, +) -> Result<()> { + // 1. Validate the collection exists + let collection = match get_collection_by_id(collection_id).await { + Ok(Some(collection)) => collection, + Ok(None) => return Err(anyhow!("Collection not found")), + Err(e) => return Err(anyhow!("Error fetching collection: {}", e)), + }; + + // 2. Check if user has permission to update sharing for the collection (Owner or FullAccess) + let has_permission = has_permission( + *collection_id, + AssetType::Collection, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to update sharing for this collection")); + } + + // 3. Process each email and update sharing permissions + for email in emails { + // The create_share_by_email function handles both creation and updates + // It performs an upsert operation in the database + match create_share_by_email( + &email, + *collection_id, + AssetType::Collection, + role, + *user_id, + ).await { + Ok(_) => { + tracing::info!("Updated sharing permission for email: {} on collection: {}", email, collection_id); + }, + Err(e) => { + tracing::error!("Failed to update sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to update sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to update sharing for the collection. + +2. `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs`: +```rust +pub async fn create_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + role: AssetPermissionRole, + created_by: Uuid, +) -> Result +``` +This function is used for both creating and updating permissions. It performs an upsert operation in the database. + +### Key Differences from Create Endpoint +While the update endpoint uses the same `create_share_by_email` function as the create endpoint, there are some key differences in its usage: + +1. **Semantic Difference**: The PUT method indicates an update operation, while POST indicates creation. +2. **Expected Behavior**: The update endpoint is expected to modify existing permissions, while the create endpoint is expected to add new ones. +3. **Error Handling**: The update endpoint might handle "permission not found" differently than the create endpoint. +4. **Documentation**: The API documentation will describe these endpoints differently to users. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the collection doesn't exist +- 403 Forbidden - If the user doesn't have permission to update sharing for the collection +- 400 Bad Request - For invalid email addresses or roles +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- Roles must be valid AssetPermissionRole values +- The collection ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent collections +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing updates + +#### Integration Tests +- Test PUT /collections/:id/sharing with valid ID, authorized user, and valid emails +- Test PUT /collections/:id/sharing with valid ID, unauthorized user +- Test PUT /collections/:id/sharing with non-existent collection ID +- Test PUT /collections/:id/sharing with invalid email formats +- Test PUT /collections/:id/sharing with non-existent user emails +- Test PUT /collections/:id/sharing with invalid roles + +#### Test Cases +1. Should update sharing permissions for valid emails and roles +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when collection doesn't exist +4. Should return 400 when email is invalid +5. Should return 400 when role is invalid + +### Performance Considerations +- For bulk updates with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of update operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can update sharing +- Validate email addresses to prevent injection attacks +- Validate roles to prevent privilege escalation +- Implement rate limiting to prevent abuse + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing update operations by user for audit purposes diff --git a/api/prds/active/api_dashboards_sharing_create.md b/api/prds/active/api_dashboards_sharing_create.md new file mode 100644 index 000000000..c09b6c08a --- /dev/null +++ b/api/prds/active/api_dashboards_sharing_create.md @@ -0,0 +1,206 @@ +# API Dashboards Sharing - Create Endpoint PRD + +## Problem Statement +Users need the ability to share dashboards with other users via a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: POST +- **Path**: /dashboards/:id/sharing +- **Description**: Shares a dashboard with specified users +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the dashboard + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct SharingRequest { + pub emails: Vec, + pub role: AssetPermissionRole, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/dashboards/sharing/create_sharing.rs` - REST handler for creating sharing permissions +2. `/libs/handlers/src/dashboards/sharing/create_sharing_handler.rs` - Business logic for creating sharing permissions + +#### REST Handler Implementation +```rust +// create_sharing.rs +pub async fn create_dashboard_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing POST request for dashboard sharing with ID: {}, user_id: {}", id, user.id); + + match create_dashboard_sharing_handler(&id, &user.id, request.emails, request.role).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions created successfully".to_string())), + Err(e) => { + tracing::error!("Error creating sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Dashboard not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// create_sharing_handler.rs +pub async fn create_dashboard_sharing_handler( + dashboard_id: &Uuid, + user_id: &Uuid, + emails: Vec, + role: AssetPermissionRole, +) -> Result<()> { + // 1. Validate the dashboard exists + let dashboard = match get_dashboard_by_id(dashboard_id).await { + Ok(Some(dashboard)) => dashboard, + Ok(None) => return Err(anyhow!("Dashboard not found")), + Err(e) => return Err(anyhow!("Error fetching dashboard: {}", e)), + }; + + // 2. Check if user has permission to share the dashboard (Owner or FullAccess) + let has_permission = has_permission( + *dashboard_id, + AssetType::Dashboard, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to share this dashboard")); + } + + // 3. Process each email and create sharing permissions + for email in emails { + // Create or update the permission using create_share_by_email + match create_share_by_email( + &email, + *dashboard_id, + AssetType::Dashboard, + role, + *user_id, + ).await { + Ok(_) => { + tracing::info!("Created sharing permission for email: {} on dashboard: {}", email, dashboard_id); + }, + Err(e) => { + tracing::error!("Failed to create sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to create sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to share the dashboard. + +2. `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs`: +```rust +pub async fn create_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + role: AssetPermissionRole, + created_by: Uuid, +) -> Result +``` +This function creates or updates an asset permission for a user identified by email. It handles: +- Email validation +- User lookup by email +- Permission creation or update +- Error handling for invalid emails or non-existent users + +3. `find_user_by_email` from `@[api/libs/sharing/src]/user_lookup.rs` (used internally by `create_share_by_email`): +```rust +pub async fn find_user_by_email(email: &str) -> Result> +``` +This function looks up a user by their email address, which is necessary for resolving email addresses to user IDs. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the dashboard doesn't exist +- 403 Forbidden - If the user doesn't have permission to share the dashboard +- 400 Bad Request - For invalid email addresses or roles +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- Roles must be valid AssetPermissionRole values +- The dashboard ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent dashboards +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing creation + +#### Integration Tests +- Test POST /dashboards/:id/sharing with valid ID, authorized user, and valid emails +- Test POST /dashboards/:id/sharing with valid ID, unauthorized user +- Test POST /dashboards/:id/sharing with non-existent dashboard ID +- Test POST /dashboards/:id/sharing with invalid email formats +- Test POST /dashboards/:id/sharing with non-existent user emails +- Test POST /dashboards/:id/sharing with invalid roles + +#### Test Cases +1. Should create sharing permissions for valid emails and roles +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when dashboard doesn't exist +4. Should return 400 when email is invalid +5. Should return 400 when role is invalid + +### Performance Considerations +- For bulk sharing with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of sharing operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can share dashboards +- Validate email addresses to prevent injection attacks +- Validate roles to prevent privilege escalation +- Implement rate limiting to prevent abuse + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing operations by user for audit purposes diff --git a/api/prds/active/api_dashboards_sharing_delete.md b/api/prds/active/api_dashboards_sharing_delete.md new file mode 100644 index 000000000..c30a47e3d --- /dev/null +++ b/api/prds/active/api_dashboards_sharing_delete.md @@ -0,0 +1,191 @@ +# API Dashboards Sharing - Delete Endpoint PRD + +## Problem Statement +Users need the ability to remove sharing permissions for dashboards through a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: DELETE +- **Path**: /dashboards/:id/sharing +- **Description**: Removes sharing permissions for a dashboard +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the dashboard + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct DeleteSharingRequest { + pub emails: Vec, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/dashboards/sharing/delete_sharing.rs` - REST handler for deleting sharing permissions +2. `/libs/handlers/src/dashboards/sharing/delete_sharing_handler.rs` - Business logic for deleting sharing permissions + +#### REST Handler Implementation +```rust +// delete_sharing.rs +pub async fn delete_dashboard_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing DELETE request for dashboard sharing with ID: {}, user_id: {}", id, user.id); + + match delete_dashboard_sharing_handler(&id, &user.id, request.emails).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions removed successfully".to_string())), + Err(e) => { + tracing::error!("Error removing sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Dashboard not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to remove sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// delete_sharing_handler.rs +pub async fn delete_dashboard_sharing_handler( + dashboard_id: &Uuid, + user_id: &Uuid, + emails: Vec, +) -> Result<()> { + // 1. Validate the dashboard exists + let dashboard = match get_dashboard_by_id(dashboard_id).await { + Ok(Some(dashboard)) => dashboard, + Ok(None) => return Err(anyhow!("Dashboard not found")), + Err(e) => return Err(anyhow!("Error fetching dashboard: {}", e)), + }; + + // 2. Check if user has permission to remove sharing for the dashboard (Owner or FullAccess) + let has_permission = has_permission( + *dashboard_id, + AssetType::Dashboard, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to remove sharing for this dashboard")); + } + + // 3. Process each email and remove sharing permissions + for email in emails { + match remove_share_by_email( + &email, + *dashboard_id, + AssetType::Dashboard, + ).await { + Ok(_) => { + tracing::info!("Removed sharing permission for email: {} on dashboard: {}", email, dashboard_id); + }, + Err(e) => { + tracing::error!("Failed to remove sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to remove sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to remove sharing for the dashboard. + +2. `remove_share_by_email` from `@[api/libs/sharing/src]/remove_asset_permissions.rs`: +```rust +pub async fn remove_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, +) -> Result<()> +``` +This function removes sharing permissions for a user identified by email. It handles: +- Email validation +- User lookup by email +- Permission removal +- Error handling for invalid emails or non-existent users + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the dashboard doesn't exist +- 403 Forbidden - If the user doesn't have permission to remove sharing for the dashboard +- 400 Bad Request - For invalid email addresses +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- The dashboard ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent dashboards +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing removal + +#### Integration Tests +- Test DELETE /dashboards/:id/sharing with valid ID, authorized user, and valid emails +- Test DELETE /dashboards/:id/sharing with valid ID, unauthorized user +- Test DELETE /dashboards/:id/sharing with non-existent dashboard ID +- Test DELETE /dashboards/:id/sharing with invalid email formats +- Test DELETE /dashboards/:id/sharing with non-existent user emails + +#### Test Cases +1. Should remove sharing permissions for valid emails +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when dashboard doesn't exist +4. Should return 400 when email is invalid +5. Should handle gracefully when trying to remove sharing for a user that doesn't have access + +### Performance Considerations +- For bulk removal with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of removal operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can remove sharing +- Validate email addresses to prevent injection attacks +- Implement rate limiting to prevent abuse +- Prevent removal of the owner's own access + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing removal operations by user for audit purposes diff --git a/api/prds/active/api_dashboards_sharing_list.md b/api/prds/active/api_dashboards_sharing_list.md new file mode 100644 index 000000000..f3865ff58 --- /dev/null +++ b/api/prds/active/api_dashboards_sharing_list.md @@ -0,0 +1,174 @@ +# API Dashboards Sharing - List Endpoint PRD + +## Problem Statement +Users need the ability to view all sharing permissions for a dashboard via a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: GET +- **Path**: /dashboards/:id/sharing +- **Description**: Lists all sharing permissions for a specific dashboard +- **Authentication**: Required +- **Authorization**: User must have at least ReadOnly access to the dashboard + +### Response Structure +```rust +#[derive(Debug, Serialize)] +pub struct SharingResponse { + pub permissions: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SharingPermission { + pub user_id: Uuid, + pub email: String, + pub name: Option, + pub avatar_url: Option, + pub role: AssetPermissionRole, +} +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/dashboards/sharing/list_sharing.rs` - REST handler for listing sharing permissions +2. `/libs/handlers/src/dashboards/sharing/list_sharing_handler.rs` - Business logic for listing sharing permissions + +#### REST Handler Implementation +```rust +// list_sharing.rs +pub async fn list_dashboard_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing GET request for dashboard sharing with ID: {}, user_id: {}", id, user.id); + + match list_dashboard_sharing_handler(&id, &user.id).await { + Ok(permissions) => { + let response = SharingResponse { + permissions: permissions.into_iter().map(|p| SharingPermission { + user_id: p.user.as_ref().map(|u| u.id).unwrap_or_default(), + email: p.user.as_ref().map(|u| u.email.clone()).unwrap_or_default(), + name: p.user.as_ref().and_then(|u| u.name.clone()), + avatar_url: p.user.as_ref().and_then(|u| u.avatar_url.clone()), + role: p.permission.role, + }).collect(), + }; + Ok(ApiResponse::Success(response)) + }, + Err(e) => { + tracing::error!("Error listing sharing permissions: {}", e); + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to list sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// list_sharing_handler.rs +pub async fn list_dashboard_sharing_handler( + dashboard_id: &Uuid, + user_id: &Uuid, +) -> Result> { + // 1. Validate the dashboard exists + let dashboard = match get_dashboard_by_id(dashboard_id).await { + Ok(Some(dashboard)) => dashboard, + Ok(None) => return Err(anyhow!("Dashboard not found")), + Err(e) => return Err(anyhow!("Error fetching dashboard: {}", e)), + }; + + // 2. Check if user has permission to view the dashboard + let user_role = check_access( + *dashboard_id, + AssetType::Dashboard, + *user_id, + IdentityType::User, + ).await?; + + if user_role.is_none() { + return Err(anyhow!("User does not have permission to view this dashboard")); + } + + // 3. Get all permissions for the dashboard + let permissions = list_shares( + *dashboard_id, + AssetType::Dashboard, + ).await?; + + Ok(permissions) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn check_access( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, +) -> Result> +``` +This function is used to verify that the user has permission to view the dashboard. It returns the user's role for the asset, or None if they don't have access. + +2. `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs`: +```rust +pub async fn list_shares( + asset_id: Uuid, + asset_type: AssetType, +) -> Result> +``` +This function retrieves all permissions for a specified asset, including user information. It filters out soft-deleted permissions and returns a structured response. + +3. The `AssetPermissionWithUser` type from `@[api/libs/sharing/src]/types.rs`: +```rust +pub struct AssetPermissionWithUser { + pub permission: SerializableAssetPermission, + pub user: Option, +} +``` +This type combines permission data with user information for a comprehensive response. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the dashboard doesn't exist +- 403 Forbidden - If the user doesn't have permission to view the dashboard +- 500 Internal Server Error - For database errors or other unexpected issues + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent dashboards +- Test error handling for unauthorized users +- Test mapping from `AssetPermissionWithUser` to `SharingPermission` + +#### Integration Tests +- Test GET /dashboards/:id/sharing with valid ID and authorized user +- Test GET /dashboards/:id/sharing with valid ID and unauthorized user +- Test GET /dashboards/:id/sharing with non-existent dashboard ID +- Test GET /dashboards/:id/sharing with dashboard that has no sharing permissions + +#### Test Cases +1. Should return all sharing permissions for a dashboard when user has access +2. Should return 403 when user doesn't have access to the dashboard +3. Should return 404 when dashboard doesn't exist +4. Should return empty array when no sharing permissions exist + +### Performance Considerations +- The `list_shares` function performs a database join between asset_permissions and users tables +- For dashboards with many sharing permissions, consider pagination in a future enhancement + +### Security Considerations +- Ensure that only users with at least ReadOnly access can view sharing permissions +- Validate the dashboard ID to prevent injection attacks +- Do not expose sensitive user information beyond what's needed + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types diff --git a/api/prds/active/api_dashboards_sharing_summary.md b/api/prds/active/api_dashboards_sharing_summary.md new file mode 100644 index 000000000..19ba1fd59 --- /dev/null +++ b/api/prds/active/api_dashboards_sharing_summary.md @@ -0,0 +1,104 @@ +# API Dashboards Sharing Endpoints - Summary PRD + +## Overview +This document provides a high-level summary of the API dashboards sharing endpoints implementation and outlines the development sequence for the individual components. + +## Problem Statement +Currently, there is no way to manage sharing permissions for dashboards through REST endpoints. Users need to be able to share dashboards with other users, update sharing permissions, and remove sharing permissions through REST endpoints. + +## Implementation Components +The implementation is broken down into the following components, each with its own detailed PRD: + +1. **List Dashboards Sharing Endpoint** - GET /dashboards/:id/sharing + - PRD: [api_dashboards_sharing_list.md](/Users/dallin/buster/buster/api/prds/active/api_dashboards_sharing_list.md) + +2. **Create Dashboards Sharing Endpoint** - POST /dashboards/:id/sharing + - PRD: [api_dashboards_sharing_create.md](/Users/dallin/buster/buster/api/prds/active/api_dashboards_sharing_create.md) + +3. **Update Dashboards Sharing Endpoint** - PUT /dashboards/:id/sharing + - PRD: [api_dashboards_sharing_update.md](/Users/dallin/buster/buster/api/prds/active/api_dashboards_sharing_update.md) + +4. **Delete Dashboards Sharing Endpoint** - DELETE /dashboards/:id/sharing + - PRD: [api_dashboards_sharing_delete.md](/Users/dallin/buster/buster/api/prds/active/api_dashboards_sharing_delete.md) + +## PRD Development Sequence and Parallelization + +### PRD Development Order +The PRDs can be developed in the following order, with opportunities for parallel work: + +1. **First: List Dashboards Sharing Endpoint PRD** (api_dashboards_sharing_list.md) + - This PRD should be completed first as it establishes the basic data structures and permission checking patterns that other PRDs will build upon. + - It introduces the core response types and error handling approaches. + +2. **Second (Can be done in parallel):** + - **Create Dashboards Sharing Endpoint PRD** (api_dashboards_sharing_create.md) + - **Delete Dashboards Sharing Endpoint PRD** (api_dashboards_sharing_delete.md) + - These PRDs can be worked on simultaneously by different team members after the List PRD is complete. + - They use different sharing library functions and have minimal dependencies on each other. + +3. **Third: Update Dashboards Sharing Endpoint PRD** (api_dashboards_sharing_update.md) + - This PRD should be completed after the Create PRD since it builds on similar concepts and uses the same underlying sharing library functions. + - The update endpoint reuses many patterns from the create endpoint with slight modifications. + +### Dependencies Between PRDs +- The List PRD establishes patterns for permission checking and response structures. +- The Create and Delete PRDs are independent of each other but depend on patterns from the List PRD. +- The Update PRD builds upon the Create PRD's approach to modifying permissions. + +## Implementation Sequence and Parallelization + +### Phase 1: Foundation (Sequential) +1. Set up the directory structure for sharing handlers and endpoints + - Create `/src/routes/rest/routes/dashboards/sharing/mod.rs` + - Create `/libs/handlers/src/dashboards/sharing/mod.rs` + - Update `/src/routes/rest/routes/dashboards/mod.rs` to include the sharing router + - Update `/libs/handlers/src/dashboards/mod.rs` to export the sharing module + +### Phase 2: Core Endpoints (Can be Parallelized) +After Phase 1 is complete, the following components can be implemented in parallel by different developers: + +- **List Sharing Endpoint** + - Uses `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs` + - Uses `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Create Sharing Endpoint** + - Uses `find_user_by_email` from `@[api/libs/sharing/src]/user_lookup.rs` + - Uses `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Update Sharing Endpoint** + - Uses `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Delete Sharing Endpoint** + - Uses `remove_share_by_email` from `@[api/libs/sharing/src]/remove_asset_permissions.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +### Phase 3: Integration and Testing (Sequential) +1. Integration testing of all endpoints together +2. Manual testing with Postman/curl +3. Performance testing +4. Documentation updates + +## Dependencies +All components depend on the sharing library at `@[api/libs/sharing/src]`, which provides the core functionality for managing asset permissions. The specific dependencies are: + +- `/api/libs/sharing/src/user_lookup.rs` - User lookup by email +- `/api/libs/sharing/src/create_asset_permission.rs` - Create/update permissions +- `/api/libs/sharing/src/remove_asset_permissions.rs` - Remove permissions +- `/api/libs/sharing/src/list_asset_permissions.rs` - List permissions +- `/api/libs/sharing/src/check_asset_permission.rs` - Check permissions +- `/api/libs/sharing/src/types.rs` - Shared types for permissions + +## Security Considerations +- All endpoints require authentication +- Only users with appropriate permissions can manage sharing +- Input validation must be thorough to prevent security issues +- Email addresses must be properly validated and resolved to user IDs +- Permission checks must be enforced for all operations + +## Rollback Plan +If issues are discovered: +1. Revert the changes to the affected files +2. Deploy the previous version +3. Investigate and fix the issues in a new PR diff --git a/api/prds/active/api_dashboards_sharing_update.md b/api/prds/active/api_dashboards_sharing_update.md new file mode 100644 index 000000000..99b284be6 --- /dev/null +++ b/api/prds/active/api_dashboards_sharing_update.md @@ -0,0 +1,205 @@ +# API Dashboards Sharing - Update Endpoint PRD + +## Problem Statement +Users need the ability to update sharing permissions for dashboards through a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: PUT +- **Path**: /dashboards/:id/sharing +- **Description**: Updates sharing permissions for a dashboard +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the dashboard + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct SharingRequest { + pub emails: Vec, + pub role: AssetPermissionRole, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/dashboards/sharing/update_sharing.rs` - REST handler for updating sharing permissions +2. `/libs/handlers/src/dashboards/sharing/update_sharing_handler.rs` - Business logic for updating sharing permissions + +#### REST Handler Implementation +```rust +// update_sharing.rs +pub async fn update_dashboard_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing PUT request for dashboard sharing with ID: {}, user_id: {}", id, user.id); + + match update_dashboard_sharing_handler(&id, &user.id, request.emails, request.role).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions updated successfully".to_string())), + Err(e) => { + tracing::error!("Error updating sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Dashboard not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to update sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// update_sharing_handler.rs +pub async fn update_dashboard_sharing_handler( + dashboard_id: &Uuid, + user_id: &Uuid, + emails: Vec, + role: AssetPermissionRole, +) -> Result<()> { + // 1. Validate the dashboard exists + let dashboard = match get_dashboard_by_id(dashboard_id).await { + Ok(Some(dashboard)) => dashboard, + Ok(None) => return Err(anyhow!("Dashboard not found")), + Err(e) => return Err(anyhow!("Error fetching dashboard: {}", e)), + }; + + // 2. Check if user has permission to update sharing for the dashboard (Owner or FullAccess) + let has_permission = has_permission( + *dashboard_id, + AssetType::Dashboard, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to update sharing for this dashboard")); + } + + // 3. Process each email and update sharing permissions + for email in emails { + // The create_share_by_email function handles both creation and updates + // It performs an upsert operation in the database + match create_share_by_email( + &email, + *dashboard_id, + AssetType::Dashboard, + role, + *user_id, + ).await { + Ok(_) => { + tracing::info!("Updated sharing permission for email: {} on dashboard: {}", email, dashboard_id); + }, + Err(e) => { + tracing::error!("Failed to update sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to update sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to update sharing for the dashboard. + +2. `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs`: +```rust +pub async fn create_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + role: AssetPermissionRole, + created_by: Uuid, +) -> Result +``` +This function is used for both creating and updating permissions. It performs an upsert operation in the database. + +### Key Differences from Create Endpoint +While the update endpoint uses the same `create_share_by_email` function as the create endpoint, there are some key differences in its usage: + +1. **Semantic Difference**: The PUT method indicates an update operation, while POST indicates creation. +2. **Expected Behavior**: The update endpoint is expected to modify existing permissions, while the create endpoint is expected to add new ones. +3. **Error Handling**: The update endpoint might handle "permission not found" differently than the create endpoint. +4. **Documentation**: The API documentation will describe these endpoints differently to users. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the dashboard doesn't exist +- 403 Forbidden - If the user doesn't have permission to update sharing for the dashboard +- 400 Bad Request - For invalid email addresses or roles +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- Roles must be valid AssetPermissionRole values +- The dashboard ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent dashboards +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing updates + +#### Integration Tests +- Test PUT /dashboards/:id/sharing with valid ID, authorized user, and valid emails +- Test PUT /dashboards/:id/sharing with valid ID, unauthorized user +- Test PUT /dashboards/:id/sharing with non-existent dashboard ID +- Test PUT /dashboards/:id/sharing with invalid email formats +- Test PUT /dashboards/:id/sharing with non-existent user emails +- Test PUT /dashboards/:id/sharing with invalid roles + +#### Test Cases +1. Should update sharing permissions for valid emails and roles +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when dashboard doesn't exist +4. Should return 400 when email is invalid +5. Should return 400 when role is invalid + +### Performance Considerations +- For bulk updates with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of update operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can update sharing +- Validate email addresses to prevent injection attacks +- Validate roles to prevent privilege escalation +- Implement rate limiting to prevent abuse + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing update operations by user for audit purposes diff --git a/api/prds/active/api_metrics_rest_endpoints.md b/api/prds/active/api_metrics_rest_endpoints.md deleted file mode 100644 index b7e2361b3..000000000 --- a/api/prds/active/api_metrics_rest_endpoints.md +++ /dev/null @@ -1,181 +0,0 @@ -# API Metrics REST Endpoints - -## Problem Statement -Currently, the metrics API only supports GET operations through REST endpoints, which limits the ability to manage metrics through the API. Users need to be able to update and delete metrics through REST endpoints to provide a complete CRUD interface for metrics management. - -### Current State -- The metrics REST API currently only supports: - - GET /metrics - List metrics - - GET /metrics/:id - Get a specific metric - - GET /metrics/:id/data - Get metric data - -### Desired State -- Add the following endpoints: - - PUT /metrics/:id - Update a metric - - DELETE /metrics/:id - Delete a metric - -### Impact -- Enables full CRUD operations for metrics through REST API -- Improves developer experience by providing consistent API patterns -- Allows for programmatic management of metrics - -## Technical Design - -### Components Affected -- REST API routes for metrics -- Metrics handlers in the handlers library - -### New Files -1. `/src/routes/rest/routes/metrics/update_metric.rs` - REST handler for updating metrics -2. `/src/routes/rest/routes/metrics/delete_metric.rs` - REST handler for deleting metrics -3. `/libs/handlers/src/metrics/update_metric_handler.rs` - Business logic for updating metrics -4. `/libs/handlers/src/metrics/delete_metric_handler.rs` - Business logic for deleting metrics - -### Modified Files -1. `/src/routes/rest/routes/metrics/mod.rs` - Add new routes -2. `/libs/handlers/src/metrics/mod.rs` - Export new handlers -3. `/libs/handlers/src/metrics/types.rs` - Add new request types if needed - -### Detailed Design - -#### 1. Update Metric Endpoint (PUT /metrics/:id) - -**Request Structure:** -```rust -#[derive(Debug, Deserialize)] -pub struct UpdateMetricRequest { - pub title: Option, - pub sql: Option, - pub chart_config: Option, - pub status: Option, - pub file: Option, -} -``` - -**Handler Implementation:** -```rust -// update_metric.rs -pub async fn update_metric_rest_handler( - Extension(user): Extension, - Path(id): Path, - Json(request): Json, -) -> Result, (StatusCode, String)> { - tracing::info!( - "Processing PUT request for metric with ID: {}, user_id: {}", - id, - user.id - ); - - match update_metric_handler(&id, &user.id, request).await { - Ok(updated_metric) => Ok(ApiResponse::JsonData(updated_metric)), - Err(e) => { - tracing::error!("Error updating metric: {}", e); - Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to update metric: {}", e))) - } - } -} -``` - -**Business Logic:** -The update_metric_handler will: -1. Validate the user has permission to update the metric -2. Fetch the existing metric -3. Update the fields provided in the request -4. Update the MetricYml content based on the provided fields -5. Save the updated metric to the database -6. Return the updated metric - -#### 2. Delete Metric Endpoint (DELETE /metrics/:id) - -**Handler Implementation:** -```rust -// delete_metric.rs -pub async fn delete_metric_rest_handler( - Extension(user): Extension, - Path(id): Path, -) -> Result, (StatusCode, String)> { - tracing::info!( - "Processing DELETE request for metric with ID: {}, user_id: {}", - id, - user.id - ); - - match delete_metric_handler(&id, &user.id).await { - Ok(_) => Ok(ApiResponse::Success("Metric deleted successfully".to_string())), - Err(e) => { - tracing::error!("Error deleting metric: {}", e); - Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete metric: {}", e))) - } - } -} -``` - -**Business Logic:** -The delete_metric_handler will: -1. Validate the user has permission to delete the metric -2. Soft delete the metric by setting the deleted_at field to the current UTC timestamp -3. Return success - -### Access Control -For the initial implementation, we will stub out access controls. Future iterations will implement proper access control based on the organization and user permissions. - -## Implementation Plan - -### Phase 1: Update Metric Endpoint -1. Create update_metric_handler.rs in libs/handlers/src/metrics -2. Update libs/handlers/src/metrics/mod.rs to export the new handler -3. Create update_metric.rs in src/routes/rest/routes/metrics -4. Update src/routes/rest/routes/metrics/mod.rs to include the new route -5. Test the endpoint with Postman/curl - -### Phase 2: Delete Metric Endpoint -1. Create delete_metric_handler.rs in libs/handlers/src/metrics -2. Update libs/handlers/src/metrics/mod.rs to export the new handler -3. Create delete_metric.rs in src/routes/rest/routes/metrics -4. Update src/routes/rest/routes/metrics/mod.rs to include the new route -5. Test the endpoint with Postman/curl - -## Testing Strategy - -### Unit Tests -- Test update_metric_handler with various input combinations -- Test delete_metric_handler for successful deletion and error cases - -### Integration Tests -- Test PUT /metrics/:id with valid and invalid payloads -- Test DELETE /metrics/:id with valid and invalid IDs -- Test error handling for both endpoints - -### Manual Testing -- Use Postman/curl to verify the endpoints work as expected -- Verify metrics are properly updated in the database -- Verify metrics are properly marked as deleted - -## Dependencies - -### Files -- `/libs/database/src/models.rs` - MetricFile model -- `/libs/database/src/schema.rs` - Database schema -- `/libs/database/src/types/metric_yml.rs` - MetricYml type -- `/libs/database/src/enums.rs` - Verification enum - -## File References -- `/src/routes/rest/routes/metrics/get_metric.rs` -- `/libs/handlers/src/metrics/get_metric_handler.rs` -- `/libs/handlers/src/metrics/types.rs` - -## Security Considerations -- All endpoints require authentication -- Authorization will be stubbed for now but should be implemented in the future -- Input validation must be thorough to prevent SQL injection - -## Monitoring and Logging -- All endpoint calls should be logged with tracing -- Errors should be logged with appropriate context -- Metrics should be collected for endpoint performance - -## Rollback Plan -If issues are discovered: -1. Revert the changes to the affected files -2. Deploy the previous version -3. Investigate and fix the issues in a new PR diff --git a/api/prds/active/api_metrics_sharing_create.md b/api/prds/active/api_metrics_sharing_create.md new file mode 100644 index 000000000..c829fa3ab --- /dev/null +++ b/api/prds/active/api_metrics_sharing_create.md @@ -0,0 +1,206 @@ +# API Metrics Sharing - Create Endpoint PRD + +## Problem Statement +Users need the ability to share metrics with other users via a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: POST +- **Path**: /metrics/:id/sharing +- **Description**: Shares a metric with specified users +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the metric + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct SharingRequest { + pub emails: Vec, + pub role: AssetPermissionRole, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/metrics/sharing/create_sharing.rs` - REST handler for creating sharing permissions +2. `/libs/handlers/src/metrics/sharing/create_sharing_handler.rs` - Business logic for creating sharing permissions + +#### REST Handler Implementation +```rust +// create_sharing.rs +pub async fn create_metric_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing POST request for metric sharing with ID: {}, user_id: {}", id, user.id); + + match create_metric_sharing_handler(&id, &user.id, request.emails, request.role).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions created successfully".to_string())), + Err(e) => { + tracing::error!("Error creating sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Metric not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// create_sharing_handler.rs +pub async fn create_metric_sharing_handler( + metric_id: &Uuid, + user_id: &Uuid, + emails: Vec, + role: AssetPermissionRole, +) -> Result<()> { + // 1. Validate the metric exists + let metric = match get_metric_by_id(metric_id).await { + Ok(Some(metric)) => metric, + Ok(None) => return Err(anyhow!("Metric not found")), + Err(e) => return Err(anyhow!("Error fetching metric: {}", e)), + }; + + // 2. Check if user has permission to share the metric (Owner or FullAccess) + let has_permission = has_permission( + *metric_id, + AssetType::MetricFile, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to share this metric")); + } + + // 3. Process each email and create sharing permissions + for email in emails { + // Create or update the permission using create_share_by_email + match create_share_by_email( + &email, + *metric_id, + AssetType::MetricFile, + role, + *user_id, + ).await { + Ok(_) => { + tracing::info!("Created sharing permission for email: {} on metric: {}", email, metric_id); + }, + Err(e) => { + tracing::error!("Failed to create sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to create sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to share the metric. + +2. `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs`: +```rust +pub async fn create_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + role: AssetPermissionRole, + created_by: Uuid, +) -> Result +``` +This function creates or updates an asset permission for a user identified by email. It handles: +- Email validation +- User lookup by email +- Permission creation or update +- Error handling for invalid emails or non-existent users + +3. `find_user_by_email` from `@[api/libs/sharing/src]/user_lookup.rs` (used internally by `create_share_by_email`): +```rust +pub async fn find_user_by_email(email: &str) -> Result> +``` +This function looks up a user by their email address, which is necessary for resolving email addresses to user IDs. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the metric doesn't exist +- 403 Forbidden - If the user doesn't have permission to share the metric +- 400 Bad Request - For invalid email addresses or roles +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- Roles must be valid AssetPermissionRole values +- The metric ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent metrics +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing creation + +#### Integration Tests +- Test POST /metrics/:id/sharing with valid ID, authorized user, and valid emails +- Test POST /metrics/:id/sharing with valid ID, unauthorized user +- Test POST /metrics/:id/sharing with non-existent metric ID +- Test POST /metrics/:id/sharing with invalid email formats +- Test POST /metrics/:id/sharing with non-existent user emails +- Test POST /metrics/:id/sharing with invalid roles + +#### Test Cases +1. Should create sharing permissions for valid emails and roles +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when metric doesn't exist +4. Should return 400 when email is invalid +5. Should return 400 when role is invalid + +### Performance Considerations +- For bulk sharing with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of sharing operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can share metrics +- Validate email addresses to prevent injection attacks +- Validate roles to prevent privilege escalation +- Implement rate limiting to prevent abuse + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing operations by user for audit purposes diff --git a/api/prds/active/api_metrics_sharing_delete.md b/api/prds/active/api_metrics_sharing_delete.md new file mode 100644 index 000000000..03d32a02e --- /dev/null +++ b/api/prds/active/api_metrics_sharing_delete.md @@ -0,0 +1,260 @@ +# API Metrics Sharing - Delete Endpoint PRD + +## Problem Statement +Users need the ability to remove sharing permissions for metrics through a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: DELETE +- **Path**: /metrics/:id/sharing +- **Description**: Removes sharing permissions for a metric +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the metric + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct DeleteSharingRequest { + pub emails: Vec, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/metrics/sharing/delete_sharing.rs` - REST handler for deleting sharing permissions +2. `/libs/handlers/src/metrics/sharing/delete_sharing_handler.rs` - Business logic for deleting sharing permissions + +#### REST Handler Implementation +```rust +// delete_sharing.rs +pub async fn delete_metric_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing DELETE request for metric sharing with ID: {}, user_id: {}", id, user.id); + + match delete_metric_sharing_handler(&id, &user.id, request.emails).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions deleted successfully".to_string())), + Err(e) => { + tracing::error!("Error deleting sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Metric not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// delete_sharing_handler.rs +pub async fn delete_metric_sharing_handler( + metric_id: &Uuid, + user_id: &Uuid, + emails: Vec, +) -> Result<()> { + // 1. Validate the metric exists + let metric = match get_metric_by_id(metric_id).await { + Ok(Some(metric)) => metric, + Ok(None) => return Err(anyhow!("Metric not found")), + Err(e) => return Err(anyhow!("Error fetching metric: {}", e)), + }; + + // 2. Check if user has permission to delete sharing for the metric (Owner or FullAccess) + let has_permission = has_permission( + *metric_id, + AssetType::MetricFile, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to delete sharing for this metric")); + } + + // 3. Process each email and delete sharing permissions + for email in emails { + // The remove_share_by_email function handles soft deletion of permissions + match remove_share_by_email( + &email, + *metric_id, + AssetType::MetricFile, + *user_id, + ).await { + Ok(_) => { + tracing::info!("Deleted sharing permission for email: {} on metric: {}", email, metric_id); + }, + Err(e) => { + // If the error is because the permission doesn't exist, we can ignore it + if e.to_string().contains("No active permission found") { + tracing::warn!("No active permission found for email {}: {}", email, e); + continue; + } + + tracing::error!("Failed to delete sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to delete sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to delete sharing for the metric. + +2. `remove_share_by_email` from `@[api/libs/sharing/src]/remove_asset_permissions.rs`: +```rust +pub async fn remove_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + updated_by: Uuid, +) -> Result<()> +``` +This function handles the soft deletion of permissions by email. Here's how it works internally: + +```rust +// From the implementation of remove_share_by_email +pub async fn remove_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + updated_by: Uuid, +) -> Result<()> { + // Validate email format + if !email.contains('@') { + return Err(SharingError::InvalidEmail(email.to_string()).into()); + } + + // Find the user by email + let user = match find_user_by_email(email).await? { + Some(user) => user, + None => return Err(SharingError::UserNotFound(email.to_string()).into()), + }; + + // Remove the permission + remove_share( + user.id, + IdentityType::User, + asset_id, + asset_type, + updated_by, + ) + .await +} +``` + +3. `find_user_by_email` from `@[api/libs/sharing/src]/user_lookup.rs` (used internally by `remove_share_by_email`): +```rust +pub async fn find_user_by_email(email: &str) -> Result> +``` +This function looks up a user by their email address, which is necessary for resolving email addresses to user IDs. + +### Soft Deletion Mechanism +The `remove_share_by_email` function performs a soft deletion by updating the `deleted_at` field in the database: + +```rust +// From the implementation of remove_share +// Soft delete - update the deleted_at field +let rows = diesel::update(asset_permissions::table) + .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()) + .set(( + asset_permissions::deleted_at.eq(now), + asset_permissions::updated_at.eq(now), + asset_permissions::updated_by.eq(updated_by), + )) + .execute(&mut conn) + .await + .context("Failed to remove asset permission")?; +``` + +This approach ensures that: +1. The permission record is preserved for audit purposes +2. The permission can be restored if needed +3. The permission won't be included in queries that filter for active permissions + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the metric doesn't exist +- 403 Forbidden - If the user doesn't have permission to delete sharing for the metric +- 400 Bad Request - For invalid email addresses +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- The metric ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent metrics +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing deletions + +#### Integration Tests +- Test DELETE /metrics/:id/sharing with valid ID, authorized user, and valid emails +- Test DELETE /metrics/:id/sharing with valid ID, unauthorized user +- Test DELETE /metrics/:id/sharing with non-existent metric ID +- Test DELETE /metrics/:id/sharing with invalid email formats +- Test DELETE /metrics/:id/sharing with non-existent user emails + +#### Test Cases +1. Should delete sharing permissions for valid emails +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when metric doesn't exist +4. Should return 400 when email is invalid +5. Should not error when trying to delete a non-existent permission + +### Performance Considerations +- For bulk deletions with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of delete operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can delete sharing +- Validate email addresses to prevent injection attacks +- Implement rate limiting to prevent abuse +- Consider adding additional checks to prevent users from removing their own Owner access + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing deletion operations by user for audit purposes diff --git a/api/prds/active/api_metrics_sharing_endpoints.md b/api/prds/active/api_metrics_sharing_endpoints.md deleted file mode 100644 index 6e1ceebb8..000000000 --- a/api/prds/active/api_metrics_sharing_endpoints.md +++ /dev/null @@ -1,251 +0,0 @@ -# API Metrics Sharing REST Endpoints - -## Problem Statement -Currently, there is no way to manage sharing permissions for metrics through REST endpoints. Users need to be able to share metrics with other users, update sharing permissions, and remove sharing permissions through REST endpoints. - -### Current State -- The metrics REST API currently only supports basic CRUD operations -- There is no way to manage sharing permissions for metrics through REST endpoints - -### Desired State -- Add the following endpoints: - - POST /metrics/:id/sharing - Share a metric with users - - PUT /metrics/:id/sharing - Update sharing permissions for users - - DELETE /metrics/:id/sharing - Remove sharing permissions for users - -### Impact -- Enables programmatic management of metric sharing permissions -- Improves collaboration capabilities for metrics -- Provides consistent API patterns for resource sharing across the application - -## Technical Design - -### Components Affected -- REST API routes for metrics -- Handlers for managing metric sharing permissions - -### New Files -1. `/src/routes/rest/routes/metrics/sharing/mod.rs` - Router configuration for sharing endpoints -2. `/src/routes/rest/routes/metrics/sharing/create_sharing.rs` - REST handler for creating sharing permissions -3. `/src/routes/rest/routes/metrics/sharing/update_sharing.rs` - REST handler for updating sharing permissions -4. `/src/routes/rest/routes/metrics/sharing/delete_sharing.rs` - REST handler for deleting sharing permissions -5. `/libs/handlers/src/metrics/sharing/mod.rs` - Export sharing handlers -6. `/libs/handlers/src/metrics/sharing/create_sharing_handler.rs` - Business logic for creating sharing permissions -7. `/libs/handlers/src/metrics/sharing/update_sharing_handler.rs` - Business logic for updating sharing permissions -8. `/libs/handlers/src/metrics/sharing/delete_sharing_handler.rs` - Business logic for deleting sharing permissions - -### Modified Files -1. `/src/routes/rest/routes/metrics/mod.rs` - Add sharing router -2. `/libs/handlers/src/metrics/mod.rs` - Export sharing module - -### Detailed Design - -#### 1. Create Sharing Endpoint (POST /metrics/:id/sharing) - -**Request Structure:** -```rust -#[derive(Debug, Deserialize)] -pub struct SharingRequest { - pub emails: Vec, - pub role: AssetPermissionRole, -} -``` - -**Handler Implementation:** -```rust -// create_sharing.rs -pub async fn create_metric_sharing_rest_handler( - Extension(user): Extension, - Path(id): Path, - Json(request): Json, -) -> Result, (StatusCode, String)> { - tracing::info!( - "Processing POST request for metric sharing with ID: {}, user_id: {}", - id, - user.id - ); - - match create_metric_sharing_handler(&id, &user.id, request.emails, request.role).await { - Ok(_) => Ok(ApiResponse::Success("Sharing permissions created successfully".to_string())), - Err(e) => { - tracing::error!("Error creating sharing permissions: {}", e); - Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create sharing permissions: {}", e))) - } - } -} -``` - -**Business Logic:** -The create_metric_sharing_handler will: -1. Validate the user has permission to share the metric -2. Validate the metric exists -3. Resolve email addresses to user IDs -4. Create AssetPermission entries for each user with the specified role -5. Return success - -#### 2. Update Sharing Endpoint (PUT /metrics/:id/sharing) - -**Request Structure:** -```rust -#[derive(Debug, Deserialize)] -pub struct SharingRequest { - pub emails: Vec, - pub role: AssetPermissionRole, -} -``` - -**Handler Implementation:** -```rust -// update_sharing.rs -pub async fn update_metric_sharing_rest_handler( - Extension(user): Extension, - Path(id): Path, - Json(request): Json, -) -> Result, (StatusCode, String)> { - tracing::info!( - "Processing PUT request for metric sharing with ID: {}, user_id: {}", - id, - user.id - ); - - match update_metric_sharing_handler(&id, &user.id, request.emails, request.role).await { - Ok(_) => Ok(ApiResponse::Success("Sharing permissions updated successfully".to_string())), - Err(e) => { - tracing::error!("Error updating sharing permissions: {}", e); - Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to update sharing permissions: {}", e))) - } - } -} -``` - -**Business Logic:** -The update_metric_sharing_handler will: -1. Validate the user has permission to update sharing for the metric -2. Validate the metric exists -3. Resolve email addresses to user IDs -4. Update existing AssetPermission entries for each user with the new role -5. Return success - -#### 3. Delete Sharing Endpoint (DELETE /metrics/:id/sharing) - -**Request Structure:** -```rust -#[derive(Debug, Deserialize)] -pub struct DeleteSharingRequest { - pub emails: Vec, -} -``` - -**Handler Implementation:** -```rust -// delete_sharing.rs -pub async fn delete_metric_sharing_rest_handler( - Extension(user): Extension, - Path(id): Path, - Json(request): Json, -) -> Result, (StatusCode, String)> { - tracing::info!( - "Processing DELETE request for metric sharing with ID: {}, user_id: {}", - id, - user.id - ); - - match delete_metric_sharing_handler(&id, &user.id, request.emails).await { - Ok(_) => Ok(ApiResponse::Success("Sharing permissions deleted successfully".to_string())), - Err(e) => { - tracing::error!("Error deleting sharing permissions: {}", e); - Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete sharing permissions: {}", e))) - } - } -} -``` - -**Business Logic:** -The delete_metric_sharing_handler will: -1. Validate the user has permission to delete sharing for the metric -2. Validate the metric exists -3. Resolve email addresses to user IDs -4. Soft delete AssetPermission entries for each user by setting deleted_at to current UTC timestamp -5. Return success - -### Database Operations - -For all endpoints, we'll be working with the AssetPermission table with the following fields: -- identity_id: The UUID of the user being granted access -- identity_type: Set to IdentityType::User -- asset_id: The UUID of the metric -- asset_type: Set to AssetType::MetricFile -- role: The AssetPermissionRole specified in the request -- created_at: Current UTC timestamp -- updated_at: Current UTC timestamp -- deleted_at: Null for active permissions, UTC timestamp for deleted permissions -- created_by: The UUID of the user making the request -- updated_by: The UUID of the user making the request - -## Implementation Plan - -### Phase 1: Create Sharing Endpoint -1. Create directory structure for sharing handlers and endpoints -2. Implement email to user ID resolution utility -3. Implement create_sharing_handler.rs -4. Implement create_sharing.rs REST endpoint -5. Update module exports -6. Test the endpoint - -### Phase 2: Update Sharing Endpoint -1. Implement update_sharing_handler.rs -2. Implement update_sharing.rs REST endpoint -3. Update module exports -4. Test the endpoint - -### Phase 3: Delete Sharing Endpoint -1. Implement delete_sharing_handler.rs -2. Implement delete_sharing.rs REST endpoint -3. Update module exports -4. Test the endpoint - -## Testing Strategy - -### Unit Tests -- Test email to user ID resolution -- Test permission validation logic -- Test database operations for creating, updating, and deleting permissions - -### Integration Tests -- Test POST /metrics/:id/sharing with valid and invalid inputs -- Test PUT /metrics/:id/sharing with valid and invalid inputs -- Test DELETE /metrics/:id/sharing with valid and invalid inputs -- Test error handling for all endpoints - -### Manual Testing -- Use Postman/curl to verify the endpoints work as expected -- Verify permissions are properly created, updated, and deleted in the database -- Verify access control works as expected after permissions are modified - -## Dependencies - -### Files -- `/libs/database/src/models.rs` - AssetPermission model -- `/libs/database/src/enums.rs` - AssetPermissionRole, IdentityType, and AssetType enums -- `/libs/database/src/schema.rs` - Database schema - -## File References -- `/src/routes/rest/routes/metrics/mod.rs` -- `/libs/handlers/src/metrics/mod.rs` - -## Security Considerations -- All endpoints require authentication -- Only users with appropriate permissions should be able to manage sharing -- Input validation must be thorough to prevent security issues -- Email addresses must be properly validated and resolved to user IDs - -## Monitoring and Logging -- All endpoint calls should be logged with tracing -- Errors should be logged with appropriate context -- Metrics should be collected for endpoint performance - -## Rollback Plan -If issues are discovered: -1. Revert the changes to the affected files -2. Deploy the previous version -3. Investigate and fix the issues in a new PR diff --git a/api/prds/active/api_metrics_sharing_list.md b/api/prds/active/api_metrics_sharing_list.md new file mode 100644 index 000000000..f947c1c02 --- /dev/null +++ b/api/prds/active/api_metrics_sharing_list.md @@ -0,0 +1,174 @@ +# API Metrics Sharing - List Endpoint PRD + +## Problem Statement +Users need the ability to view all sharing permissions for a metric via a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: GET +- **Path**: /metrics/:id/sharing +- **Description**: Lists all sharing permissions for a specific metric +- **Authentication**: Required +- **Authorization**: User must have at least ReadOnly access to the metric + +### Response Structure +```rust +#[derive(Debug, Serialize)] +pub struct SharingResponse { + pub permissions: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SharingPermission { + pub user_id: Uuid, + pub email: String, + pub name: Option, + pub avatar_url: Option, + pub role: AssetPermissionRole, +} +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/metrics/sharing/list_sharing.rs` - REST handler for listing sharing permissions +2. `/libs/handlers/src/metrics/sharing/list_sharing_handler.rs` - Business logic for listing sharing permissions + +#### REST Handler Implementation +```rust +// list_sharing.rs +pub async fn list_metric_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing GET request for metric sharing with ID: {}, user_id: {}", id, user.id); + + match list_metric_sharing_handler(&id, &user.id).await { + Ok(permissions) => { + let response = SharingResponse { + permissions: permissions.into_iter().map(|p| SharingPermission { + user_id: p.user.as_ref().map(|u| u.id).unwrap_or_default(), + email: p.user.as_ref().map(|u| u.email.clone()).unwrap_or_default(), + name: p.user.as_ref().and_then(|u| u.name.clone()), + avatar_url: p.user.as_ref().and_then(|u| u.avatar_url.clone()), + role: p.permission.role, + }).collect(), + }; + Ok(ApiResponse::Success(response)) + }, + Err(e) => { + tracing::error!("Error listing sharing permissions: {}", e); + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to list sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// list_sharing_handler.rs +pub async fn list_metric_sharing_handler( + metric_id: &Uuid, + user_id: &Uuid, +) -> Result> { + // 1. Validate the metric exists + let metric = match get_metric_by_id(metric_id).await { + Ok(Some(metric)) => metric, + Ok(None) => return Err(anyhow!("Metric not found")), + Err(e) => return Err(anyhow!("Error fetching metric: {}", e)), + }; + + // 2. Check if user has permission to view the metric + let user_role = check_access( + *metric_id, + AssetType::MetricFile, + *user_id, + IdentityType::User, + ).await?; + + if user_role.is_none() { + return Err(anyhow!("User does not have permission to view this metric")); + } + + // 3. Get all permissions for the metric + let permissions = list_shares( + *metric_id, + AssetType::MetricFile, + ).await?; + + Ok(permissions) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn check_access( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, +) -> Result> +``` +This function is used to verify that the user has permission to view the metric. It returns the user's role for the asset, or None if they don't have access. + +2. `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs`: +```rust +pub async fn list_shares( + asset_id: Uuid, + asset_type: AssetType, +) -> Result> +``` +This function retrieves all permissions for a specified asset, including user information. It filters out soft-deleted permissions and returns a structured response. + +3. The `AssetPermissionWithUser` type from `@[api/libs/sharing/src]/types.rs`: +```rust +pub struct AssetPermissionWithUser { + pub permission: SerializableAssetPermission, + pub user: Option, +} +``` +This type combines permission data with user information for a comprehensive response. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the metric doesn't exist +- 403 Forbidden - If the user doesn't have permission to view the metric +- 500 Internal Server Error - For database errors or other unexpected issues + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent metrics +- Test error handling for unauthorized users +- Test mapping from `AssetPermissionWithUser` to `SharingPermission` + +#### Integration Tests +- Test GET /metrics/:id/sharing with valid ID and authorized user +- Test GET /metrics/:id/sharing with valid ID and unauthorized user +- Test GET /metrics/:id/sharing with non-existent metric ID +- Test GET /metrics/:id/sharing with metric that has no sharing permissions + +#### Test Cases +1. Should return all sharing permissions for a metric when user has access +2. Should return 403 when user doesn't have access to the metric +3. Should return 404 when metric doesn't exist +4. Should return empty array when no sharing permissions exist + +### Performance Considerations +- The `list_shares` function performs a database join between asset_permissions and users tables +- For metrics with many sharing permissions, consider pagination in a future enhancement + +### Security Considerations +- Ensure that only users with at least ReadOnly access can view sharing permissions +- Validate the metric ID to prevent injection attacks +- Do not expose sensitive user information beyond what's needed + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types diff --git a/api/prds/active/api_metrics_sharing_summary.md b/api/prds/active/api_metrics_sharing_summary.md new file mode 100644 index 000000000..a0f2fbba3 --- /dev/null +++ b/api/prds/active/api_metrics_sharing_summary.md @@ -0,0 +1,104 @@ +# API Metrics Sharing Endpoints - Summary PRD + +## Overview +This document provides a high-level summary of the API metrics sharing endpoints implementation and outlines the development sequence for the individual components. + +## Problem Statement +Currently, there is no way to manage sharing permissions for metrics through REST endpoints. Users need to be able to share metrics with other users, update sharing permissions, and remove sharing permissions through REST endpoints. + +## Implementation Components +The implementation is broken down into the following components, each with its own detailed PRD: + +1. **List Metrics Sharing Endpoint** - GET /metrics/:id/sharing + - PRD: [api_metrics_sharing_list.md](/Users/dallin/buster/buster/api/prds/active/api_metrics_sharing_list.md) + +2. **Create Metrics Sharing Endpoint** - POST /metrics/:id/sharing + - PRD: [api_metrics_sharing_create.md](/Users/dallin/buster/buster/api/prds/active/api_metrics_sharing_create.md) + +3. **Update Metrics Sharing Endpoint** - PUT /metrics/:id/sharing + - PRD: [api_metrics_sharing_update.md](/Users/dallin/buster/buster/api/prds/active/api_metrics_sharing_update.md) + +4. **Delete Metrics Sharing Endpoint** - DELETE /metrics/:id/sharing + - PRD: [api_metrics_sharing_delete.md](/Users/dallin/buster/buster/api/prds/active/api_metrics_sharing_delete.md) + +## PRD Development Sequence and Parallelization + +### PRD Development Order +The PRDs can be developed in the following order, with opportunities for parallel work: + +1. **First: List Metrics Sharing Endpoint PRD** (api_metrics_sharing_list.md) + - This PRD should be completed first as it establishes the basic data structures and permission checking patterns that other PRDs will build upon. + - It introduces the core response types and error handling approaches. + +2. **Second (Can be done in parallel):** + - **Create Metrics Sharing Endpoint PRD** (api_metrics_sharing_create.md) + - **Delete Metrics Sharing Endpoint PRD** (api_metrics_sharing_delete.md) + - These PRDs can be worked on simultaneously by different team members after the List PRD is complete. + - They use different sharing library functions and have minimal dependencies on each other. + +3. **Third: Update Metrics Sharing Endpoint PRD** (api_metrics_sharing_update.md) + - This PRD should be completed after the Create PRD since it builds on similar concepts and uses the same underlying sharing library functions. + - The update endpoint reuses many patterns from the create endpoint with slight modifications. + +### Dependencies Between PRDs +- The List PRD establishes patterns for permission checking and response structures. +- The Create and Delete PRDs are independent of each other but depend on patterns from the List PRD. +- The Update PRD builds upon the Create PRD's approach to modifying permissions. + +## Implementation Sequence and Parallelization + +### Phase 1: Foundation (Sequential) +1. Set up the directory structure for sharing handlers and endpoints + - Create `/src/routes/rest/routes/metrics/sharing/mod.rs` + - Create `/libs/handlers/src/metrics/sharing/mod.rs` + - Update `/src/routes/rest/routes/metrics/mod.rs` to include the sharing router + - Update `/libs/handlers/src/metrics/mod.rs` to export the sharing module + +### Phase 2: Core Endpoints (Can be Parallelized) +After Phase 1 is complete, the following components can be implemented in parallel by different developers: + +- **List Sharing Endpoint** + - Uses `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs` + - Uses `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Create Sharing Endpoint** + - Uses `find_user_by_email` from `@[api/libs/sharing/src]/user_lookup.rs` + - Uses `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Update Sharing Endpoint** + - Uses `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +- **Delete Sharing Endpoint** + - Uses `remove_share_by_email` from `@[api/libs/sharing/src]/remove_asset_permissions.rs` + - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs` + +### Phase 3: Integration and Testing (Sequential) +1. Integration testing of all endpoints together +2. Manual testing with Postman/curl +3. Performance testing +4. Documentation updates + +## Dependencies +All components depend on the sharing library at `@[api/libs/sharing/src]`, which provides the core functionality for managing asset permissions. The specific dependencies are: + +- `/api/libs/sharing/src/user_lookup.rs` - User lookup by email +- `/api/libs/sharing/src/create_asset_permission.rs` - Create/update permissions +- `/api/libs/sharing/src/remove_asset_permissions.rs` - Remove permissions +- `/api/libs/sharing/src/list_asset_permissions.rs` - List permissions +- `/api/libs/sharing/src/check_asset_permission.rs` - Check permissions +- `/api/libs/sharing/src/types.rs` - Shared types for permissions + +## Security Considerations +- All endpoints require authentication +- Only users with appropriate permissions can manage sharing +- Input validation must be thorough to prevent security issues +- Email addresses must be properly validated and resolved to user IDs +- Permission checks must be enforced for all operations + +## Rollback Plan +If issues are discovered: +1. Revert the changes to the affected files +2. Deploy the previous version +3. Investigate and fix the issues in a new PR diff --git a/api/prds/active/api_metrics_sharing_update.md b/api/prds/active/api_metrics_sharing_update.md new file mode 100644 index 000000000..3c284b571 --- /dev/null +++ b/api/prds/active/api_metrics_sharing_update.md @@ -0,0 +1,228 @@ +# API Metrics Sharing - Update Endpoint PRD + +## Problem Statement +Users need the ability to update sharing permissions for metrics through a REST API endpoint. + +## Technical Design + +### Endpoint Specification +- **Method**: PUT +- **Path**: /metrics/:id/sharing +- **Description**: Updates sharing permissions for a metric +- **Authentication**: Required +- **Authorization**: User must have Owner or FullAccess permission for the metric + +### Request Structure +```rust +#[derive(Debug, Deserialize)] +pub struct SharingRequest { + pub emails: Vec, + pub role: AssetPermissionRole, +} +``` + +### Response Structure +```rust +// Success response is a simple message +// Error responses include appropriate status codes and error messages +``` + +### Implementation Details + +#### New Files +1. `/src/routes/rest/routes/metrics/sharing/update_sharing.rs` - REST handler for updating sharing permissions +2. `/libs/handlers/src/metrics/sharing/update_sharing_handler.rs` - Business logic for updating sharing permissions + +#### REST Handler Implementation +```rust +// update_sharing.rs +pub async fn update_metric_sharing_rest_handler( + Extension(user): Extension, + Path(id): Path, + Json(request): Json, +) -> Result, (StatusCode, String)> { + tracing::info!("Processing PUT request for metric sharing with ID: {}, user_id: {}", id, user.id); + + match update_metric_sharing_handler(&id, &user.id, request.emails, request.role).await { + Ok(_) => Ok(ApiResponse::Success("Sharing permissions updated successfully".to_string())), + Err(e) => { + tracing::error!("Error updating sharing permissions: {}", e); + + // Map specific errors to appropriate status codes + if e.to_string().contains("not found") { + return Err((StatusCode::NOT_FOUND, format!("Metric not found: {}", e))); + } else if e.to_string().contains("permission") { + return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e))); + } else if e.to_string().contains("invalid email") { + return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e))); + } + + Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to update sharing permissions: {}", e))) + } + } +} +``` + +#### Handler Implementation +```rust +// update_sharing_handler.rs +pub async fn update_metric_sharing_handler( + metric_id: &Uuid, + user_id: &Uuid, + emails: Vec, + role: AssetPermissionRole, +) -> Result<()> { + // 1. Validate the metric exists + let metric = match get_metric_by_id(metric_id).await { + Ok(Some(metric)) => metric, + Ok(None) => return Err(anyhow!("Metric not found")), + Err(e) => return Err(anyhow!("Error fetching metric: {}", e)), + }; + + // 2. Check if user has permission to update sharing for the metric (Owner or FullAccess) + let has_permission = has_permission( + *metric_id, + AssetType::MetricFile, + *user_id, + IdentityType::User, + AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions + ).await?; + + if !has_permission { + return Err(anyhow!("User does not have permission to update sharing for this metric")); + } + + // 3. Process each email and update sharing permissions + for email in emails { + // The create_share_by_email function handles both creation and updates + // It performs an upsert operation in the database + match create_share_by_email( + &email, + *metric_id, + AssetType::MetricFile, + role, + *user_id, + ).await { + Ok(_) => { + tracing::info!("Updated sharing permission for email: {} on metric: {}", email, metric_id); + }, + Err(e) => { + tracing::error!("Failed to update sharing for email {}: {}", email, e); + return Err(anyhow!("Failed to update sharing for email {}: {}", email, e)); + } + } + } + + Ok(()) +} +``` + +### Sharing Library Integration +This endpoint leverages the following functions from the sharing library: + +1. `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`: +```rust +pub async fn has_permission( + asset_id: Uuid, + asset_type: AssetType, + identity_id: Uuid, + identity_type: IdentityType, + required_role: AssetPermissionRole, +) -> Result +``` +This function checks if a user has the required permission level for an asset. It's used to verify that the user has Owner or FullAccess permission to update sharing for the metric. + +2. `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.rs`: +```rust +pub async fn create_share_by_email( + email: &str, + asset_id: Uuid, + asset_type: AssetType, + role: AssetPermissionRole, + created_by: Uuid, +) -> Result +``` +This function is used for both creating and updating permissions. It performs an upsert operation in the database: + +```rust +// From the implementation of create_share_by_email +// This shows how it handles the upsert operation +let permission = diesel::insert_into(asset_permissions::table) + .values(&new_permission) + .on_conflict(( + asset_permissions::identity_id, + asset_permissions::identity_type, + asset_permissions::asset_id, + asset_permissions::asset_type, + )) + .do_update() + .set(( + asset_permissions::role.eq(excluded(asset_permissions::role)), + asset_permissions::updated_at.eq(now), + asset_permissions::updated_by.eq(created_by), + asset_permissions::deleted_at.eq(None), // Undelete if previously deleted + )) + .get_result::(&mut conn) + .await + .context("Failed to create or update asset permission")?; +``` + +### Key Differences from Create Endpoint +While the update endpoint uses the same `create_share_by_email` function as the create endpoint, there are some key differences in its usage: + +1. **Semantic Difference**: The PUT method indicates an update operation, while POST indicates creation. +2. **Expected Behavior**: The update endpoint is expected to modify existing permissions, while the create endpoint is expected to add new ones. +3. **Error Handling**: The update endpoint might handle "permission not found" differently than the create endpoint. +4. **Documentation**: The API documentation will describe these endpoints differently to users. + +### Error Handling +The handler will return appropriate error responses: +- 404 Not Found - If the metric doesn't exist +- 403 Forbidden - If the user doesn't have permission to update sharing for the metric +- 400 Bad Request - For invalid email addresses or roles +- 500 Internal Server Error - For database errors or other unexpected issues + +### Input Validation +- Email addresses must be properly formatted (contains '@') +- Roles must be valid AssetPermissionRole values +- The metric ID must be a valid UUID + +### Testing Strategy + +#### Unit Tests +- Test permission validation logic +- Test error handling for non-existent metrics +- Test error handling for unauthorized users +- Test error handling for invalid emails +- Test successful sharing updates + +#### Integration Tests +- Test PUT /metrics/:id/sharing with valid ID, authorized user, and valid emails +- Test PUT /metrics/:id/sharing with valid ID, unauthorized user +- Test PUT /metrics/:id/sharing with non-existent metric ID +- Test PUT /metrics/:id/sharing with invalid email formats +- Test PUT /metrics/:id/sharing with non-existent user emails +- Test PUT /metrics/:id/sharing with invalid roles + +#### Test Cases +1. Should update sharing permissions for valid emails and roles +2. Should return 403 when user doesn't have Owner or FullAccess permission +3. Should return 404 when metric doesn't exist +4. Should return 400 when email is invalid +5. Should return 400 when role is invalid + +### Performance Considerations +- For bulk updates with many emails, consider implementing a background job for processing +- Monitor database performance for large batches of update operations + +### Security Considerations +- Ensure that only users with Owner or FullAccess permission can update sharing +- Validate email addresses to prevent injection attacks +- Validate roles to prevent privilege escalation +- Implement rate limiting to prevent abuse + +### Monitoring +- Log all requests with appropriate context +- Track performance metrics for the endpoint +- Monitor error rates and types +- Track sharing update operations by user for audit purposes diff --git a/api/prds/active/api_sharing_endpoints_master_summary.md b/api/prds/active/api_sharing_endpoints_master_summary.md new file mode 100644 index 000000000..10d164b5e --- /dev/null +++ b/api/prds/active/api_sharing_endpoints_master_summary.md @@ -0,0 +1,136 @@ +# API Sharing Endpoints - Master Summary PRD + +## Overview +This document provides a high-level summary of all sharing endpoints for different asset types (metrics, dashboards, collections, and chats) and outlines the development strategy across these components. + +## Problem Statement +Currently, there is no way to manage sharing permissions for various asset types through REST endpoints. Users need to be able to share assets with other users, update sharing permissions, and remove sharing permissions through REST endpoints. + +## Asset Types +The sharing endpoints will be implemented for the following asset types: + +1. **Metrics** + - Summary PRD: [api_metrics_sharing_summary.md](/Users/dallin/buster/buster/api/prds/active/api_metrics_sharing_summary.md) + +2. **Dashboards** + - Summary PRD: [api_dashboards_sharing_summary.md](/Users/dallin/buster/buster/api/prds/active/api_dashboards_sharing_summary.md) + +3. **Collections** + - Summary PRD: [api_collections_sharing_summary.md](/Users/dallin/buster/buster/api/prds/active/api_collections_sharing_summary.md) + +4. **Chats** + - Summary PRD: [api_chats_sharing_summary.md](/Users/dallin/buster/buster/api/prds/active/api_chats_sharing_summary.md) + +## Development Strategy + +### Cross-Asset Type Parallelization +The development of sharing endpoints can be parallelized across asset types. Different teams or developers can work on different asset types simultaneously: + +1. **Team A**: Metrics sharing endpoints +2. **Team B**: Dashboards sharing endpoints +3. **Team C**: Collections sharing endpoints +4. **Team D**: Chats sharing endpoints + +### Within-Asset Type Sequencing +For each asset type, the development should follow this sequence: + +1. First: List Sharing Endpoint (GET /:asset-type/:id/sharing) +2. Second (can be done in parallel): + - Create Sharing Endpoint (POST /:asset-type/:id/sharing) + - Delete Sharing Endpoint (DELETE /:asset-type/:id/sharing) +3. Third: Update Sharing Endpoint (PUT /:asset-type/:id/sharing) + +### Priority Order +If resources are limited and sequential development is necessary, the following priority order is recommended: + +1. Metrics sharing endpoints (highest priority) +2. Dashboards sharing endpoints +3. Collections sharing endpoints +4. Chats sharing endpoints (lowest priority) + +## Shared Components and Code Reuse + +### Common Response Types +All sharing endpoints will use the same response structure: + +```rust +pub struct SharingResponse { + pub permissions: Vec, +} + +pub struct SharingPermission { + pub user_id: Uuid, + pub email: String, + pub name: Option, + pub avatar_url: Option, + pub role: AssetPermissionRole, +} +``` + +### Common Request Types +Create and update endpoints will use the same request structure: + +```rust +pub struct SharingRequest { + pub emails: Vec, + pub role: AssetPermissionRole, +} +``` + +Delete endpoints will use: + +```rust +pub struct DeleteSharingRequest { + pub emails: Vec, +} +``` + +### Shared Library Functions +All endpoints will leverage the same sharing library functions: + +- `check_access` and `has_permission` from `check_asset_permission.rs` +- `list_shares` from `list_asset_permissions.rs` +- `create_share_by_email` from `create_asset_permission.rs` +- `remove_share_by_email` from `remove_asset_permissions.rs` +- `find_user_by_email` from `user_lookup.rs` + +## Testing Strategy + +### Unit Tests +Each endpoint should have unit tests covering: +- Permission validation logic +- Error handling +- Mapping between API types and sharing library types + +### Integration Tests +Integration tests should cover: +- Valid and invalid inputs for each endpoint +- Error handling +- Permission checks + +### Cross-Asset Type Tests +Tests should verify that sharing works consistently across different asset types. + +## Security Considerations +- All endpoints require authentication +- Only users with appropriate permissions can manage sharing +- Input validation must be thorough to prevent security issues +- Email addresses must be properly validated and resolved to user IDs +- Permission checks must be enforced for all operations + +## Rollout Plan +1. Deploy metrics sharing endpoints first +2. Monitor for issues and gather feedback +3. Roll out remaining asset types in priority order +4. Update documentation and notify users of new sharing capabilities + +## Dependencies +All components depend on the sharing library at `@[api/libs/sharing/src]`, which provides the core functionality for managing asset permissions. + +## Timeline Estimation +- Metrics sharing endpoints: 1 week +- Dashboards sharing endpoints: 1 week +- Collections sharing endpoints: 1 week +- Chats sharing endpoints: 1 week + +Total estimated time: 4 weeks if done sequentially, or 1-2 weeks if done in parallel. diff --git a/api/prds/active/enhancement_streaming_library_agent_integration.md b/api/prds/active/enhancement_streaming_library_agent_integration.md deleted file mode 100644 index f7e589570..000000000 --- a/api/prds/active/enhancement_streaming_library_agent_integration.md +++ /dev/null @@ -1,506 +0,0 @@ -# Streaming Library Enhancement for Agent Integration - -## Problem Statement - -The current implementation of the chat handling system in `libs/handlers/src/chats/post_chat_handler.rs` lacks clean abstraction for managing streaming logic for agent messages. The post chat handler is responsible for too many concerns, including agent instantiation, chat context loading, message processing, and streaming management. This has led to: - -1. Difficulty in maintaining and extending the streaming functionality -2. Lack of clear separation between different message types (assistant text, assistant tool, tool output) -3. Inconsistent chunk tracking across message types -4. Complex code that is difficult to test and debug - -### Current Limitations - -- The streaming library does not handle caching and chunk tracking for all tool calls consistently -- There's no clear distinction between assistant tool calls and tool outputs -- The chunk tracking mechanism is not centralized, making it difficult to ensure all message chunks are properly tracked -- The `post_chat_handler.rs` has too many responsibilities, making it hard to maintain and extend -- No consistent way to collect and store reasoning messages for later display - -### Impact - -- **User Impact**: Inconsistent user experience when streaming messages, especially with complex tool calls -- **System Impact**: Increased complexity and potential for bugs in message handling -- **Business Impact**: Slower development velocity due to complex codebase, increased maintenance cost - -## Requirements - -### Functional Requirements - -#### Core Functionality - -- The streaming library must handle all streaming logic for agent messages -- It must support different LiteLlmMessage types (Assistant with tool calls, Assistant with content, Tool messages) -- It must track and cache all tool calls and their outputs using shared IDs -- It must store messages for redisplay -- It must support both reasoning and response messages - -#### Message Handling - -- **Message Parsing**: The StreamingParser must parse and process LiteLlmMessage types - - Acceptance Criteria: Successfully parse and process all LiteLlmMessage variants - - Dependencies: Current StreamingParser implementation - -- **Tool Call Tracking**: The system must track tool calls and their outputs using shared IDs - - Acceptance Criteria: Successfully associate tool calls with their outputs - - Dependencies: LiteLlmMessage format from Agent - -- **Message Storage**: The system must store messages for future reference - - Acceptance Criteria: Successfully retrieve stored messages - - Dependencies: StreamingParser implementation - -#### Post Chat Handler Integration - -- **Simplified Interface**: The post chat handler must have a clean interface for streaming - - Acceptance Criteria: Post chat handler code is significantly simplified - - Dependencies: Current post_chat_handler implementation - -### Non-Functional Requirements - -- **Performance Requirements** - - Message processing must not introduce significant latency (<50ms per message) - - Memory usage should be optimized for large message streams - -- **Maintainability Requirements** - - Clear separation of concerns between components - - Comprehensive test coverage (>80%) for all new components - - Well-documented public API - -- **Compatibility Requirements** - - Must be backward compatible with existing processors and message formats - - Must integrate seamlessly with the current agent implementation - -## Technical Design - -### System Architecture - -```mermaid -graph TD - A[Post Chat Handler] --> B[Enhanced StreamingParser] - D[Agent] -- "LiteLlmMessages" --> A - B -- "Store Messages" --> B -``` - -### Core Components - -#### Component 1: Message Types - -```rust -/// Represents the different types of LiteLlmMessages that can be processed -pub enum MessageType { - /// An Assistant message with tool calls (not null) - AssistantToolCall, - /// An Assistant message with content (text response) - AssistantResponse, - /// A Tool message (output from executed tool call) - ToolOutput, -} - -/// A tool call with its associated information -pub struct ToolCallInfo { - /// The ID of the tool call - pub id: String, - /// The name of the tool - pub name: String, - /// The input parameters - pub input: Value, - /// The output content (if available) - pub output: Option, - /// The timestamp when the tool call was created - pub timestamp: DateTime, - /// The current state of the tool call - pub state: ToolCallState, - /// The chunks received so far for this tool call - pub chunks: Vec, -} - -/// The state of a tool call -pub enum ToolCallState { - /// The tool call is in progress - InProgress, - /// The tool call is complete - Complete, - /// The tool call has an output - HasOutput, -} - -/// A processed message -pub struct ProcessedMessage { - /// The ID of the message - pub id: String, - /// The type of the message - pub message_type: MessageType, - /// The processed content - pub content: ProcessedOutput, - /// The timestamp when the message was created - pub timestamp: DateTime, -} -``` - -#### Component 2: Enhanced StreamingParser - -```rust -/// Enhanced StreamingParser with support for LiteLlmMessage types -pub struct StreamingParser { - /// Buffer for incomplete JSON - buffer: String, - /// Registry of processors for different message types - processors: ProcessorRegistry, - /// Map of tool call IDs to their information - tool_calls: HashMap, - /// List of reasoning messages (tool calls and outputs) - reasoning_messages: Vec, - /// List of response messages - response_messages: Vec, -} - -impl StreamingParser { - /// Creates a new StreamingParser - pub fn new() -> Self { - Self { - buffer: String::new(), - processors: ProcessorRegistry::new(), - tool_calls: HashMap::new(), - reasoning_messages: Vec::new(), - response_messages: Vec::new(), - } - } - - /// Process a LiteLlmMessage - pub fn process_message(&mut self, message: &LiteLlmMessage) -> Result> { - match message { - LiteLlmMessage::Assistant { tool_calls: Some(tool_calls), .. } => { - self.process_assistant_tool_call(message, tool_calls) - }, - LiteLlmMessage::Assistant { content: Some(content), tool_calls: None, .. } => { - self.process_assistant_response(message, content) - }, - LiteLlmMessage::Tool { content, tool_call_id, .. } => { - self.process_tool_output(message, tool_call_id, content) - }, - _ => Ok(None), // Ignore other message types - } - } - - /// Process an Assistant message with tool calls - fn process_assistant_tool_call( - &mut self, - message: &LiteLlmMessage, - tool_calls: &[ToolCall] - ) -> Result> { - for tool_call in tool_calls { - let id = tool_call.id.clone(); - let name = tool_call.function.name.clone(); - let arguments = tool_call.function.arguments.clone(); - - // Parse arguments as JSON - let input = serde_json::from_str::(&arguments) - .unwrap_or_else(|_| serde_json::json!({"raw": arguments})); - - // Register or update tool call - if let Some(existing_tool_call) = self.tool_calls.get_mut(&id) { - // Update existing tool call with new chunks - existing_tool_call.chunks.push(arguments.clone()); - existing_tool_call.input = input.clone(); - if existing_tool_call.state == ToolCallState::InProgress { - existing_tool_call.state = ToolCallState::Complete; - } - } else { - // Register new tool call - self.tool_calls.insert(id.clone(), ToolCallInfo { - id: id.clone(), - name: name.clone(), - input: input.clone(), - output: None, - timestamp: Utc::now(), - state: ToolCallState::Complete, - chunks: vec![arguments.clone()], - }); - } - - // Process with appropriate processor - if let Some(processor) = self.processors.get_processor_for_tool(&name) { - let processed = processor.process(&input)?; - - // Store as reasoning message - self.add_reasoning_message(id.clone(), MessageType::AssistantToolCall, processed.clone()); - - return Ok(Some(processed)); - } - } - - Ok(None) - } - - /// Process an Assistant message with content (text response) - fn process_assistant_response( - &mut self, - message: &LiteLlmMessage, - content: &str - ) -> Result> { - // For response messages, we just store the text - self.response_messages.push(content.to_string()); - - // Create a simple processed output - let processed = ProcessedOutput::Text(ReasoningText { - id: message.get_id().unwrap_or_else(|| Uuid::new_v4().to_string()), - reasoning_type: "response".to_string(), - title: "Assistant Response".to_string(), - secondary_title: "".to_string(), - message: Some(content.to_string()), - message_chunk: None, - status: Some("complete".to_string()), - }); - - Ok(Some(processed)) - } - - /// Process a Tool message (output from executed tool call) - fn process_tool_output( - &mut self, - message: &LiteLlmMessage, - tool_call_id: &str, - content: &str - ) -> Result> { - // Parse content as JSON if possible - let output = serde_json::from_str::(content) - .unwrap_or_else(|_| serde_json::json!({"text": content})); - - // Update tool call with output - if let Some(tool_call) = self.tool_calls.get_mut(tool_call_id) { - tool_call.output = Some(output.clone()); - tool_call.state = ToolCallState::HasOutput; - - // Get the tool name - let name = tool_call.name.clone(); - - // Process with appropriate processor - if let Some(processor) = self.processors.get_processor_for_tool(&name) { - let processed = processor.process_output(&output)?; - - // Store as reasoning message - self.add_reasoning_message( - tool_call_id.to_string(), - MessageType::ToolOutput, - processed.clone() - ); - - return Ok(Some(processed)); - } - } - - Ok(None) - } - - /// Adds a reasoning message - fn add_reasoning_message(&mut self, id: String, message_type: MessageType, content: ProcessedOutput) { - self.reasoning_messages.push(ProcessedMessage { - id, - message_type, - content, - timestamp: Utc::now(), - }); - } - - /// Gets all reasoning messages - pub fn get_reasoning_messages(&self) -> &[ProcessedMessage] { - &self.reasoning_messages - } - - /// Gets all response messages - pub fn get_response_messages(&self) -> &[String] { - &self.response_messages - } - - /// Gets all tool calls - pub fn get_tool_calls(&self) -> &HashMap { - &self.tool_calls - } - - /// Gets a specific tool call by ID - pub fn get_tool_call(&self, id: &str) -> Option<&ToolCallInfo> { - self.tool_calls.get(id) - } - - /// Registers a processor for a specific tool - pub fn register_processor(&mut self, name: &str, processor: Box) { - self.processors.register_tool_processor(name, processor); - } -} -``` - -### Data Flow - -The streaming library enhancement will follow this simplified data flow: - -1. **Agent Execution**: The agent runs and produces LiteLlmMessage objects (Assistant with tool calls, Assistant with content, Tool messages) -2. **Message Streaming**: LiteLlmMessages stream into the post_chat_handler -3. **Parsing and Processing**: Messages are passed to the StreamingParser, which: - - Identifies the message type (AssistantToolCall, AssistantResponse, ToolOutput) - - Processes messages through appropriate processors - - Tracks tool calls and their outputs using shared IDs - - Stores messages internally for later retrieval -4. **Message Storage**: After the agent finishes execution, the collected messages are retrieved from the StreamingParser for storage and display - -This flow ensures a clean separation of concerns while minimizing the number of components: - -- StreamingParser handles both real-time processing and message storage -- Tool calls and their outputs are linked using shared IDs -- All chunk tracking is handled within the StreamingParser - -### Example Usage in post_chat_handler - -```rust -pub async fn post_chat_handler( - request: ChatCreateNewChat, - user: AuthenticatedUser, - tx: Option>>, -) -> Result { - // Initialize enhanced StreamingParser - let mut streaming_parser = StreamingParser::new(); - - // Register processors for different tool types - streaming_parser.register_processor("create_plan", Box::new(CreatePlanProcessor::new())); - streaming_parser.register_processor("create_metrics", Box::new(CreateMetricsProcessor::new())); - // ... register other processors - - // Initialize agent and get stream receiver - let mut agent = BusterSuperAgent::new(user.clone(), chat_id).await?; - let mut chat = AgentThread::new(Some(chat_id), user.id, initial_messages); - let mut rx = agent.run(&mut chat).await?; - - // Process streaming messages - while let Ok(message_result) = rx.recv().await { - match message_result { - Ok(message) => { - // Process the LiteLlmMessage with the StreamingParser - if let Some(processed) = streaming_parser.process_message(&message)? { - // Send to client if tx is available - if let Some(tx) = &tx { - match message { - LiteLlmMessage::Assistant { tool_calls: Some(_), .. } => { - let event = ThreadEvent::ReasoningMessage { - id: message.get_id().unwrap_or_default(), - content: processed, - }; - tx.send(Ok((BusterContainer::new(), event))).await?; - }, - LiteLlmMessage::Assistant { content: Some(_), .. } => { - let event = ThreadEvent::ResponseMessage { - content: processed, - }; - tx.send(Ok((BusterContainer::new(), event))).await?; - }, - LiteLlmMessage::Tool { .. } => { - let event = ThreadEvent::ReasoningMessage { - id: message.get_id().unwrap_or_default(), - content: processed, - }; - tx.send(Ok((BusterContainer::new(), event))).await?; - }, - _ => {} - } - } - } - }, - Err(e) => { - tracing::error!("Error receiving message: {}", e); - return Err(anyhow!("Error receiving message: {}", e)); - } - } - } - - // After agent execution, collect all messages - let reasoning_messages = streaming_parser.get_reasoning_messages(); - let response_messages = streaming_parser.get_response_messages(); - - // Create chat with messages - // ... -} -``` - -## Implementation Plan - -### Phase 1: Core Functionality - -1. ✅ **Update Message Types** - - ✅ Define MessageType enum for different message types - - ✅ Create ToolCallInfo struct for managing tool calls - - ✅ Define ToolCallState enum for tracking tool call state - - ✅ Create ProcessedMessage struct for encapsulating processed messages - -2. ✅ **Enhance StreamingParser** - - ✅ Update the StreamingParser to handle LiteLlmMessage types - - ✅ Implement methods for processing different message types - - ✅ Add storage for tool calls, reasoning messages, and response messages - -### Phase 2: Integration - -1. ✅ **Update ProcessorRegistry** - - ✅ Enhance the ProcessorRegistry to support tool-specific processors - - ✅ Add methods for retrieving processors by tool name - -2. **Integrate with post_chat_handler** - - Update post_chat_handler to use the enhanced StreamingParser - - Simplify the message handling logic in post_chat_handler - -3. **Phase 3: Testing and Validation** - - Write unit tests for the enhanced StreamingParser - - Write integration tests for the entire flow - - Validate that all requirements are met - -## Testing Strategy - -### Unit Testing - -1. **StreamingParser Tests** - - Test processing of different LiteLlmMessage types - - Test tool call tracking and association with outputs - - Test message storage and retrieval - -2. **ProcessorRegistry Tests** - - Test registration and retrieval of processors - - Test processor selection based on tool name - -### Integration Testing - -1. **End-to-End Flow Tests** - - Test the entire flow from agent execution to message display - - Verify that all message types are correctly processed and stored - -2. **Performance Tests** - - Test with large message streams to ensure performance requirements are met - - Measure latency and memory usage - -## Success Criteria - -1. **Functional Success** - - All LiteLlmMessage types are correctly processed - - Tool calls and their outputs are correctly associated using shared IDs - - Messages are stored and can be retrieved for display - -2. **Non-Functional Success** - - Performance requirements are met - - Code is maintainable and well-documented - - Test coverage is at least 80% - -## Risks and Mitigations - -1. **Risk**: Incompatibility with existing processors - - **Mitigation**: Ensure backward compatibility by maintaining the existing processor interface - -2. **Risk**: Performance degradation with large message streams - - **Mitigation**: Implement efficient caching and chunk tracking mechanisms - -3. **Risk**: Incomplete or malformed messages in the stream - - **Mitigation**: Implement robust error handling and recovery mechanisms - -## Appendix - -### Glossary - -- **LiteLlmMessage**: A message from the LiteLLM library, which can be an Assistant message with tool calls, an Assistant message with content, or a Tool message -- **Tool Call**: A request from the assistant to execute a tool -- **Tool Output**: The result of executing a tool -- **StreamingParser**: The component responsible for parsing and processing messages -- **ProcessorRegistry**: A registry of processors for different message types diff --git a/api/prds/active/sharing_access_controls.md b/api/prds/active/sharing_access_controls.md deleted file mode 100644 index 5382f3897..000000000 --- a/api/prds/active/sharing_access_controls.md +++ /dev/null @@ -1,180 +0,0 @@ -# Sharing Access Controls PRD - -## Overview -This PRD outlines the implementation of sharing access controls for assets within the system. The sharing functionality will allow users to grant permissions to other users for specific assets, manage those permissions, and check access rights. - -## Background -The system needs a robust permission system to control access to various assets. Each asset has an owner and can be shared with other users with different permission levels. The sharing functionality will be implemented in the `libs/sharing` library. - -## Goals -- ✅ Implement a modular, testable sharing access control system -- ✅ Support creating, updating, removing, and listing asset permissions -- ✅ Enable permission checks for assets -- ✅ Provide email-based user lookup for sharing functionality - -## Non-Goals -- Implementing UI components for sharing -- Handling organization-wide permission policies -- Implementing batch operations beyond what's specified - -## Technical Design - -The implementation will be divided into the following components: - -### 1. User Lookup by Email ✅ - -Create a module to look up users by their email addresses, which will be used when sharing assets with users via email. - -### 2. Create/Update Asset Permissions ✅ - -Implement functionality to create or update permissions for a user on an asset, using email address as the identifier. - -### 3. Remove Asset Permissions ✅ - -Implement functionality to remove a user's permissions for an asset, using email address as the identifier. - -### 4. List Asset Permissions ✅ - -Implement functionality to list all permissions for a given asset. - -### 5. Check Asset Permissions ✅ - -Implement functionality to check if a user has the required permission level for an asset. - -## Detailed Requirements - -### 1. User Lookup by Email ✅ - -**Ticket: Implement User Lookup by Email** - -- ✅ Create a `user_lookup.rs` module in the sharing library -- ✅ Implement a function to find a user by email address -- ✅ Return the user ID and other relevant information -- ✅ Handle cases where the user doesn't exist -- ✅ Ensure proper error handling - -```rust -// Function signature -pub async fn find_user_by_email(email: &str) -> Result>; -``` - -### 2. Create/Update Asset Permissions ✅ - -**Ticket: Implement Create/Update Asset Permissions by Email** - -- ✅ Enhance the existing `create_asset_permission.rs` module -- ✅ Implement a function to create or update permissions using email -- ✅ Support Owner and FullAccess permission levels -- ✅ Validate inputs and handle errors appropriately -- ✅ Update the existing record if permission already exists - -```rust -// Function signature -pub async fn create_share_by_email( - email: &str, - asset_id: Uuid, - asset_type: AssetType, - role: AssetPermissionRole, - created_by: Uuid, -) -> Result; -``` - -### 3. Remove Asset Permissions ✅ - -**Ticket: Implement Remove Asset Permissions by Email** - -- ✅ Enhance the existing `remove_asset_permissions.rs` module -- ✅ Implement a function to remove permissions using email -- ✅ Soft delete the permission record (set deleted_at to current time) -- ✅ Validate inputs and handle errors appropriately - -```rust -// Function signature -pub async fn remove_share_by_email( - email: &str, - asset_id: Uuid, - asset_type: AssetType, - updated_by: Uuid, -) -> Result<()>; -``` - -### 4. List Asset Permissions ✅ - -**Ticket: Implement List Asset Permissions** - -- ✅ Enhance the existing `list_asset_permissions.rs` module -- ✅ 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 - -```rust -// Function signature -pub async fn list_shares( - asset_id: Uuid, - asset_type: AssetType, -) -> Result>; -``` - -### 5. Check Asset Permissions ✅ - -**Ticket: Implement Check Asset Permissions** - -- ✅ Uncomment and enhance the existing `check_asset_permission.rs` module -- ✅ Ensure the function checks if a user has the required permission level -- ✅ Support checking against specific permission levels -- ✅ Optimize for performance with caching if necessary - -```rust -// Function signature already exists in the codebase -pub async fn check_access( - asset_id: Uuid, - asset_type: AssetType, - identity_id: Uuid, - identity_type: IdentityType, -) -> Result>; -``` - -## Permission Requirements - -Each function has specific permission requirements: - -1. **Create/Update Permissions**: Requires Owner or FullAccess permission -2. **Remove Permissions**: Requires Owner or FullAccess permission -3. **List Permissions**: Available to all permission levels -4. **Check Permissions**: Internal function, no permission requirements - -## Testing Strategy - -Each component should have: - -1. **Unit Tests**: Test individual functions with mocked dependencies ✅ -2. **Integration Tests**: Test the interaction between components ✅ -3. **Permission Tests**: Verify permission checks are enforced correctly ✅ -4. **Error Handling Tests**: Verify proper error handling for edge cases ✅ - -## Implementation Plan - -The implementation will be divided into the following tickets: - -1. **Ticket 1**: Implement User Lookup by Email ✅ -2. **Ticket 2**: Implement Create/Update Asset Permissions by Email ✅ -3. **Ticket 3**: Implement Remove Asset Permissions by Email ✅ -4. **Ticket 4**: Implement List Asset Permissions ✅ -5. **Ticket 5**: Implement Check Asset Permissions ✅ - -Each ticket should be implemented and tested independently, allowing for parallel development and incremental deployment. - -## Success Metrics - -- ✅ All functions pass unit and integration tests -- ✅ Performance meets requirements (response times under 100ms) -- ✅ Error handling is robust and user-friendly -- ✅ Code is modular and maintainable - -## Future Considerations - -- Adding support for bulk operations -- Implementing more granular permission levels -- Adding support for time-limited permissions -- Integrating with notification systems for permission changes \ No newline at end of file diff --git a/api/prds/active/sharing_access_controls_summary.md b/api/prds/active/sharing_access_controls_summary.md deleted file mode 100644 index 4424f844c..000000000 --- a/api/prds/active/sharing_access_controls_summary.md +++ /dev/null @@ -1,119 +0,0 @@ -# Sharing Access Controls Summary PRD - -## Overview -This document provides a high-level summary of the sharing access controls implementation, divided into modular, testable tickets. Each component has its own detailed PRD for implementation. - -## Components - -### 1. User Lookup by Email -**PRD**: [sharing_user_lookup.md](/prds/active/sharing_user_lookup.md) - -Implements functionality to look up users by their email addresses, which is essential for email-based sharing features. - -**Key Function**: -```rust -pub async fn find_user_by_email(email: &str) -> Result>; -``` - -### 2. Create/Update Asset Permissions -**PRD**: [sharing_create_permissions.md](/prds/active/sharing_create_permissions.md) - -Implements functionality to create or update permissions for a user on an asset, using email address as the identifier. - -**Key Function**: -```rust -pub async fn create_share_by_email( - email: &str, - asset_id: Uuid, - asset_type: AssetType, - role: AssetPermissionRole, - created_by: Uuid, -) -> Result; -``` - -**Permission Required**: Owner or FullAccess - -### 3. Remove Asset Permissions -**PRD**: [sharing_remove_permissions.md](/prds/active/sharing_remove_permissions.md) - -Implements functionality to remove a user's permissions for an asset, using email address as the identifier. - -**Key Function**: -```rust -pub async fn remove_share_by_email( - email: &str, - asset_id: Uuid, - asset_type: AssetType, - updated_by: Uuid, -) -> Result<()>; -``` - -**Permission Required**: Owner or FullAccess - -### 4. List Asset Permissions -**PRD**: [sharing_list_permissions.md](/prds/active/sharing_list_permissions.md) - -Implements functionality to list all permissions for a given asset. - -**Key Function**: -```rust -pub async fn list_shares( - asset_id: Uuid, - asset_type: AssetType, -) -> Result>; -``` - -**Permission Required**: All permission levels - -### 5. Check Asset Permissions -**PRD**: [sharing_check_permissions.md](/prds/active/sharing_check_permissions.md) - -Implements functionality to check if a user has the required permission level for an asset. - -**Key Functions**: -```rust -pub async fn check_access( - asset_id: Uuid, - asset_type: AssetType, - identity_id: Uuid, - identity_type: IdentityType, -) -> Result>; - -pub async fn has_permission( - asset_id: Uuid, - asset_type: AssetType, - identity_id: Uuid, - identity_type: IdentityType, - required_role: AssetPermissionRole, -) -> Result; -``` - -**Permission Required**: Internal function, no permission requirements - -## Implementation Strategy - -These components should be implemented in the following order: - -1. User Lookup by Email -2. Check Asset Permissions -3. Create/Update Asset Permissions -4. Remove Asset Permissions -5. List Asset Permissions - -This order ensures that dependencies are satisfied before they are needed by other components. - -## Testing Strategy - -Each component should have: - -1. **Unit Tests**: Test individual functions with mocked dependencies -2. **Integration Tests**: Test the interaction between components -3. **Permission Tests**: Verify permission checks are enforced correctly -4. **Error Handling Tests**: Verify proper error handling for edge cases - -## Success Metrics - -- All functions pass unit and integration tests -- Performance meets requirements (response times under 100ms) -- Error handling is robust and user-friendly -- Code is modular and maintainable diff --git a/api/prds/active/sharing_check_permissions.md b/api/prds/active/sharing_check_permissions.md deleted file mode 100644 index 66f3a0eec..000000000 --- a/api/prds/active/sharing_check_permissions.md +++ /dev/null @@ -1,136 +0,0 @@ -# Check Asset Permissions PRD ✅ - -## Overview -This PRD outlines the implementation of functionality to check if a user has the required permission level for an asset within the sharing access controls system. - -## Background -The system needs to verify that users have appropriate permissions before allowing them to perform actions on assets. This requires enhancing and enabling the existing permission checking functionality. - -## Goals -- ✅ Enable and enhance the existing `check_asset_permission.rs` module -- ✅ Ensure the function checks if a user has the required permission level -- ✅ Support checking against specific permission levels -- ✅ Optimize for performance with caching if necessary - -## Non-Goals -- Implementing UI components for permission checking -- Complex permission hierarchies beyond what's already defined -- Implementing new permission types - -## Technical Design - -### Component: Enhanced Check Asset Permission Module - -Uncomment and enhance the existing `check_asset_permission.rs` module in the library: - -```rust -// Already exists in the codebase -pub async fn check_access( - asset_id: Uuid, - asset_type: AssetType, - identity_id: Uuid, - identity_type: IdentityType, -) -> Result> { - // Implementation details -} - -// Add a new helper function -pub async fn has_permission( - asset_id: Uuid, - asset_type: AssetType, - identity_id: Uuid, - identity_type: IdentityType, - required_role: AssetPermissionRole, -) -> Result { - // Implementation details -} -``` - -### Implementation Details - -1. Uncomment the existing `check_asset_permission` module in `lib.rs` -2. Implement the new `has_permission` function that uses `check_access` -3. The function will check if the user's permission level is sufficient for the required role -4. It will use the role hierarchy defined in the `AssetPermissionRole` enum - -### Permission Hierarchy - -The function will use the existing `max` function in `AssetPermissionRole` to compare permission levels: - -```rust -// Example implementation -pub async fn has_permission( - asset_id: Uuid, - asset_type: AssetType, - identity_id: Uuid, - identity_type: IdentityType, - required_role: AssetPermissionRole, -) -> Result { - let user_role = check_access(asset_id, asset_type, identity_id, identity_type).await?; - - match user_role { - Some(role) => { - // Check if user's role is sufficient for the required role - // This logic depends on how roles are compared in your system - Ok(matches!( - (role, required_role), - (AssetPermissionRole::Owner, _) | - (AssetPermissionRole::FullAccess, _) | - (AssetPermissionRole::CanEdit, AssetPermissionRole::CanEdit | AssetPermissionRole::CanView) | - (AssetPermissionRole::CanView, AssetPermissionRole::CanView) - )) - } - None => Ok(false), - } -} -``` - -### Error Handling - -The function should handle the following error cases: -- Database connection errors -- Query execution errors -- Invalid asset ID or type - -## Testing Strategy - -### Unit Tests -- Test checking permissions for users with different permission levels -- Test checking against different required permission levels -- Test handling users without permissions -- Test error handling for database issues - -### Integration Tests -- Test the function in combination with permission creation and removal - -## Dependencies -- Database models and schema -- Diesel ORM -- Error handling utilities - -## Implementation Plan -1. ✅ Uncomment the `check_asset_permission` module in `lib.rs` -2. ✅ Implement the `has_permission` function -3. ✅ Add error handling -4. ✅ Write tests -5. ✅ Update the library exports in `lib.rs` - -## Success Criteria -- ✅ Function correctly checks if a user has the required permission level -- ✅ Appropriate error handling is implemented -- ✅ Tests pass successfully -- ✅ Code is well-documented - -## Permission Requirements -- Internal function, no permission requirements - -## Implementation Summary -- ✅ Uncommented the `check_asset_permission` module in `lib.rs` -- ✅ Implemented the `has_permission` function with a robust permission hierarchy -- ✅ Fixed issues with the bulk permission checking approach to be more memory efficient -- ✅ Added comprehensive role checks for all permission levels -- ✅ Created test cases to verify functionality -- ✅ Ensured all functions are properly exported -- ✅ Completed all required implementation tasks - -The implementation is now ready for review and integration with the rest of the access controls system. diff --git a/api/prds/active/sharing_create_permissions.md b/api/prds/active/sharing_create_permissions.md deleted file mode 100644 index df2161f1c..000000000 --- a/api/prds/active/sharing_create_permissions.md +++ /dev/null @@ -1,93 +0,0 @@ -# Create/Update Asset Permissions by Email PRD - -## Overview -This PRD outlines the implementation of functionality to create or update asset permissions using email addresses as user identifiers within the sharing access controls system. - -## Background -Users need to be able to share assets with other users by specifying their email addresses and the desired permission level. This requires enhancing the existing permission creation functionality to work with email addresses. - -## Goals -- Implement a function to create or update permissions using email addresses -- Support Owner and FullAccess permission levels -- Validate inputs and handle errors appropriately -- Update existing records if permissions already exist - -## Non-Goals -- Implementing UI components for sharing -- Handling organization-wide permission policies -- Supporting permission levels beyond Owner and FullAccess - -## Technical Design - -### Component: Enhanced Create Asset Permission Module - -Enhance the existing `create_asset_permission.rs` module with a new function: - -```rust -pub async fn create_share_by_email( - email: &str, - asset_id: Uuid, - asset_type: AssetType, - role: AssetPermissionRole, - created_by: Uuid, -) -> Result { - // Implementation details -} -``` - -### Implementation Details - -1. The function will first look up the user by email using the `find_user_by_email` function -2. If the user is found, it will create or update the permission record -3. If the user is not found, it will return an appropriate error -4. The function will validate that the role is either Owner or FullAccess -5. The function will use the existing `create_share` function to create or update the permission - -### Permission Validation - -The function should validate that: -- The role is either Owner or FullAccess -- The asset type is valid (not deprecated) -- The user has permission to share the asset (requires separate permission check) - -### Error Handling - -The function should handle the following error cases: -- User not found -- Invalid permission level -- Database errors -- Permission validation errors - -## Testing Strategy - -### Unit Tests -- Test creating a new permission -- Test updating an existing permission -- Test handling a non-existent user -- Test permission validation - -### Integration Tests -- Test the function in combination with permission checking - -## Dependencies -- User lookup module -- Existing create_share function -- Database models and schema -- Diesel ORM -- Error handling utilities - -## Implementation Plan -1. ✅ Enhance the `create_asset_permission.rs` file -2. ✅ Implement the `create_share_by_email` function -3. ✅ Add validation and error handling -4. ✅ Write tests -5. ✅ Update the library exports in `lib.rs` - -## Success Criteria -- ✅ Function correctly creates or updates permissions using email addresses -- ✅ Appropriate validation and error handling is implemented -- ✅ Tests pass successfully -- ✅ Code is well-documented - -## Permission Requirements -- Requires Owner or FullAccess permission to execute diff --git a/api/prds/active/sharing_list_permissions.md b/api/prds/active/sharing_list_permissions.md deleted file mode 100644 index c406d96de..000000000 --- a/api/prds/active/sharing_list_permissions.md +++ /dev/null @@ -1,119 +0,0 @@ -# List Asset Permissions PRD - -## Overview -This PRD outlines the implementation of functionality to list all permissions for a given asset within the sharing access controls system. - -## Background -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 - -## Non-Goals -- Implementing UI components for displaying permissions -- Listing permissions across multiple assets -- Complex search or filtering beyond basic permission types - -## Technical Design - -### Component: Enhanced List Asset Permissions Module - -Enhance the existing `list_asset_permissions.rs` module with a new function: - -```rust -pub async fn list_shares( - asset_id: Uuid, - asset_type: AssetType, -) -> Result> { - // Implementation details -} -``` - -### Data Structure - -Create a new struct to represent a permission with user information: - -```rust -pub struct AssetPermissionWithUser { - pub permission: AssetPermission, - pub user: Option, -} - -pub struct UserInfo { - pub id: Uuid, - pub email: String, - pub name: Option, - pub avatar_url: Option, -} -``` - -### 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 - -### Database Query - -The function will use the following query pattern: - -```rust -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::deleted_at.is_null()) - .select(( - asset_permissions::all_columns, - users::id, - users::email, - users::name, - users::avatar_url, - )) - .load::<(AssetPermission, Uuid, String, Option, Option)>(&mut conn) - .await -``` - -### Error Handling - -The function should handle the following error cases: -- ✅ Database connection errors -- ✅ Query execution errors -- ✅ Invalid asset ID or type - -## Testing Strategy - -### Unit Tests -- ✅ 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 design for the function in combination with permission creation and removal - -## Dependencies -- ✅ 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. ✅ 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 -- ✅ Test design complete -- ✅ Code is well-documented - -## Permission Requirements -- ✅ Available to all permission levels diff --git a/api/prds/active/sharing_remove_permissions.md b/api/prds/active/sharing_remove_permissions.md deleted file mode 100644 index ed43fb016..000000000 --- a/api/prds/active/sharing_remove_permissions.md +++ /dev/null @@ -1,104 +0,0 @@ -# Remove Asset Permissions by Email PRD - -## Overview -This PRD outlines the implementation of functionality to remove asset permissions using email addresses as user identifiers within the sharing access controls system. - -## Background -Users need to be able to revoke access to assets from other users by specifying their email addresses. This requires enhancing the existing permission removal functionality to work with email addresses. - -## Goals -- Implement a function to remove permissions using email addresses -- Soft delete the permission record (set deleted_at to current time) -- Validate inputs and handle errors appropriately -- Ensure proper permission checks - -## Non-Goals -- Implementing UI components for permission removal -- Hard deleting permission records -- Batch removal operations - -## Technical Design - -### Component: Enhanced Remove Asset Permissions Module - -Enhance the existing `remove_asset_permissions.rs` module with a new function: - -```rust -pub async fn remove_share_by_email( - email: &str, - asset_id: Uuid, - asset_type: AssetType, - updated_by: Uuid, -) -> Result<()> { - // Implementation details -} -``` - -### Implementation Details - -1. The function will first look up the user by email using the `find_user_by_email` function -2. If the user is found, it will soft delete the permission record -3. If the user is not found, it will return an appropriate error -4. The function will validate that the caller has permission to remove shares (Owner or FullAccess) -5. The function will use a database update to set the deleted_at field to the current time - -### Database Update - -The function will use the following update pattern: - -```rust -diesel::update(asset_permissions::table) - .filter(asset_permissions::identity_id.eq(user_id)) - .filter(asset_permissions::identity_type.eq(IdentityType::User)) - .filter(asset_permissions::asset_id.eq(asset_id)) - .filter(asset_permissions::asset_type.eq(asset_type)) - .filter(asset_permissions::deleted_at.is_null()) - .set(( - asset_permissions::deleted_at.eq(Utc::now()), - asset_permissions::updated_at.eq(Utc::now()), - asset_permissions::updated_by.eq(updated_by), - )) - .execute(&mut conn) - .await -``` - -### Error Handling - -The function should handle the following error cases: -- User not found -- Permission record not found -- Database errors -- Permission validation errors - -## Testing Strategy - -### Unit Tests -- Test removing an existing permission -- Test handling a non-existent user -- Test handling a non-existent permission -- Test permission validation - -### Integration Tests -- Test the function in combination with permission creation and checking - -## Dependencies -- User lookup module -- Database models and schema -- Diesel ORM -- Error handling utilities - -## Implementation Plan -1. ✅ Enhance the `remove_asset_permissions.rs` file -2. ✅ Implement the `remove_share_by_email` function -3. ✅ Add validation and error handling -4. ✅ Write tests -5. ✅ Update the library exports in `lib.rs` - -## Success Criteria -- ✅ Function correctly removes permissions using email addresses -- ✅ Appropriate validation and error handling is implemented -- ✅ Tests pass successfully -- ✅ Code is well-documented - -## Permission Requirements -- Requires Owner or FullAccess permission to execute diff --git a/api/prds/active/sharing_user_lookup.md b/api/prds/active/sharing_user_lookup.md deleted file mode 100644 index 59b251bcf..000000000 --- a/api/prds/active/sharing_user_lookup.md +++ /dev/null @@ -1,85 +0,0 @@ -# User Lookup by Email PRD - -## Overview -This PRD outlines the implementation of a user lookup functionality by email address within the sharing access controls system. This component is essential for enabling email-based sharing features. - -## Background -When sharing assets with users, it's more user-friendly to use email addresses rather than user IDs. This requires a reliable way to look up users by their email addresses. - -## Goals -- Implement a function to find users by email address -- Return appropriate user information needed for sharing -- Handle cases where users don't exist -- Ensure proper error handling - -## Non-Goals -- Creating or modifying user records -- Implementing complex user search functionality -- Handling authentication or authorization - -## Technical Design - -### Component: User Lookup Module - -Create a new module `user_lookup.rs` in the sharing library with the following functionality: - -```rust -pub async fn find_user_by_email(email: &str) -> Result> { - // Implementation details -} -``` - -### Implementation Details - -1. The function will query the database to find a user with the given email address -2. If a user is found, return the user object -3. If no user is found, return None -4. Handle any database errors appropriately - -### Database Query - -The function will use the following query pattern: - -```rust -users::table - .filter(users::email.eq(email)) - .filter(users::deleted_at.is_null()) - .first::(&mut conn) - .optional() - .await -``` - -### Error Handling - -The function should handle the following error cases: -- Database connection errors -- Query execution errors -- Invalid email format - -## Testing Strategy - -### Unit Tests -- Test finding an existing user -- Test handling a non-existent user -- Test error handling for database issues - -### Integration Tests -- Test the function in combination with permission creation - -## Dependencies -- Database models and schema -- Diesel ORM -- Error handling utilities - -## Implementation Plan -1. ✅ Create the `user_lookup.rs` file -2. ✅ Implement the `find_user_by_email` function -3. ✅ Add error handling -4. ✅ Write tests -5. ✅ Update the library exports in `lib.rs` - -## Success Criteria -- ✅ Function correctly finds users by email -- ✅ Appropriate error handling is implemented -- ✅ Tests pass successfully -- ✅ Code is well-documented