feat(authz): check permission on read handlers + check create permission on folder

This commit is contained in:
Edouard Vanbelle
2026-05-21 11:07:04 +02:00
parent cba9be8c21
commit 3362e277ab
21 changed files with 428 additions and 180 deletions
+35 -9
View File
@@ -6,18 +6,21 @@ use crate::application::dtos::display_helpers::{
category_for, icon_class_for, icon_special_class_for,
};
use crate::application::dtos::trash_dto::TrashedItemDto;
use crate::application::ports::authorization_ports::AuthorizationEngine;
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::application::ports::trash_ports::TrashUseCase;
use crate::common::errors::{DomainError, ErrorKind, Result};
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
use crate::domain::repositories::folder_repository::FolderRepository;
use crate::domain::repositories::trash_repository::TrashRepository;
use crate::domain::services::authorization::{Permission, Resource, Subject};
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
use crate::infrastructure::repositories::pg::trash_db_repository::TrashDbRepository;
use crate::infrastructure::services::dedup_service::DedupService;
use crate::infrastructure::services::file_content_cache::FileContentCache;
use crate::infrastructure::services::pg_acl_engine::PgAclEngine;
use crate::infrastructure::services::thumbnail_service::ThumbnailService;
/**
@@ -56,6 +59,9 @@ pub struct TrashService {
/// Content cache — invalidated when files are permanently deleted from trash.
content_cache: Option<Arc<FileContentCache>>,
/// Authz engine
authz: Arc<PgAclEngine>,
/// Number of days items should be kept in trash before automatic cleanup
retention_days: u32,
}
@@ -71,6 +77,7 @@ impl TrashService {
dedup_service: Arc<DedupService>,
thumbnail_service: Option<Arc<ThumbnailService>>,
content_cache: Option<Arc<FileContentCache>>,
authz: Arc<PgAclEngine>,
) -> Self {
Self {
trash_repository,
@@ -80,6 +87,7 @@ impl TrashService {
dedup_service,
thumbnail_service,
content_cache,
authz,
retention_days,
}
}
@@ -176,6 +184,7 @@ impl TrashUseCase for TrashService {
Ok(dtos)
}
// TODO: change item_type into Resource enum
#[instrument(skip(self))]
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: Uuid) -> Result<()> {
info!(
@@ -209,10 +218,23 @@ 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
.require(
Subject::User(user_id),
Permission::Delete,
Resource::File(file_id),
)
.await?;
let file = match self
.file_read_port
.get_file_for_owner(item_id, user_id)
@@ -236,6 +258,7 @@ impl TrashUseCase for TrashService {
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,
@@ -286,6 +309,17 @@ impl TrashUseCase for TrashService {
Ok(())
}
"folder" => {
// check deletion permition
let folder_id = Uuid::parse_str(item_id)
.map_err(|_| DomainError::not_found("Folder", item_id))?;
self.authz
.require(
Subject::User(user_id),
Permission::Delete,
Resource::Folder(folder_id),
)
.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.
@@ -301,18 +335,10 @@ impl TrashUseCase for TrashService {
)
})?;
// Ownership check — return NotFound (not Forbidden) to
// prevent leaking whether the folder exists.
if folder.owner_id() != Some(user_id) {
return Err(DomainError::not_found(
"Folder",
format!("Folder not found: {}", item_id),
));
}
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,