removed the prds for sharing

This commit is contained in:
dal 2025-03-19 16:19:44 -06:00
parent 6358771ec6
commit c98060e59c
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
22 changed files with 0 additions and 4247 deletions

View File

@ -1,242 +0,0 @@
# API Chats Sharing - Create Endpoint PRD
## Problem Statement
Users need the ability to share chats with other users via a REST API endpoint.
## Status
✅ Completed
## 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
// Array of recipients to share with
pub type ShareRequest = Vec<ShareRecipient>;
#[derive(Debug, Deserialize)]
pub struct ShareRecipient {
pub email: String,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<Vec<ShareRecipient>>,
) -> Result<ApiResponse<String>, (StatusCode, String)> {
tracing::info!("Processing POST request for chat sharing with ID: {}, user_id: {}", id, user.id);
// Convert request to a list of (email, role) pairs
let emails_and_roles: Vec<(String, AssetPermissionRole)> = request
.into_iter()
.map(|recipient| (recipient.email, recipient.role))
.collect();
match create_chat_sharing_handler(&id, &user.id, emails_and_roles).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions created successfully".to_string())),
Err(e) => {
tracing::error!("Error creating sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Chat not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.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_and_roles: Vec<(String, 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, role) in emails_and_roles {
match create_share_by_email(
&email,
*chat_id,
AssetType::Chat,
role,
*user_id,
).await {
Ok(_) => {
tracing::info!("Created sharing permission for email: {} on chat: {} with role: {:?}", email, chat_id, role);
},
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<bool>
```
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 sharing permissions 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<Option<User>>
```
This function looks up a user by email address and returns the user object if found.
### 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
- 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
- The role must be a valid AssetPermissionRole (ReadOnly, ReadWrite, FullAccess)
### 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 Cases
1. Should create sharing permissions for valid emails with specified 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 handle gracefully when trying to share with a user that already has access
### 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
- Validate email addresses to prevent injection attacks
- Implement rate limiting to prevent abuse
- Consider implementing notifications for users who receive new sharing permissions
### Monitoring
- Log all requests with appropriate context
- Track performance metrics for the endpoint
- Monitor error rates and types
- Track sharing creation operations by user for audit purposes

View File

@ -1,208 +0,0 @@
# 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
// Array of emails to remove sharing permissions for
pub type DeleteShareRequest = Vec<String>;
```
### 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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(emails): Json<Vec<String>>,
) -> Result<ApiResponse<String>, (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, emails).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions removed successfully".to_string())),
Err(e) => {
tracing::error!("Error removing sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Chat not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.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<String>,
) -> 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 delete 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<bool>
```
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 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 specific email on a specific asset.
### 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 delete 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 deletions
#### 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 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 implementing notifications for users whose permissions have been removed
### 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

View File

@ -1,174 +0,0 @@
# 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<SharingPermission>,
}
#[derive(Debug, Serialize)]
pub struct SharingPermission {
pub user_id: Uuid,
pub email: String,
pub name: Option<String>,
pub avatar_url: Option<String>,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
) -> Result<ApiResponse<SharingResponse>, (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<Vec<AssetPermissionWithUser>> {
// 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<Option<AssetPermissionRole>>
```
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<Vec<AssetPermissionWithUser>>
```
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<UserInfo>,
}
```
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

View File

@ -1,104 +0,0 @@
# 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/api_chats_sharing_delete/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

View File

@ -1,233 +0,0 @@
# 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
// Array of recipients to share with
pub type ShareRequest = Vec<ShareRecipient>;
#[derive(Debug, Deserialize)]
pub struct ShareRecipient {
pub email: String,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<Vec<ShareRecipient>>,
) -> Result<ApiResponse<String>, (StatusCode, String)> {
tracing::info!("Processing PUT request for chat sharing with ID: {}, user_id: {}", id, user.id);
// Convert request to a list of (email, role) pairs
let emails_and_roles: Vec<(String, AssetPermissionRole)> = request
.into_iter()
.map(|recipient| (recipient.email, recipient.role))
.collect();
match update_chat_sharing_handler(&id, &user.id, emails_and_roles).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions updated successfully".to_string())),
Err(e) => {
tracing::error!("Error updating sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Chat not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.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_and_roles: Vec<(String, 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, role) in emails_and_roles {
match create_share_by_email(
&email,
*chat_id,
AssetType::Chat,
role,
*user_id,
).await {
Ok(_) => {
tracing::info!("Updated sharing permission for email: {} on chat: {} with role: {:?}", email, chat_id, role);
},
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<bool>
```
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. The update endpoint is intended to modify existing permissions, while the create endpoint is for establishing new permissions.
2. The update endpoint uses a PUT HTTP method, following REST conventions for updates.
3. The update endpoint should be used when changing the role of an existing permission.
### 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
- 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
- The role must be a valid AssetPermissionRole (ReadOnly, ReadWrite, FullAccess)
### 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 Cases
1. Should update sharing permissions for valid emails with specified roles
1. Should return 403 when user doesn't have Owner or FullAccess permission
1. Should return 404 when chat doesn't exist
1. Should return 400 when email is invalid
1. Should handle gracefully when trying to update sharing for a user that doesn't have access
### 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
- Implement rate limiting to prevent abuse
- Consider implementing notifications for users whose permissions have changed
### 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

View File

@ -1,214 +0,0 @@
# 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
// Array of share recipients with email and role
pub type ShareRecipient = struct {
pub email: String,
pub role: AssetPermissionRole,
};
pub type CreateSharingRequest = Vec<ShareRecipient>;
```
### 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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<Vec<ShareRecipient>>,
) -> Result<ApiResponse<String>, (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).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions created successfully".to_string())),
Err(e) => {
tracing::error!("Error creating sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Collection not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.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,
request: Vec<ShareRecipient>,
) -> 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 recipient and create sharing permissions
let emails_and_roles: Vec<(String, AssetPermissionRole)> = request
.into_iter()
.map(|recipient| (recipient.email, recipient.role))
.collect();
for (email, role) in emails_and_roles {
// 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<bool>
```
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<AssetPermission>
```
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<Option<User>>
```
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

View File

@ -1,222 +0,0 @@
# 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
// Simple array of email strings
pub type DeleteSharingRequest = Vec<String>;
```
### 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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<Vec<String>>,
) -> Result<ApiResponse<String>, (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).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions deleted successfully".to_string())),
Err(e) => {
tracing::error!("Error deleting sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Collection not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.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_collection_sharing_handler(
collection_id: &Uuid,
user_id: &Uuid,
emails: Vec<String>,
) -> 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 delete 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 delete sharing for this collection"));
}
// 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,
*collection_id,
AssetType::Collection,
*user_id,
).await {
Ok(_) => {
tracing::info!("Deleted sharing permission for email: {} on collection: {}", email, collection_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<bool>
```
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,
user_id: Uuid,
) -> 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

View File

@ -1,176 +0,0 @@
# 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.
✅ Implemented
## 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<SharingPermission>,
}
#[derive(Debug, Serialize)]
pub struct SharingPermission {
pub user_id: Uuid,
pub email: String,
pub name: Option<String>,
pub avatar_url: Option<String>,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
) -> Result<ApiResponse<SharingResponse>, (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<Vec<AssetPermissionWithUser>> {
// 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<Option<AssetPermissionRole>>
```
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<Vec<AssetPermissionWithUser>>
```
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<UserInfo>,
}
```
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

View File

@ -1,104 +0,0 @@
# 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

View File

@ -1,213 +0,0 @@
# 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
// Array of share recipients with email and role
pub type ShareRecipient = struct {
pub email: String,
pub role: AssetPermissionRole,
};
pub type UpdateSharingRequest = Vec<ShareRecipient>;
```
### 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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<Vec<ShareRecipient>>,
) -> Result<ApiResponse<String>, (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).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions updated successfully".to_string())),
Err(e) => {
tracing::error!("Error updating sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Collection not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.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,
request: Vec<ShareRecipient>,
) -> 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 recipient and update sharing permissions
let emails_and_roles: Vec<(String, AssetPermissionRole)> = request
.into_iter()
.map(|recipient| (recipient.email, recipient.role))
.collect();
for (email, role) in emails_and_roles {
// 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<bool>
```
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<AssetPermission>
```
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

View File

@ -1,339 +0,0 @@
# API Collections Update - PUT Endpoint PRD
## Problem Statement ✅
Users need the ability to update sharing permissions for collections through a REST API endpoint. Currently, while the ability to create, list, and delete sharing permissions is implemented, the specific endpoint for updating existing permissions (PUT) is missing. This forces users to rely on complex combinations of create and delete operations to accomplish what should be a simple update operation.
Key issues:
- No dedicated endpoint for updating permissions
- Inconsistent UX between collections and other asset types
- Unnecessary complexity for simple permission updates
### Current Limitations
- Users must delete and recreate permissions to modify them
- Increased API calls for simple permission modifications
- Risk of temporary permission loss during delete/create operations
### Impact
- User Impact: More complex workflows for users managing collections
- System Impact: Higher number of database operations for permission changes
- Business Impact: Inconsistent experience across asset types, reducing platform cohesion
## Requirements
### Functional Requirements ✅
#### Core Functionality
- Implement PUT /collections/:id/sharing endpoint
- Details: Add the ability to update sharing permissions for a collection using a PUT request
- Acceptance Criteria: Successfully update existing permissions with new role values
- Dependencies: Existing sharing library components
#### User Interface
- Update endpoint must follow established patterns
- Details: Must match the behavior and structure of other sharing update endpoints
- Acceptance Criteria: Response structure matches other sharing update endpoints
- Dependencies: None
#### Data Management
- Update permissions in the asset_permissions table
- Details: Must use the sharing library's create_share_by_email function for consistent upsert behavior
- Acceptance Criteria: Permission roles are updated in the database
- Dependencies: Sharing library's create_share_by_email function
### Non-Functional Requirements ✅
- Performance Requirements
- The update operation should complete within 200ms for a typical request
- Security Requirements
- Only users with Owner or FullAccess permissions can update sharing permissions
- Proper validation of email addresses and roles
- Scalability Requirements
- Support batch updates for multiple users in a single request
## Technical Design ✅
### System Architecture
The update sharing endpoint follows the established architecture pattern for REST endpoints:
```mermaid
graph TD
A[Client] -->|PUT request| B[REST Endpoint]
B -->|Call handler| C[Business Logic Handler]
C -->|Check permissions| D[Sharing Library]
C -->|Update permissions| E[Database]
D -->|Permission check| E
E -->|Results| C
C -->|Response| B
B -->|JSON response| A
```
### Core Components ✅
#### Component 1: REST Endpoint Handler
```rust
// update_sharing.rs
pub async fn update_collection_sharing_rest_handler(
Extension(user): Extension<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<Vec<ShareRecipient>>,
) -> Result<ApiResponse<String>, (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).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions updated successfully".to_string())),
Err(e) => {
tracing::error!("Error updating sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Collection not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.contains("Invalid email") {
return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e)));
}
Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to update sharing permissions: {}", e)))
}
}
}
```
#### Component 2: Business Logic Handler
```rust
// update_sharing_handler.rs
pub async fn update_collection_sharing_handler(
collection_id: &Uuid,
user_id: &Uuid,
request: Vec<ShareRecipient>,
) -> 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 recipient and update sharing permissions
let emails_and_roles: Vec<(String, AssetPermissionRole)> = request
.into_iter()
.map(|recipient| (recipient.email, recipient.role))
.collect();
for (email, role) in emails_and_roles {
// 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(())
}
```
### API Changes
```rust
// The request type is reused from create_sharing
pub type ShareRecipient = struct {
pub email: String,
pub role: AssetPermissionRole,
};
pub type UpdateSharingRequest = Vec<ShareRecipient>;
// The response is a simple success message
```
### File Changes
#### New Files
- `src/routes/rest/routes/collections/sharing/update_sharing.rs`
- Purpose: REST handler for updating sharing permissions
- Key components: update_collection_sharing_rest_handler function
- Dependencies: handlers, axum, middleware
- `libs/handlers/src/collections/sharing/update_sharing_handler.rs`
- Purpose: Business logic for updating sharing permissions
- Key components: update_collection_sharing_handler function
- Dependencies: sharing library, database
#### Modified Files
- `src/routes/rest/routes/collections/sharing/mod.rs`
- Changes: Add the PUT route for /collections/:id/sharing
- Impact: Makes the update endpoint available
- Dependencies: None
- `libs/handlers/src/collections/sharing/mod.rs`
- Changes: Export the update_collection_sharing_handler function
- Impact: Makes the handler available to route handlers
- Dependencies: None
## Implementation Plan
### Phase 1: Development ✅
1. Create handler implementation
- [x] Implement update_collection_sharing_handler.rs
- [x] Export handler in mod.rs
- [x] Add type definitions if needed
2. Create REST endpoint
- [x] Implement update_sharing.rs
- [x] Update router in mod.rs to include PUT route
- [x] Add validation and error handling
3. Manual testing
- [x] Test with valid inputs
- [x] Test with invalid inputs
- [x] Test with unauthorized users
### Phase 2: Testing and Documentation ⏳
1. Unit tests
- [x] Write unit tests for handler
- [x] Test error handling
- [x] Test permission checking
2. Integration tests
- [x] Write integration tests for endpoint
- [x] Test with various scenarios
- [x] Verify database state after updates
3. Documentation
- [x] Update API documentation
- [x] Document type definitions
- [x] Add examples
## Testing Strategy ✅
### Unit Tests
Testing the update_collection_sharing_handler function:
```rust
#[cfg(test)]
mod tests {
use super::*;
use sharing::types::{AssetType, IdentityType, AssetPermissionRole};
use tokio;
use mock::db::{MockDb, MockDbPool};
use uuid::Uuid;
#[tokio::test]
async fn test_update_collection_sharing_handler_success() {
// Setup mock DB
let mut mock_db = MockDb::new();
mock_db.expect_transaction().return_once(|f| f());
// Test with valid inputs
let collection_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let request = vec![
ShareRecipient {
email: "test@example.com".to_string(),
role: AssetPermissionRole::ReadOnly,
},
];
let result = update_collection_sharing_handler(&collection_id, &user_id, request).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_update_collection_sharing_handler_not_found() {
// Test with non-existent collection
let collection_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let request = vec![];
let result = update_collection_sharing_handler(&collection_id, &user_id, request).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[tokio::test]
async fn test_update_collection_sharing_handler_no_permission() {
// Test with user who doesn't have permission
let collection_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let request = vec![];
let result = update_collection_sharing_handler(&collection_id, &user_id, request).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("permission"));
}
}
```
### Integration Tests
#### Scenario 1: Update sharing permissions for a collection
- Setup: Create a collection and initial sharing permissions
- Steps:
1. Authenticate as owner
2. Send PUT request to /collections/:id/sharing with new role values
3. Verify response is successful
4. Check database for updated roles
- Expected Results: Sharing permissions are updated in the database
- Validation Criteria: Response code 200, updated roles in database match request
#### Scenario 2: Attempt update without permission
- Setup: Create a collection and share with a test user as ReadOnly
- Steps:
1. Authenticate as the test user (who only has ReadOnly access)
2. Send PUT request to /collections/:id/sharing
3. Verify response is 403 Forbidden
- Expected Results: Request is rejected with appropriate error
- Validation Criteria: Response code 403, no changes in database
### Security Considerations
- Security Requirement 1: Permission Validation
- Description: Only users with Owner or FullAccess permissions can update sharing
- Implementation: Check permission using has_permission function
- Validation: Test with users having different permission levels
- Security Requirement 2: Input Validation
- Description: Validate email addresses and roles
- Implementation: Leverage validation in create_share_by_email function
- Validation: Test with invalid emails and roles
### Performance Considerations
- Performance Requirement 1: Efficient Database Operations
- Description: Minimize database operations
- Implementation: Use batch processing for multiple recipients
- Validation: Performance testing with varying numbers of recipients
### References
- [API Collections Sharing Summary](/Users/dallin/api_collections_update/api/prds/active/api_collections_sharing_summary.md)
- [API Collections Sharing List](/Users/dallin/api_collections_update/api/prds/active/api_collections_sharing_list.md)
- [API Collections Sharing Create](/Users/dallin/api_collections_update/api/prds/active/api_collections_sharing_create.md)
- [API Collections Sharing Delete](/Users/dallin/api_collections_update/api/prds/active/api_collections_sharing_delete.md)

View File

@ -1,212 +0,0 @@
# 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 ShareRecipient {
pub email: String,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<Vec<ShareRecipient>>,
) -> Result<ApiResponse<String>, (StatusCode, String)> {
tracing::info!("Processing POST request for dashboard sharing with ID: {}, user_id: {}", id, user.id);
let emails_and_roles: Vec<(String, AssetPermissionRole)> = request
.into_iter()
.map(|recipient| (recipient.email, recipient.role))
.collect();
match create_dashboard_sharing_handler(&id, &user.id, emails_and_roles).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions created successfully".to_string())),
Err(e) => {
tracing::error!("Error creating sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Dashboard not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.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_and_roles: Vec<(String, 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, role) in emails_and_roles {
// 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<bool>
```
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<AssetPermission>
```
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<Option<User>>
```
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

View File

@ -1,200 +0,0 @@
# 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
// Simple array of email strings
pub type DeleteSharingRequest = Vec<String>;
```
### 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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<Vec<String>>,
) -> Result<ApiResponse<String>, (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).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions deleted successfully".to_string())),
Err(e) => {
tracing::error!("Error deleting sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Dashboard not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.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_dashboard_sharing_handler(
dashboard_id: &Uuid,
user_id: &Uuid,
emails: Vec<String>,
) -> 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 delete 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 delete sharing for this dashboard"));
}
// 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,
*dashboard_id,
AssetType::Dashboard,
*user_id,
).await {
Ok(_) => {
tracing::info!("Deleted sharing permission for email: {} on dashboard: {}", email, dashboard_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<bool>
```
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,
user_id: Uuid,
) -> 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

View File

@ -1,180 +0,0 @@
# API Dashboards Sharing - List Endpoint PRD
## Implementation Status
✅ Implemented the REST handler for listing dashboard sharing permissions
✅ Implemented the business logic handler
✅ Added tests
✅ Connected to the existing sharing library
## 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<SharingPermission>,
}
#[derive(Debug, Serialize)]
pub struct SharingPermission {
pub user_id: Uuid,
pub email: String,
pub name: Option<String>,
pub avatar_url: Option<String>,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
) -> Result<ApiResponse<SharingResponse>, (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<Vec<AssetPermissionWithUser>> {
// 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<Option<AssetPermissionRole>>
```
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<Vec<AssetPermissionWithUser>>
```
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<UserInfo>,
}
```
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

View File

@ -1,104 +0,0 @@
# 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

View File

@ -1,211 +0,0 @@
# 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 ShareRecipient {
pub email: String,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<Vec<ShareRecipient>>,
) -> Result<ApiResponse<String>, (StatusCode, String)> {
tracing::info!("Processing PUT request for dashboard sharing with ID: {}, user_id: {}", id, user.id);
let emails_and_roles: Vec<(String, AssetPermissionRole)> = request
.into_iter()
.map(|recipient| (recipient.email, recipient.role))
.collect();
match update_dashboard_sharing_handler(&id, &user.id, emails_and_roles).await {
Ok(_) => Ok(ApiResponse::JsonData("Sharing permissions updated successfully".to_string())),
Err(e) => {
tracing::error!("Error updating sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Dashboard not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.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_and_roles: Vec<(String, 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, role) in emails_and_roles {
// 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<bool>
```
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<AssetPermission>
```
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

View File

@ -1,206 +0,0 @@
# 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<String>,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<SharingRequest>,
) -> Result<ApiResponse<String>, (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<String>,
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<bool>
```
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<AssetPermission>
```
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<Option<User>>
```
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

View File

@ -1,260 +0,0 @@
# 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<String>,
}
```
### 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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<DeleteSharingRequest>,
) -> Result<ApiResponse<String>, (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<String>,
) -> 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<bool>
```
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<Option<User>>
```
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

View File

@ -1,174 +0,0 @@
# 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<SharingPermission>,
}
#[derive(Debug, Serialize)]
pub struct SharingPermission {
pub user_id: Uuid,
pub email: String,
pub name: Option<String>,
pub avatar_url: Option<String>,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
) -> Result<ApiResponse<SharingResponse>, (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<Vec<AssetPermissionWithUser>> {
// 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<Option<AssetPermissionRole>>
```
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<Vec<AssetPermissionWithUser>>
```
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<UserInfo>,
}
```
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

View File

@ -1,106 +0,0 @@
# 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/api_metrics_sharing_update/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

View File

@ -1,229 +0,0 @@
# API Metrics Sharing - Update Endpoint PRD
✅ Implemented
## 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<String>,
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<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<SharingRequest>,
) -> Result<ApiResponse<String>, (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<String>,
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<bool>
```
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<AssetPermission>
```
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::<AssetPermission>(&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

View File

@ -1,136 +0,0 @@
# 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<SharingPermission>,
}
pub struct SharingPermission {
pub user_id: Uuid,
pub email: String,
pub name: Option<String>,
pub avatar_url: Option<String>,
pub role: AssetPermissionRole,
}
```
### Common Request Types
Create and update endpoints will use the same request structure:
```rust
pub struct SharingRequest {
pub emails: Vec<String>,
pub role: AssetPermissionRole,
}
```
Delete endpoints will use:
```rust
pub struct DeleteSharingRequest {
pub emails: Vec<String>,
}
```
### 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.