mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
Issue #224: File Manager Permissions
The File Manager (Library) backend had no permission enforcement - endpoints were returning data to any authenticated user regardless of their group permissions. Closes #224
This commit is contained in:
@@ -16,7 +16,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from backend.app.core.auth import (
|
||||
require_auth_if_enabled,
|
||||
require_ownership_permission,
|
||||
require_permission_if_auth_enabled,
|
||||
)
|
||||
@@ -237,7 +236,11 @@ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", "
|
||||
|
||||
@router.get("/folders", response_model=list[FolderTreeItem])
|
||||
@router.get("/folders/", response_model=list[FolderTreeItem])
|
||||
async def list_folders(response: Response, db: AsyncSession = Depends(get_db)):
|
||||
async def list_folders(
|
||||
response: Response,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Get all folders as a tree structure."""
|
||||
# Prevent browser caching of folder list
|
||||
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
@@ -289,7 +292,11 @@ async def list_folders(response: Response, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/folders/by-project/{project_id}", response_model=list[FolderResponse])
|
||||
async def get_folders_by_project(project_id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def get_folders_by_project(
|
||||
project_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Get all folders linked to a specific project."""
|
||||
result = await db.execute(
|
||||
select(LibraryFolder, Project.name)
|
||||
@@ -326,7 +333,11 @@ async def get_folders_by_project(project_id: int, db: AsyncSession = Depends(get
|
||||
|
||||
|
||||
@router.get("/folders/by-archive/{archive_id}", response_model=list[FolderResponse])
|
||||
async def get_folders_by_archive(archive_id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def get_folders_by_archive(
|
||||
archive_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Get all folders linked to a specific archive."""
|
||||
result = await db.execute(
|
||||
select(LibraryFolder, PrintArchive.print_name)
|
||||
@@ -364,7 +375,11 @@ async def get_folders_by_archive(archive_id: int, db: AsyncSession = Depends(get
|
||||
|
||||
@router.post("/folders", response_model=FolderResponse)
|
||||
@router.post("/folders/", response_model=FolderResponse)
|
||||
async def create_folder(data: FolderCreate, db: AsyncSession = Depends(get_db)):
|
||||
async def create_folder(
|
||||
data: FolderCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_UPLOAD)),
|
||||
):
|
||||
"""Create a new folder."""
|
||||
# Verify parent exists if specified
|
||||
if data.parent_id is not None:
|
||||
@@ -415,7 +430,11 @@ async def create_folder(data: FolderCreate, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/folders/{folder_id}", response_model=FolderResponse)
|
||||
async def get_folder(folder_id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def get_folder(
|
||||
folder_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Get a folder by ID."""
|
||||
result = await db.execute(
|
||||
select(LibraryFolder, Project.name, PrintArchive.print_name)
|
||||
@@ -449,8 +468,17 @@ async def get_folder(folder_id: int, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.put("/folders/{folder_id}", response_model=FolderResponse)
|
||||
async def update_folder(folder_id: int, data: FolderUpdate, db: AsyncSession = Depends(get_db)):
|
||||
"""Update a folder."""
|
||||
async def update_folder(
|
||||
folder_id: int,
|
||||
data: FolderUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_UPDATE_ALL)),
|
||||
):
|
||||
"""Update a folder.
|
||||
|
||||
Note: Folders require library:update_all permission since they don't have
|
||||
ownership tracking.
|
||||
"""
|
||||
result = await db.execute(select(LibraryFolder).where(LibraryFolder.id == folder_id))
|
||||
folder = result.scalar_one_or_none()
|
||||
|
||||
@@ -595,6 +623,7 @@ async def list_files(
|
||||
folder_id: int | None = None,
|
||||
include_root: bool = True,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""List files, optionally filtered by folder.
|
||||
|
||||
@@ -669,7 +698,7 @@ async def upload_file(
|
||||
folder_id: int | None = None,
|
||||
generate_stl_thumbnails: bool = Query(default=True),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User | None = Depends(require_auth_if_enabled),
|
||||
current_user: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_UPLOAD)),
|
||||
):
|
||||
"""Upload a file to the library."""
|
||||
try:
|
||||
@@ -805,7 +834,7 @@ async def extract_zip_file(
|
||||
create_folder_from_zip: bool = Query(default=False),
|
||||
generate_stl_thumbnails: bool = Query(default=True),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User | None = Depends(require_auth_if_enabled),
|
||||
current_user: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_UPLOAD)),
|
||||
):
|
||||
"""Upload and extract a ZIP file to the library.
|
||||
|
||||
@@ -1064,9 +1093,13 @@ async def extract_zip_file(
|
||||
async def batch_generate_stl_thumbnails(
|
||||
request: BatchThumbnailRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_UPDATE_ALL)),
|
||||
):
|
||||
"""Generate thumbnails for STL files in batch.
|
||||
|
||||
Note: Requires library:update_all permission since this is a batch operation
|
||||
that may affect files owned by different users.
|
||||
|
||||
Can generate thumbnails for:
|
||||
- Specific file IDs (file_ids)
|
||||
- All STL files in a folder (folder_id)
|
||||
@@ -1188,6 +1221,7 @@ def is_sliced_file(filename: str) -> bool:
|
||||
async def add_files_to_queue(
|
||||
request: AddToQueueRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.QUEUE_CREATE)),
|
||||
):
|
||||
"""Add library files to the print queue.
|
||||
|
||||
@@ -1266,6 +1300,7 @@ async def add_files_to_queue(
|
||||
async def get_library_file_plates(
|
||||
file_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Get available plates from a multi-plate 3MF library file.
|
||||
|
||||
@@ -1477,6 +1512,7 @@ async def get_library_file_filament_requirements(
|
||||
file_id: int,
|
||||
plate_id: int | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Get filament requirements from a library file.
|
||||
|
||||
@@ -1599,6 +1635,7 @@ async def print_library_file(
|
||||
printer_id: int,
|
||||
body: FilePrintRequest | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.PRINTERS_CONTROL)),
|
||||
):
|
||||
"""Print a library file directly.
|
||||
|
||||
@@ -1786,7 +1823,11 @@ async def print_library_file(
|
||||
|
||||
|
||||
@router.get("/files/{file_id}", response_model=FileResponseSchema)
|
||||
async def get_file(file_id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def get_file(
|
||||
file_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Get a file by ID with full details."""
|
||||
result = await db.execute(
|
||||
select(LibraryFile).options(selectinload(LibraryFile.created_by)).where(LibraryFile.id == file_id)
|
||||
@@ -1961,7 +2002,11 @@ async def delete_file(
|
||||
|
||||
|
||||
@router.get("/files/{file_id}/download")
|
||||
async def download_file(file_id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def download_file(
|
||||
file_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Download a file."""
|
||||
result = await db.execute(select(LibraryFile).where(LibraryFile.id == file_id))
|
||||
file = result.scalar_one_or_none()
|
||||
@@ -2008,7 +2053,11 @@ async def get_thumbnail(file_id: int, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/files/{file_id}/gcode")
|
||||
async def get_gcode(file_id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def get_gcode(
|
||||
file_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Get gcode for a file (for preview)."""
|
||||
result = await db.execute(select(LibraryFile).where(LibraryFile.id == file_id))
|
||||
file = result.scalar_one_or_none()
|
||||
@@ -2046,8 +2095,22 @@ async def get_gcode(file_id: int, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/files/move")
|
||||
async def move_files(data: FileMoveRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Move multiple files to a folder."""
|
||||
async def move_files(
|
||||
data: FileMoveRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
auth_result: tuple[User | None, bool] = Depends(
|
||||
require_ownership_permission(
|
||||
Permission.LIBRARY_UPDATE_ALL,
|
||||
Permission.LIBRARY_UPDATE_OWN,
|
||||
)
|
||||
),
|
||||
):
|
||||
"""Move multiple files to a folder.
|
||||
|
||||
Files not owned by the user are skipped (unless user has *_all permission).
|
||||
"""
|
||||
user, can_modify_all = auth_result
|
||||
|
||||
# Verify folder exists if specified
|
||||
if data.folder_id is not None:
|
||||
folder_result = await db.execute(select(LibraryFolder).where(LibraryFolder.id == data.folder_id))
|
||||
@@ -2056,14 +2119,19 @@ async def move_files(data: FileMoveRequest, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
# Update files
|
||||
moved = 0
|
||||
skipped = 0
|
||||
for file_id in data.file_ids:
|
||||
result = await db.execute(select(LibraryFile).where(LibraryFile.id == file_id))
|
||||
file = result.scalar_one_or_none()
|
||||
if file:
|
||||
# Ownership check
|
||||
if not can_modify_all and file.created_by_id != user.id:
|
||||
skipped += 1
|
||||
continue
|
||||
file.folder_id = data.folder_id
|
||||
moved += 1
|
||||
|
||||
return {"status": "success", "moved": moved}
|
||||
return {"status": "success", "moved": moved, "skipped": skipped}
|
||||
|
||||
|
||||
@router.post("/bulk-delete", response_model=BulkDeleteResponse)
|
||||
@@ -2133,7 +2201,10 @@ async def bulk_delete(
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_library_stats(db: AsyncSession = Depends(get_db)):
|
||||
async def get_library_stats(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
|
||||
):
|
||||
"""Get library statistics."""
|
||||
# Total files
|
||||
total_files_result = await db.execute(select(func.count(LibraryFile.id)))
|
||||
|
||||
Reference in New Issue
Block a user