mirror of https://github.com/buster-so/buster.git
merging api_dashboards_sharing_delete
This commit is contained in:
commit
cbd52d51bb
|
@ -0,0 +1,146 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use database::{
|
||||||
|
enums::{AssetPermissionRole, AssetType, IdentityType},
|
||||||
|
pool::get_pg_pool,
|
||||||
|
schema::dashboard_files,
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Deletes sharing permissions for a specific dashboard
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `dashboard_id` - The unique identifier of the dashboard
|
||||||
|
/// * `user_id` - The unique identifier of the user requesting the deletion
|
||||||
|
/// * `emails` - Vector of email addresses to remove sharing for
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Result indicating success or failure
|
||||||
|
pub async fn delete_dashboard_sharing_handler(
|
||||||
|
dashboard_id: &Uuid,
|
||||||
|
user_id: &Uuid,
|
||||||
|
emails: Vec<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!(
|
||||||
|
dashboard_id = %dashboard_id,
|
||||||
|
user_id = %user_id,
|
||||||
|
email_count = emails.len(),
|
||||||
|
"Deleting dashboard sharing permissions"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Validate the dashboard 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 dashboard_exists = dashboard_files::table
|
||||||
|
.filter(dashboard_files::id.eq(dashboard_id))
|
||||||
|
.filter(dashboard_files::deleted_at.is_null())
|
||||||
|
.count()
|
||||||
|
.get_result::<i64>(&mut conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Error checking if dashboard exists: {}", e);
|
||||||
|
anyhow!("Database error: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if dashboard_exists == 0 {
|
||||||
|
error!(
|
||||||
|
dashboard_id = %dashboard_id,
|
||||||
|
"Dashboard not found"
|
||||||
|
);
|
||||||
|
return Err(anyhow!("Dashboard not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if user has permission to delete sharing for the dashboard (Owner or FullAccess)
|
||||||
|
let has_permission = has_permission(
|
||||||
|
*dashboard_id,
|
||||||
|
AssetType::DashboardFile,
|
||||||
|
*user_id,
|
||||||
|
IdentityType::User,
|
||||||
|
AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(
|
||||||
|
dashboard_id = %dashboard_id,
|
||||||
|
user_id = %user_id,
|
||||||
|
"Error checking dashboard permissions: {}", e
|
||||||
|
);
|
||||||
|
anyhow!("Error checking dashboard permissions: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !has_permission {
|
||||||
|
error!(
|
||||||
|
dashboard_id = %dashboard_id,
|
||||||
|
user_id = %user_id,
|
||||||
|
"User does not have permission to delete sharing for this dashboard"
|
||||||
|
);
|
||||||
|
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::DashboardFile,
|
||||||
|
*user_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
info!(
|
||||||
|
dashboard_id = %dashboard_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!(
|
||||||
|
dashboard_id = %dashboard_id,
|
||||||
|
email = %email,
|
||||||
|
"No active permission found to delete"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
error!(
|
||||||
|
dashboard_id = %dashboard_id,
|
||||||
|
email = %email,
|
||||||
|
"Failed to delete sharing: {}", e
|
||||||
|
);
|
||||||
|
return Err(anyhow!("Failed to delete sharing for email {}: {}", email, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
dashboard_id = %dashboard_id,
|
||||||
|
email_count = emails.len(),
|
||||||
|
"Successfully deleted dashboard sharing permissions"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_dashboard_sharing_handler() {
|
||||||
|
// Placeholder test implementation
|
||||||
|
assert!(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
mod list_sharing_handler;
|
mod list_sharing_handler;
|
||||||
mod create_sharing_handler;
|
mod create_sharing_handler;
|
||||||
|
|
||||||
pub use list_sharing_handler::list_dashboard_sharing_handler;
|
pub use create_sharing_handler::create_dashboard_sharing_handler;
|
||||||
pub use create_sharing_handler::create_dashboard_sharing_handler;
|
mod delete_sharing_handler;
|
||||||
|
|
||||||
|
pub use delete_sharing_handler::delete_dashboard_sharing_handler;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# API Dashboards Sharing - Delete Endpoint PRD
|
# API Dashboards Sharing - Delete Endpoint PRD ✅
|
||||||
|
|
||||||
## Problem Statement
|
## Problem Statement
|
||||||
Users need the ability to remove sharing permissions for dashboards through a REST API endpoint.
|
Users need the ability to remove sharing permissions for dashboards through a REST API endpoint.
|
||||||
|
|
|
@ -18,7 +18,7 @@ The implementation is broken down into the following components, each with its o
|
||||||
3. **Update Dashboards Sharing Endpoint** - PUT /dashboards/:id/sharing
|
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)
|
- 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
|
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: [api_dashboards_sharing_delete.md](/Users/dallin/buster/buster/api/prds/active/api_dashboards_sharing_delete.md)
|
||||||
|
|
||||||
## PRD Development Sequence and Parallelization
|
## PRD Development Sequence and Parallelization
|
||||||
|
@ -54,10 +54,10 @@ The PRDs can be developed in the following order, with opportunities for paralle
|
||||||
- Update `/src/routes/rest/routes/dashboards/mod.rs` to include the sharing router
|
- 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
|
- Update `/libs/handlers/src/dashboards/mod.rs` to export the sharing module
|
||||||
|
|
||||||
### Phase 2: Core Endpoints (Can be Parallelized)
|
### Phase 2: Core Endpoints (Can be Parallelized) ✅
|
||||||
After Phase 1 is complete, the following components can be implemented in parallel by different developers:
|
After Phase 1 is complete, the following components can be implemented in parallel by different developers:
|
||||||
|
|
||||||
- **List Sharing Endpoint**
|
- **List Sharing Endpoint** ✅
|
||||||
- Uses `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs`
|
- Uses `list_shares` from `@[api/libs/sharing/src]/list_asset_permissions.rs`
|
||||||
- Uses `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs`
|
- Uses `check_access` from `@[api/libs/sharing/src]/check_asset_permission.rs`
|
||||||
|
|
||||||
|
@ -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`
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
|
routing::{delete, get},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,4 +15,5 @@ pub fn router() -> Router {
|
||||||
.route("/", get(list_dashboards::list_dashboard_rest_handler))
|
.route("/", get(list_dashboards::list_dashboard_rest_handler))
|
||||||
.route("/:id/sharing", get(sharing::list_dashboard_sharing_rest_handler))
|
.route("/:id/sharing", get(sharing::list_dashboard_sharing_rest_handler))
|
||||||
.route("/:id/sharing", post(sharing::create_dashboard_sharing_rest_handler))
|
.route("/:id/sharing", post(sharing::create_dashboard_sharing_rest_handler))
|
||||||
|
.route("/:id/sharing", delete(sharing::delete_dashboard_sharing_rest_handler))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
use axum::{
|
||||||
|
extract::{Extension, Json, Path},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use handlers::dashboards::sharing::delete_dashboard_sharing_handler;
|
||||||
|
use middleware::AuthenticatedUser;
|
||||||
|
use tracing::info;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::routes::rest::ApiResponse;
|
||||||
|
|
||||||
|
/// REST handler for deleting dashboard sharing permissions
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `user` - The authenticated user making the request
|
||||||
|
/// * `id` - The unique identifier of the dashboard
|
||||||
|
/// * `request` - Vector of email addresses to remove sharing for
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A success message or an error response
|
||||||
|
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)> {
|
||||||
|
info!(
|
||||||
|
dashboard_id = %id,
|
||||||
|
user_id = %user.id,
|
||||||
|
email_count = request.len(),
|
||||||
|
"Processing DELETE request for dashboard sharing permissions"
|
||||||
|
);
|
||||||
|
|
||||||
|
match delete_dashboard_sharing_handler(&id, &user.id, request).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!(
|
||||||
|
dashboard_id = %id,
|
||||||
|
user_id = %user.id,
|
||||||
|
"Successfully deleted dashboard 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!("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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
mod list_sharing;
|
mod list_sharing;
|
||||||
mod create_sharing;
|
mod create_sharing;
|
||||||
|
mod delete_sharing;
|
||||||
|
|
||||||
pub use list_sharing::list_dashboard_sharing_rest_handler;
|
pub use list_sharing::list_dashboard_sharing_rest_handler;
|
||||||
pub use create_sharing::create_dashboard_sharing_rest_handler;
|
pub use delete_sharing::delete_dashboard_sharing_rest_handler;
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{header, Method, Request, StatusCode},
|
||||||
|
};
|
||||||
|
use database::enums::{AssetPermissionRole, AssetType, IdentityType};
|
||||||
|
use serde_json::json;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::common::{
|
||||||
|
fixtures::{dashboards::create_test_dashboard, users::create_test_user},
|
||||||
|
http::client::TestClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_dashboard_sharing() {
|
||||||
|
// Setup test data
|
||||||
|
let client = TestClient::new().await;
|
||||||
|
let test_user = create_test_user();
|
||||||
|
let dashboard = create_test_dashboard();
|
||||||
|
|
||||||
|
// Create a user to share with
|
||||||
|
let shared_user = create_test_user();
|
||||||
|
|
||||||
|
// Setup sharing permission manually
|
||||||
|
client.setup_asset_permission(
|
||||||
|
dashboard.id,
|
||||||
|
AssetType::DashboardFile,
|
||||||
|
shared_user.id,
|
||||||
|
IdentityType::User,
|
||||||
|
AssetPermissionRole::CanView,
|
||||||
|
test_user.id,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
// Create the delete request
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::DELETE)
|
||||||
|
.uri(format!("/dashboards/{}/sharing", dashboard.id))
|
||||||
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
.header("X-User-ID", test_user.id.to_string())
|
||||||
|
.body(Body::from(
|
||||||
|
serde_json::to_string(&vec![shared_user.email.clone()]).unwrap(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
let response = client.app.oneshot(request).await.unwrap();
|
||||||
|
|
||||||
|
// Verify response
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// Verify that the sharing permission is actually deleted
|
||||||
|
let permissions = client
|
||||||
|
.get_asset_permissions(dashboard.id, AssetType::DashboardFile)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Should only contain the owner's permission
|
||||||
|
assert_eq!(permissions.len(), 1);
|
||||||
|
assert_eq!(permissions[0].identity_id, test_user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_dashboard_sharing_not_found() {
|
||||||
|
// Setup
|
||||||
|
let client = TestClient::new().await;
|
||||||
|
let user = create_test_user();
|
||||||
|
let non_existent_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
// Create request with non-existent dashboard
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::DELETE)
|
||||||
|
.uri(format!("/dashboards/{}/sharing", non_existent_id))
|
||||||
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
.header("X-User-ID", user.id.to_string())
|
||||||
|
.body(Body::from(
|
||||||
|
serde_json::to_string(&vec!["test@example.com".to_string()]).unwrap(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
let response = client.app.oneshot(request).await.unwrap();
|
||||||
|
|
||||||
|
// Verify 404 response
|
||||||
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_dashboard_sharing_insufficient_permissions() {
|
||||||
|
// Setup
|
||||||
|
let client = TestClient::new().await;
|
||||||
|
let owner = create_test_user();
|
||||||
|
let user_without_permission = create_test_user();
|
||||||
|
let dashboard = create_test_dashboard();
|
||||||
|
|
||||||
|
// Set up the dashboard with owner
|
||||||
|
client.setup_asset_permission(
|
||||||
|
dashboard.id,
|
||||||
|
AssetType::DashboardFile,
|
||||||
|
owner.id,
|
||||||
|
IdentityType::User,
|
||||||
|
AssetPermissionRole::Owner,
|
||||||
|
owner.id,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
// Create request from user without permission
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::DELETE)
|
||||||
|
.uri(format!("/dashboards/{}/sharing", dashboard.id))
|
||||||
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
.header("X-User-ID", user_without_permission.id.to_string())
|
||||||
|
.body(Body::from(
|
||||||
|
serde_json::to_string(&vec!["test@example.com".to_string()]).unwrap(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
let response = client.app.oneshot(request).await.unwrap();
|
||||||
|
|
||||||
|
// Verify 403 Forbidden response
|
||||||
|
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_dashboard_sharing_invalid_email() {
|
||||||
|
// Setup
|
||||||
|
let client = TestClient::new().await;
|
||||||
|
let user = create_test_user();
|
||||||
|
let dashboard = create_test_dashboard();
|
||||||
|
|
||||||
|
// Set up the dashboard with owner
|
||||||
|
client.setup_asset_permission(
|
||||||
|
dashboard.id,
|
||||||
|
AssetType::DashboardFile,
|
||||||
|
user.id,
|
||||||
|
IdentityType::User,
|
||||||
|
AssetPermissionRole::Owner,
|
||||||
|
user.id,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
// Create request with invalid email
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::DELETE)
|
||||||
|
.uri(format!("/dashboards/{}/sharing", dashboard.id))
|
||||||
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
.header("X-User-ID", user.id.to_string())
|
||||||
|
.body(Body::from(
|
||||||
|
serde_json::to_string(&vec!["invalid-email-no-at-sign".to_string()]).unwrap(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
let response = client.app.oneshot(request).await.unwrap();
|
||||||
|
|
||||||
|
// Verify 400 Bad Request response
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
mod list_sharing_test;
|
mod list_sharing_test;
|
||||||
mod create_sharing_test;
|
mod create_sharing_test;
|
||||||
|
mod delete_sharing_test;
|
||||||
|
|
Loading…
Reference in New Issue