feat(authz): covert and test chunked upload with permissions

This commit is contained in:
Edouard Vanbelle
2026-05-21 19:11:28 +02:00
parent bd1b17b589
commit a53c09f361
3 changed files with 139 additions and 8 deletions
@@ -770,8 +770,15 @@ impl ChunkedUploadService {
}
/// Cancel an upload and cleanup — disk I/O outside lock.
async fn cancel_upload_inner(&self, upload_id: &str, user_id: &str) -> Result<(), String> {
self.verify_session_owner(upload_id, user_id)?;
///
/// Returns:
/// - `DomainError::NotFound` if no session matches `upload_id` for `user_id`
/// (covers both "session missing" and "owned by someone else" — same
/// error for anti-enumeration).
/// - `DomainError::InternalError` for unexpected disk I/O failures.
async fn cancel_upload_inner(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError> {
self.verify_session_owner(upload_id, user_id)
.map_err(|_| DomainError::not_found("Upload", upload_id))?;
// Remove from map (~µs)
let removed = self.sessions.remove(upload_id).map(|(_, s)| s);
@@ -861,9 +868,11 @@ impl ChunkedUploadPort for ChunkedUploadService {
}
async fn cancel_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError> {
// Inner function now returns DomainError with proper variants
// (NotFound for missing/wrong-owner sessions, InternalError otherwise),
// so no mapping needed here.
self.cancel_upload_inner(upload_id, &user_id.to_string())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
}
fn should_use_chunked(&self, size: u64) -> bool {
@@ -21,8 +21,10 @@ use utoipa::ToSchema;
use crate::application::ports::chunked_upload_ports::ChunkedUploadPort;
use crate::application::ports::chunked_upload_ports::DEFAULT_CHUNK_SIZE;
use crate::application::ports::file_ports::FileUploadUseCase;
use crate::application::ports::folder_ports::FolderUseCase;
use crate::application::ports::storage_ports::StorageUsagePort;
use crate::common::di::AppState;
use crate::domain::services::authorization::Permission;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::AuthUser;
@@ -90,8 +92,6 @@ impl ChunkedUploadHandler {
/// "expires_at": 86400
/// }
/// ```
/// TODO: how is implemented security (owneship, permission ?)
/// current caveat: upload can start without know is path permits upload
pub(super) async fn create_upload_impl(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
@@ -120,6 +120,27 @@ impl ChunkedUploadHandler {
.into_response();
}
// ── Permission pre-check: caller must have Create on the target
// folder BEFORE we allocate a session and accept chunks. The
// upload service re-checks at finalize time, but failing here
// avoids wasting client+server resources on chunks that will be
// rejected. None = caller's root namespace, no check needed.
if let Some(ref fid) = request.folder_id
&& let Err(err) = state
.applications
.folder_service_concrete
.has_permission(auth_user.id, Permission::Create, fid)
.await
{
tracing::warn!(
"⛔ CHUNKED UPLOAD REJECTED (no perm): user='{}' folder='{}' err='{}'",
auth_user.username,
fid,
err
);
return AppError::from(err).into_response();
}
// ── Quota enforcement ────────────────────────────────────
if let Some(storage_svc) = state.storage_usage_service.as_ref()
&& let Err(err) = storage_svc
@@ -352,9 +373,7 @@ impl ChunkedUploadHandler {
.await
{
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(e) => {
AppError::internal_error(format!("Failed to cancel upload: {}", e)).into_response()
}
Err(e) => AppError::from(e).into_response(),
}
}
}