From 0a0c73bbd776eadb30f5dd9bfa3bcfcbdf661338 Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Tue, 23 Jun 2026 09:30:09 +0800 Subject: [PATCH] fix(api): enforce media result permission boundaries --- apps/api/app/api/routes/assets.py | 33 +++++- apps/api/app/api/routes/generated_videos.py | 81 ++++++++------- apps/api/app/api/routes/generation_tasks.py | 108 ++++++++++++-------- apps/api/app/api/routes/permissions.py | 16 +++ 4 files changed, 153 insertions(+), 85 deletions(-) create mode 100644 apps/api/app/api/routes/permissions.py diff --git a/apps/api/app/api/routes/assets.py b/apps/api/app/api/routes/assets.py index cca253167..4bbe5ae07 100644 --- a/apps/api/app/api/routes/assets.py +++ b/apps/api/app/api/routes/assets.py @@ -1,8 +1,15 @@ from typing import Any -from app.dependencies import get_asset_repository +from app.auth import AuthenticatedUser, get_current_user +from app.api.routes.permissions import require_workspace_member +from app.dependencies import ( + get_asset_library_repository, + get_asset_repository, + get_project_repository, + get_workspace_member_repository, +) from app.schemas.asset import AssetResponse, CreateAssetRequest, ListAssetsResponse -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from packages.application import ( CreateAssetCommand, @@ -10,6 +17,7 @@ from packages.application import ( ListAssetsUseCase, ) from packages.domain import AssetStatus, ClassificationStatus +from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() @@ -41,8 +49,15 @@ def _to_asset_response(item) -> AssetResponse: @router.get("", response_model=ListAssetsResponse) def list_assets( library_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), asset_repository: Any = Depends(get_asset_repository), + asset_library_repository: Any = Depends(get_asset_library_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ListAssetsResponse: + library = asset_library_repository.get(library_id) + if library is None: + raise HTTPException(status_code=404, detail=f"AssetLibrary {library_id} not found") + require_workspace_member(library.workspace_id, authenticated_user, workspace_member_repository) use_case = ListAssetsUseCase(asset_repository) items = use_case.execute(library_id) return ListAssetsResponse(items=[_to_asset_response(item) for item in items]) @@ -51,8 +66,20 @@ def list_assets( @router.post("", response_model=AssetResponse) def create_asset( request: CreateAssetRequest, + authenticated_user: AuthenticatedUser = Depends(get_current_user), asset_repository: Any = Depends(get_asset_repository), + asset_library_repository: Any = Depends(get_asset_library_repository), + project_repository: Any = Depends(get_project_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> AssetResponse: + require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository) + project = project_repository.find_by_id(request.project_id) + if project is None or project.workspace_id != request.workspace_id: + raise HTTPException(status_code=404, detail=f"Project {request.project_id} not found") + library = asset_library_repository.get(request.library_id) + if library is None or library.workspace_id != request.workspace_id or library.project_id != request.project_id: + raise HTTPException(status_code=404, detail=f"AssetLibrary {request.library_id} not found") + use_case = CreateAssetUseCase(asset_repository) item = use_case.execute( CreateAssetCommand( @@ -73,7 +100,7 @@ def create_asset( status=AssetStatus(request.status), classification_status=ClassificationStatus(request.classification_status), quality_score=request.quality_score, - uploaded_by_user_id=request.uploaded_by_user_id, + uploaded_by_user_id=authenticated_user.user.id, ) ) return _to_asset_response(item) diff --git a/apps/api/app/api/routes/generated_videos.py b/apps/api/app/api/routes/generated_videos.py index 1f3c47b91..876140210 100644 --- a/apps/api/app/api/routes/generated_videos.py +++ b/apps/api/app/api/routes/generated_videos.py @@ -1,7 +1,9 @@ from typing import Any +from app.auth import AuthenticatedUser, get_current_user +from app.api.routes.permissions import require_workspace_member from app.core.storage import OSSStorageService, get_storage_service -from app.dependencies import get_generated_video_repository +from app.dependencies import get_generated_video_repository, get_project_repository, get_workspace_member_repository from app.schemas.generated_video import ( GeneratedVideoDownloadUrlResponse, GeneratedVideoResponse, @@ -14,47 +16,12 @@ from packages.application import ( GetGeneratedVideoUseCase, ListGeneratedVideosUseCase, ) +from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() -@router.get("", response_model=ListGeneratedVideosResponse) -def list_generated_videos( - project_id: str, - generated_video_repository: Any = Depends(get_generated_video_repository), -) -> ListGeneratedVideosResponse: - use_case = ListGeneratedVideosUseCase(generated_video_repository) - items = use_case.execute(project_id) - return ListGeneratedVideosResponse( - items=[ - GeneratedVideoResponse( - id=item.id, - workspace_id=item.workspace_id, - project_id=item.project_id, - generation_task_id=item.generation_task_id, - name=item.name, - file_url=item.file_url, - file_size=item.file_size, - duration=item.duration, - thumbnail_url=item.thumbnail_url, - width=item.width, - height=item.height, - fps=item.fps, - ) - for item in items - ] - ) - - -@router.get("/{video_id}", response_model=GeneratedVideoResponse) -def get_generated_video( - video_id: str, - generated_video_repository: Any = Depends(get_generated_video_repository), -) -> GeneratedVideoResponse: - use_case = GetGeneratedVideoUseCase(generated_video_repository) - item = use_case.execute(video_id) - if item is None: - raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found") +def _to_generated_video_response(item) -> GeneratedVideoResponse: return GeneratedVideoResponse( id=item.id, workspace_id=item.workspace_id, @@ -71,12 +38,50 @@ def get_generated_video( ) +@router.get("", response_model=ListGeneratedVideosResponse) +def list_generated_videos( + project_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), + generated_video_repository: Any = Depends(get_generated_video_repository), + project_repository: Any = Depends(get_project_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), +) -> ListGeneratedVideosResponse: + project = project_repository.find_by_id(project_id) + if project is None: + raise HTTPException(status_code=404, detail=f"Project {project_id} not found") + require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository) + use_case = ListGeneratedVideosUseCase(generated_video_repository) + items = use_case.execute(project_id) + return ListGeneratedVideosResponse(items=[_to_generated_video_response(item) for item in items]) + + +@router.get("/{video_id}", response_model=GeneratedVideoResponse) +def get_generated_video( + video_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), + generated_video_repository: Any = Depends(get_generated_video_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), +) -> GeneratedVideoResponse: + use_case = GetGeneratedVideoUseCase(generated_video_repository) + item = use_case.execute(video_id) + if item is None: + raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found") + require_workspace_member(item.workspace_id, authenticated_user, workspace_member_repository) + return _to_generated_video_response(item) + + @router.get("/{video_id}/download-url", response_model=GeneratedVideoDownloadUrlResponse) def get_generated_video_download_url( video_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), generated_video_repository: Any = Depends(get_generated_video_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), storage_service: OSSStorageService = Depends(get_storage_service), ) -> GeneratedVideoDownloadUrlResponse: + video = generated_video_repository.get(video_id) + if video is None: + raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found") + require_workspace_member(video.workspace_id, authenticated_user, workspace_member_repository) use_case = GetGeneratedVideoDownloadUrlUseCase(generated_video_repository) file_url = use_case.execute(video_id) if file_url is None: diff --git a/apps/api/app/api/routes/generation_tasks.py b/apps/api/app/api/routes/generation_tasks.py index 24aeca7f4..f7431ac0a 100644 --- a/apps/api/app/api/routes/generation_tasks.py +++ b/apps/api/app/api/routes/generation_tasks.py @@ -1,9 +1,14 @@ from typing import Any +from app.auth import AuthenticatedUser, get_current_user +from app.api.routes.permissions import require_workspace_member from app.core.celery_app import celery_app from app.dependencies import ( + get_asset_library_repository, get_generated_video_repository, get_generation_task_repository, + get_project_repository, + get_workspace_member_repository, ) from app.schemas.generated_video import ( GeneratedVideoResponse, @@ -21,15 +26,60 @@ from packages.application import ( GetGenerationTaskUseCase, ListGeneratedVideosByTaskUseCase, ) +from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() +def _to_generation_task_response(task) -> GenerationTaskResponse: + return GenerationTaskResponse( + id=task.id, + workspace_id=task.workspace_id, + project_id=task.project_id, + asset_library_id=task.asset_library_id, + strategy_id=task.strategy_id, + voice_library_id=task.voice_library_id, + status=task.status.value, + progress=task.progress, + result_count=task.result_count, + error_message=task.error_message, + ) + + +def _to_generated_video_response(item) -> GeneratedVideoResponse: + return GeneratedVideoResponse( + id=item.id, + workspace_id=item.workspace_id, + project_id=item.project_id, + generation_task_id=item.generation_task_id, + name=item.name, + file_url=item.file_url, + file_size=item.file_size, + duration=item.duration, + thumbnail_url=item.thumbnail_url, + width=item.width, + height=item.height, + fps=item.fps, + ) + + @router.post("/tasks", response_model=GenerationTaskResponse) def create_generation_task( request: CreateGenerationTaskRequest, + authenticated_user: AuthenticatedUser = Depends(get_current_user), generation_task_repository: Any = Depends(get_generation_task_repository), + project_repository: Any = Depends(get_project_repository), + asset_library_repository: Any = Depends(get_asset_library_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> GenerationTaskResponse: + require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository) + project = project_repository.find_by_id(request.project_id) + if project is None or project.workspace_id != request.workspace_id: + raise HTTPException(status_code=404, detail=f"Project {request.project_id} not found") + library = asset_library_repository.get(request.asset_library_id) + if library is None or library.workspace_id != request.workspace_id or library.project_id != request.project_id: + raise HTTPException(status_code=404, detail=f"AssetLibrary {request.asset_library_id} not found") + use_case = CreateGenerationTaskUseCase(generation_task_repository) task = use_case.execute( CreateGenerationTaskCommand( @@ -38,70 +88,40 @@ def create_generation_task( asset_library_id=request.asset_library_id, strategy_id=request.strategy_id, voice_library_id=request.voice_library_id, - created_by_user_id=request.created_by_user_id, + created_by_user_id=authenticated_user.user.id, ) ) celery_app.send_task("worker.generate_video", args=[task.id]) - return GenerationTaskResponse( - id=task.id, - workspace_id=task.workspace_id, - project_id=task.project_id, - asset_library_id=task.asset_library_id, - strategy_id=task.strategy_id, - voice_library_id=task.voice_library_id, - status=task.status.value, - progress=task.progress, - result_count=task.result_count, - error_message=task.error_message, - ) + return _to_generation_task_response(task) @router.get("/tasks/{task_id}", response_model=GenerationTaskResponse) def get_generation_task( task_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), generation_task_repository: Any = Depends(get_generation_task_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> GenerationTaskResponse: use_case = GetGenerationTaskUseCase(generation_task_repository) task = use_case.execute(task_id) if task is None: raise HTTPException(status_code=404, detail=f"GenerationTask {task_id} not found") - return GenerationTaskResponse( - id=task.id, - workspace_id=task.workspace_id, - project_id=task.project_id, - asset_library_id=task.asset_library_id, - strategy_id=task.strategy_id, - voice_library_id=task.voice_library_id, - status=task.status.value, - progress=task.progress, - result_count=task.result_count, - error_message=task.error_message, - ) + require_workspace_member(task.workspace_id, authenticated_user, workspace_member_repository) + return _to_generation_task_response(task) @router.get("/tasks/{task_id}/results", response_model=ListGeneratedVideosResponse) def list_generation_results( task_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), + generation_task_repository: Any = Depends(get_generation_task_repository), generated_video_repository: Any = Depends(get_generated_video_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ListGeneratedVideosResponse: + task = generation_task_repository.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail=f"GenerationTask {task_id} not found") + require_workspace_member(task.workspace_id, authenticated_user, workspace_member_repository) use_case = ListGeneratedVideosByTaskUseCase(generated_video_repository) items = use_case.execute(task_id) - return ListGeneratedVideosResponse( - items=[ - GeneratedVideoResponse( - id=item.id, - workspace_id=item.workspace_id, - project_id=item.project_id, - generation_task_id=item.generation_task_id, - name=item.name, - file_url=item.file_url, - file_size=item.file_size, - duration=item.duration, - thumbnail_url=item.thumbnail_url, - width=item.width, - height=item.height, - fps=item.fps, - ) - for item in items - ] - ) + return ListGeneratedVideosResponse(items=[_to_generated_video_response(item) for item in items]) diff --git a/apps/api/app/api/routes/permissions.py b/apps/api/app/api/routes/permissions.py new file mode 100644 index 000000000..4a16d4d95 --- /dev/null +++ b/apps/api/app/api/routes/permissions.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from app.auth import AuthenticatedUser +from fastapi import HTTPException + +from packages.ports.workspace_member_repository import WorkspaceMemberRepository + + +def require_workspace_member( + workspace_id: str, + authenticated_user: AuthenticatedUser, + workspace_member_repository: WorkspaceMemberRepository, +) -> None: + member = workspace_member_repository.find_by_workspace_and_user(workspace_id, authenticated_user.user.id) + if member is None: + raise HTTPException(status_code=403, detail="Workspace access denied")