api chats sharing delete created

This commit is contained in:
dal 2025-03-19 15:19:07 -06:00
parent 31cf5beb36
commit d4ed618ffd
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
12 changed files with 456 additions and 19 deletions

View File

@ -7,6 +7,7 @@ pub mod types;
pub mod streaming_parser; pub mod streaming_parser;
pub mod context_loaders; pub mod context_loaders;
pub mod list_chats_handler; pub mod list_chats_handler;
pub mod sharing;
pub use get_chat_handler::get_chat_handler; pub use get_chat_handler::get_chat_handler;
pub use get_raw_llm_messages_handler::get_raw_llm_messages_handler; pub use get_raw_llm_messages_handler::get_raw_llm_messages_handler;
@ -14,5 +15,6 @@ pub use post_chat_handler::post_chat_handler;
pub use update_chats_handler::update_chats_handler; pub use update_chats_handler::update_chats_handler;
pub use delete_chats_handler::delete_chats_handler; pub use delete_chats_handler::delete_chats_handler;
pub use list_chats_handler::list_chats_handler; pub use list_chats_handler::list_chats_handler;
pub use sharing::delete_chat_sharing_handler;
pub use types::*; pub use types::*;
pub use streaming_parser::StreamingParser; pub use streaming_parser::StreamingParser;

View File

@ -0,0 +1,178 @@
use anyhow::{anyhow, Result};
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType},
pool::get_pg_pool,
schema::chats,
};
use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use sharing::{
check_asset_permission::has_permission,
remove_asset_permissions::remove_share_by_email,
};
use tracing::{error, info};
use uuid::Uuid;
/// Handler for deleting sharing permissions for a specific chat
pub async fn delete_chat_sharing_handler(
chat_id: &Uuid,
user_id: &Uuid,
emails: Vec<String>,
) -> Result<()> {
info!(
chat_id = %chat_id,
user_id = %user_id,
email_count = emails.len(),
"Deleting chat sharing permissions"
);
// 1. Validate the chat exists
let mut conn = get_pg_pool().get().await.map_err(|e| {
error!("Database connection error: {}", e);
anyhow!("Failed to get database connection: {}", e)
})?;
let chat_exists = chats::table
.filter(chats::id.eq(chat_id))
.filter(chats::deleted_at.is_null())
.count()
.get_result::<i64>(&mut conn)
.await
.map_err(|e| {
error!("Error checking if chat exists: {}", e);
anyhow!("Database error: {}", e)
})?;
if chat_exists == 0 {
error!(chat_id = %chat_id, "Chat not found");
return Err(anyhow!("Chat not found"));
}
// 2. Check if user has permission to delete sharing (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
.map_err(|e| {
error!(
chat_id = %chat_id,
user_id = %user_id,
"Error checking chat permissions: {}", e
);
anyhow!("Error checking chat permissions: {}", e)
})?;
if !has_permission {
error!(
chat_id = %chat_id,
user_id = %user_id,
"User does not have permission to delete sharing for this chat"
);
return Err(anyhow!("User does not have permission to delete sharing for this chat"));
}
// 3. Process each email and delete sharing permissions
for email in &emails {
// Validate email format
if !email.contains('@') {
error!(email = %email, "Invalid email format");
return Err(anyhow!("Invalid email format: {}", email));
}
match remove_share_by_email(
email,
*chat_id,
AssetType::Chat,
*user_id,
)
.await
{
Ok(_) => {
info!(
chat_id = %chat_id,
email = %email,
"Deleted sharing permission"
);
},
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") {
info!(
chat_id = %chat_id,
email = %email,
"No active permission found to delete"
);
continue;
}
error!(
chat_id = %chat_id,
email = %email,
"Failed to delete sharing: {}", e
);
return Err(anyhow!("Failed to delete sharing for email {}: {}", email, e));
}
}
}
info!(
chat_id = %chat_id,
email_count = emails.len(),
"Successfully deleted chat sharing permissions"
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use database::enums::{AssetPermissionRole, AssetType, IdentityType};
use mockall::predicate::*;
use mockall::mock;
// Mock the has_permission function
mock! {
HasPermission {}
impl HasPermission {
pub async fn has_permission(
asset_id: Uuid,
asset_type: AssetType,
identity_id: Uuid,
identity_type: IdentityType,
required_role: AssetPermissionRole,
) -> Result<bool>;
}
}
// Mock the remove_share_by_email function
mock! {
RemoveShareByEmail {}
impl RemoveShareByEmail {
pub async fn remove_share_by_email(
email: &str,
asset_id: Uuid,
asset_type: AssetType,
updated_by: Uuid,
) -> Result<()>;
}
}
// Basic placeholder test
#[tokio::test]
async fn test_delete_chat_sharing_handler_invalid_email() {
// This is a simple placeholder test - would be replaced with actual mocked tests in real implementation
// Invalid email format test doesn't require mocking database or other functions
let chat_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let emails = vec!["invalid-email".to_string()];
let result = delete_chat_sharing_handler(&chat_id, &user_id, emails).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid email format"));
}
}

View File

@ -0,0 +1,3 @@
mod delete_sharing_handler;
pub use delete_sharing_handler::delete_chat_sharing_handler;

View File

@ -2,28 +2,28 @@
## Problem Statement ## Problem Statement
Users need the ability to remove sharing permissions for chats through a REST API endpoint. Users need the ability to remove sharing permissions for chats through a REST API endpoint.
## Technical Design ## Technical Design
### Endpoint Specification ### Endpoint Specification
- **Method**: DELETE - **Method**: DELETE
- **Path**: /chats/:id/sharing - **Path**: /chats/:id/sharing
- **Description**: Removes sharing permissions for a chat - **Description**: Removes sharing permissions for a chat
- **Authentication**: Required - **Authentication**: Required
- **Authorization**: User must have Owner or FullAccess permission for the chat - **Authorization**: User must have Owner or FullAccess permission for the chat
### Request Structure ### Request Structure
```rust ```rust
// Array of emails to remove sharing permissions for // Array of emails to remove sharing permissions for
pub type DeleteShareRequest = Vec<String>; pub type DeleteShareRequest = Vec<String>;
``` ```
### Response Structure ### Response Structure
```rust ```rust
// Success response is a simple message // Success response is a simple message
// Error responses include appropriate status codes and error messages // Error responses include appropriate status codes and error messages
``` ```
@ -32,8 +32,8 @@ pub type DeleteShareRequest = Vec<String>;
#### New Files #### New Files
1. `/src/routes/rest/routes/chats/sharing/delete_sharing.rs` - REST handler for deleting sharing permissions 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 2. `/libs/handlers/src/chats/sharing/delete_sharing_handler.rs` - Business logic for deleting sharing permissions
#### REST Handler Implementation #### REST Handler Implementation
@ -182,11 +182,11 @@ The handler will return appropriate error responses:
#### Test Cases #### Test Cases
1. Should remove sharing permissions for valid emails 1. Should remove sharing permissions for valid emails
1. Should return 403 when user doesn't have Owner or FullAccess permission ✅ 2. Should return 403 when user doesn't have Owner or FullAccess permission
1. Should return 404 when chat doesn't exist ✅ 3. Should return 404 when chat doesn't exist
1. Should return 400 when email is invalid ✅ 4. Should return 400 when email is invalid
1. Should handle gracefully when trying to remove sharing for a user that doesn't have access ✅ 5. Should handle gracefully when trying to remove sharing for a user that doesn't have access
### Performance Considerations ### Performance Considerations

View File

@ -18,8 +18,8 @@ The implementation is broken down into the following components, each with its o
3. **Update Chats Sharing Endpoint** - PUT /chats/:id/sharing 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) - 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 4. **Delete Chats Sharing Endpoint** - DELETE /chats/:id/sharing
- PRD: [api_chats_sharing_delete.md](/Users/dallin/buster/buster/api/prds/active/api_chats_sharing_delete.md) - PRD: [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 Sequence and Parallelization
@ -70,7 +70,7 @@ After Phase 1 is complete, the following components can be implemented in parall
- Uses `create_share_by_email` from `@[api/libs/sharing/src]/create_asset_permission.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` - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`
- **Delete Sharing Endpoint** - **Delete Sharing Endpoint**
- Uses `remove_share_by_email` from `@[api/libs/sharing/src]/remove_asset_permissions.rs` - 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` - Uses `has_permission` from `@[api/libs/sharing/src]/check_asset_permission.rs`

View File

@ -9,6 +9,7 @@ mod get_chat_raw_llm_messages;
mod list_chats; mod list_chats;
mod post_chat; mod post_chat;
mod update_chats; mod update_chats;
pub mod sharing;
pub use delete_chats::delete_chats_route; pub use delete_chats::delete_chats_route;
pub use get_chat::get_chat_route; pub use get_chat::get_chat_route;
@ -16,6 +17,7 @@ pub use get_chat_raw_llm_messages::get_chat_raw_llm_messages;
pub use list_chats::list_chats_route; pub use list_chats::list_chats_route;
pub use post_chat::post_chat_route; pub use post_chat::post_chat_route;
pub use update_chats::update_chats_route; pub use update_chats::update_chats_route;
pub use sharing::delete_chat_sharing_rest_handler;
pub fn router() -> Router { pub fn router() -> Router {
Router::new() Router::new()
@ -25,4 +27,5 @@ pub fn router() -> Router {
.route("/", delete(delete_chats_route)) .route("/", delete(delete_chats_route))
.route("/:id", get(get_chat_route)) .route("/:id", get(get_chat_route))
.route("/:id/raw_llm_messages", get(get_chat_raw_llm_messages)) .route("/:id/raw_llm_messages", get(get_chat_raw_llm_messages))
.route("/:id/sharing", delete(delete_chat_sharing_rest_handler))
} }

View File

@ -0,0 +1,74 @@
use axum::{
extract::{Extension, Json, Path},
http::StatusCode,
};
use handlers::chats::delete_chat_sharing_handler;
use middleware::AuthenticatedUser;
use tracing::info;
use uuid::Uuid;
use crate::routes::rest::ApiResponse;
/// REST handler for deleting sharing permissions for a chat
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)> {
info!(
chat_id = %id,
user_id = %user.id,
email_count = emails.len(),
"Processing DELETE request for chat sharing permissions"
);
match delete_chat_sharing_handler(&id, &user.id, emails).await {
Ok(_) => {
info!(chat_id = %id, user_id = %user.id, "Successfully deleted chat sharing permissions");
Ok(ApiResponse::JsonData("Sharing permissions deleted successfully".to_string()))
}
Err(e) => {
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 delete sharing permissions: {}", e)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{body::Body, http::Request, response::Response};
use axum_test::{TestServer, TestResponse};
use middleware::AuthenticatedUser;
use serde_json::{json, Value};
use uuid::Uuid;
async fn mock_server_response(
status_code: StatusCode,
message: &str,
) -> Result<ApiResponse<String>, (StatusCode, String)> {
if status_code == StatusCode::OK {
Ok(ApiResponse::JsonData(message.to_string()))
} else {
Err((status_code, message.to_string()))
}
}
// Note: This is a placeholder for a real test that would be implemented
// using a test framework like axum_test
#[tokio::test]
async fn test_delete_chat_sharing_rest_handler_success() {
// In a real test, we would set up test data and use a test server
// with mocked dependencies to ensure the REST handler works correctly
assert!(true);
}
}

View File

@ -0,0 +1,3 @@
mod delete_sharing;
pub use delete_sharing::delete_chat_sharing_rest_handler;

View File

@ -0,0 +1 @@
pub mod sharing;

View File

@ -0,0 +1,171 @@
use crate::common::{
assertions::response::{assert_status, StatusCompare},
db::MockDB,
fixtures::{
chats::ChatFixtureBuilder,
users::UserFixtureBuilder,
},
http::client::{ClientWithDatabase, RequestBuilderExt},
};
use database::enums::{AssetPermissionRole, AssetType, IdentityType};
use sharing::create_asset_permission::create_share_by_email;
use reqwest::StatusCode;
use serde_json::json;
use uuid::Uuid;
#[tokio::test]
async fn test_delete_chat_sharing_success() {
// Setup test database
let mock_db = MockDB::new().await;
let db_conn = mock_db.get_connection().await;
// Create test user
let user = UserFixtureBuilder::new()
.with_email("test@example.com")
.build(&db_conn)
.await;
// Create another user to share with
let shared_user = UserFixtureBuilder::new()
.with_email("shared@example.com")
.build(&db_conn)
.await;
// Create a test chat owned by the user
let chat = ChatFixtureBuilder::new()
.with_created_by(user.id)
.build(&db_conn)
.await;
// Create sharing permission for the chat
create_share_by_email(
&shared_user.email,
chat.id,
AssetType::Chat,
AssetPermissionRole::FullAccess,
user.id,
).await.unwrap();
// Create client with test database
let client = ClientWithDatabase::new(mock_db);
// Send DELETE request to remove sharing
let response = client
.delete(&format!("/chats/{}/sharing", chat.id))
.with_authentication(&user.id.to_string())
.json(&vec![shared_user.email.clone()])
.send()
.await;
// Assert success response
assert_status!(response, StatusCompare::Is(StatusCode::OK));
// Verify the permission no longer exists
// This would require checking the database or making a GET request to /chats/{id}/sharing
// In a real test, we would validate this properly
}
#[tokio::test]
async fn test_delete_chat_sharing_not_found() {
// Setup test database
let mock_db = MockDB::new().await;
// Create test user
let user = UserFixtureBuilder::new()
.with_email("test@example.com")
.build(&mock_db.get_connection().await)
.await;
// Create client with test database
let client = ClientWithDatabase::new(mock_db);
// Send DELETE request for a non-existent chat
let non_existent_chat_id = Uuid::new_v4();
let response = client
.delete(&format!("/chats/{}/sharing", non_existent_chat_id))
.with_authentication(&user.id.to_string())
.json(&vec!["shared@example.com".to_string()])
.send()
.await;
// Assert not found response
assert_status!(response, StatusCompare::Is(StatusCode::NOT_FOUND));
}
#[tokio::test]
async fn test_delete_chat_sharing_invalid_email() {
// Setup test database
let mock_db = MockDB::new().await;
let db_conn = mock_db.get_connection().await;
// Create test user
let user = UserFixtureBuilder::new()
.with_email("test@example.com")
.build(&db_conn)
.await;
// Create a test chat owned by the user
let chat = ChatFixtureBuilder::new()
.with_created_by(user.id)
.build(&db_conn)
.await;
// Create client with test database
let client = ClientWithDatabase::new(mock_db);
// Send DELETE request with invalid email format
let response = client
.delete(&format!("/chats/{}/sharing", chat.id))
.with_authentication(&user.id.to_string())
.json(&vec!["invalid-email".to_string()])
.send()
.await;
// Assert bad request response
assert_status!(response, StatusCompare::Is(StatusCode::BAD_REQUEST));
}
#[tokio::test]
async fn test_delete_chat_sharing_unauthorized() {
// Setup test database
let mock_db = MockDB::new().await;
let db_conn = mock_db.get_connection().await;
// Create test user (owner)
let owner = UserFixtureBuilder::new()
.with_email("owner@example.com")
.build(&db_conn)
.await;
// Create another user (unauthorized)
let unauthorized_user = UserFixtureBuilder::new()
.with_email("unauthorized@example.com")
.build(&db_conn)
.await;
// Create a shared user
let shared_user = UserFixtureBuilder::new()
.with_email("shared@example.com")
.build(&db_conn)
.await;
// Create a test chat owned by the owner
let chat = ChatFixtureBuilder::new()
.with_created_by(owner.id)
.build(&db_conn)
.await;
// Create client with test database
let client = ClientWithDatabase::new(mock_db);
// Send DELETE request from unauthorized user
let response = client
.delete(&format!("/chats/{}/sharing", chat.id))
.with_authentication(&unauthorized_user.id.to_string())
.json(&vec![shared_user.email.clone()])
.send()
.await;
// Assert forbidden response
assert_status!(response, StatusCompare::Is(StatusCode::FORBIDDEN));
}

View File

@ -0,0 +1 @@
pub mod delete_sharing_test;

View File

@ -2,4 +2,5 @@
pub mod dashboards; pub mod dashboards;
pub mod collections; pub mod collections;
pub mod metrics; pub mod metrics;
pub mod threads_and_messages; pub mod threads_and_messages;
pub mod chats;