fix(authz): permit policiy: a user with Delete permission can delete a file/folder. Only the owner can permanently delete or restore a trashed item

This commit is contained in:
Edouard Vanbelle
2026-05-21 22:45:49 +02:00
parent a1c21ce446
commit dd68d783e0
2 changed files with 12 additions and 28 deletions
+5 -19
View File
@@ -218,13 +218,6 @@ impl TrashUseCase for TrashService {
"file" => {
info!("Processing file to move to trash: {}", item_id);
// XXX: right now only owner can move to trash, need to improve
// Get the file — ownership-verified at SQL level.
// Returns NotFound if the file does not exist OR belongs to
// another user, preventing cross-user trash operations.
debug!("Getting file data (owner-scoped): {}", item_id);
let file_id = Uuid::parse_str(item_id)
.map_err(|_| DomainError::not_found("File", item_id))?;
self.authz
@@ -235,11 +228,11 @@ impl TrashUseCase for TrashService {
)
.await?;
let file = match self
.file_read_port
.get_file_for_owner(item_id, user_id)
.await
{
// Authz already passed — use the non-owner-scoped read so that
// grantees with Delete permission can trash files they don't own.
// The file's user_id in storage.files is unchanged, so the item
// will appear in the original owner's trash view.
let file = match self.file_read_port.get_file(item_id).await {
Ok(file) => {
debug!("File found: {} ({})", file.name(), item_id);
file
@@ -257,8 +250,6 @@ impl TrashUseCase for TrashService {
let original_path = file.storage_path().to_string();
debug!("Original file path: {}", original_path);
// Create the trash item
// FIXME: item will be created with user_id that mat not be the owner_id
debug!("Creating TrashedItem object for the file");
let trashed_item = TrashedItem::new(
item_uuid,
@@ -320,9 +311,6 @@ impl TrashUseCase for TrashService {
)
.await?;
// Get the folder and verify ownership.
// Returns NotFound if the folder does not exist or belongs
// to another user — prevents cross-user trash operations.
let folder = self
.folder_storage_port
.get_folder(item_id)
@@ -337,8 +325,6 @@ impl TrashUseCase for TrashService {
let original_path = folder.storage_path().to_string();
// Create the trash item
// FIXME: item will be created with user_id that mat not be the owner_id
let trashed_item = TrashedItem::new(
item_uuid,
user_uuid,
+7 -9
View File
@@ -1179,24 +1179,22 @@ Content-Type: application/json
HTTP 200
# Batch trash — CURRENT LIMITATION: even with Admin (Delete grant via
# engine), the trash flow inside trash_service uses get_file_for_owner
# at the data layer, which is owner-scoped. So a non-owner with Delete
# grant gets engine-OK but the SQL filter blocks the fetch → 400.
# This is documented inconsistency; a follow-up should make trash use
# the engine for its lookup too. For now: only the owner can trash.
# Frank (Admin grant = Delete) trashes batch_file_2 — item goes to
# Alice's trash because file.user_id is unchanged (Alice is still owner).
POST {{base_url}}/api/batch/trash
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_2_id}}"], "folder_ids": [] }
HTTP 400
HTTP 200
[Asserts]
jsonpath "$.stats.successful" == 1
# Alice (owner) CAN batch-trash — keeps coverage of the success path.
# Alice trashes batch_file_1 (which frank moved into batch_sub_a in Phase 3C).
POST {{base_url}}/api/batch/trash
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_2_id}}"], "folder_ids": [] }
{ "file_ids": ["{{batch_file_1_id}}"], "folder_ids": [] }
HTTP 200
[Asserts]