From cb2e20ffd40bbf7cda48789e2d61da6da6e5482b Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Mon, 22 Jun 2026 19:37:57 +0800 Subject: [PATCH] fix(api): enforce workspace access on project media routes --- apps/api/app/api/routes/asset_libraries.py | 31 +++++++++++++++++-- apps/api/app/api/routes/projects.py | 23 +++++++++++++- apps/api/app/api/routes/upload.py | 36 ++++++++++++++++++++-- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/apps/api/app/api/routes/asset_libraries.py b/apps/api/app/api/routes/asset_libraries.py index d6e6e46eb..c3465c09d 100644 --- a/apps/api/app/api/routes/asset_libraries.py +++ b/apps/api/app/api/routes/asset_libraries.py @@ -1,23 +1,36 @@ from typing import Any -from app.dependencies import get_asset_library_repository +from app.auth import AuthenticatedUser, get_current_user +from app.dependencies import get_asset_library_repository, get_project_repository, get_workspace_member_repository from app.schemas.asset_library import ( AssetLibraryResponse, CreateAssetLibraryRequest, ListAssetLibrariesResponse, ) -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException, status from packages.application import ( CreateAssetLibraryCommand, CreateAssetLibraryUseCase, + GetProjectUseCase, ListAssetLibrariesUseCase, ) from packages.domain import AssetLibraryKind +from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() +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=status.HTTP_403_FORBIDDEN, detail="Workspace access denied") + + def _to_asset_library_response(item) -> AssetLibraryResponse: return AssetLibraryResponse( id=item.id, @@ -34,8 +47,15 @@ def _to_asset_library_response(item) -> AssetLibraryResponse: def list_asset_libraries( project_id: str, kind: str | None = None, + authenticated_user: AuthenticatedUser = Depends(get_current_user), 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), ) -> ListAssetLibrariesResponse: + project = GetProjectUseCase(project_repository).execute(project_id) + if project is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + _require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository) use_case = ListAssetLibrariesUseCase(asset_library_repository) parsed_kind = AssetLibraryKind(kind) if kind else None items = use_case.execute(project_id, kind=parsed_kind) @@ -45,8 +65,15 @@ def list_asset_libraries( @router.post("", response_model=AssetLibraryResponse) def create_asset_library( request: CreateAssetLibraryRequest, + authenticated_user: AuthenticatedUser = Depends(get_current_user), 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), ) -> AssetLibraryResponse: + project = GetProjectUseCase(project_repository).execute(request.project_id) + if project is None or project.workspace_id != request.workspace_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + _require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository) use_case = CreateAssetLibraryUseCase(asset_library_repository) item = use_case.execute( CreateAssetLibraryCommand( diff --git a/apps/api/app/api/routes/projects.py b/apps/api/app/api/routes/projects.py index e84785c94..5939c53ee 100644 --- a/apps/api/app/api/routes/projects.py +++ b/apps/api/app/api/routes/projects.py @@ -1,6 +1,7 @@ from typing import Any -from app.dependencies import get_project_repository +from app.auth import AuthenticatedUser, get_current_user +from app.dependencies import get_project_repository, get_workspace_member_repository from app.schemas.project import ( CreateProjectRequest, ListProjectsResponse, @@ -14,10 +15,21 @@ from packages.application import ( GetProjectUseCase, ListProjectsUseCase, ) +from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() +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=status.HTTP_403_FORBIDDEN, detail="Workspace access denied") + + def _to_project_response(item) -> ProjectResponse: return ProjectResponse( id=item.id, @@ -30,20 +42,26 @@ def _to_project_response(item) -> ProjectResponse: @router.get("/{project_id}", response_model=ProjectResponse) def get_project( project_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ProjectResponse: use_case = GetProjectUseCase(project_repository) project = use_case.execute(project_id) if project is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + _require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository) return _to_project_response(project) @router.get("", response_model=ListProjectsResponse) def list_projects( workspace_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ListProjectsResponse: + _require_workspace_member(workspace_id, authenticated_user, workspace_member_repository) use_case = ListProjectsUseCase(project_repository) projects = use_case.execute(workspace_id) return ListProjectsResponse(items=[_to_project_response(item) for item in projects]) @@ -52,8 +70,11 @@ def list_projects( @router.post("", response_model=ProjectResponse) def create_project( request: CreateProjectRequest, + authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), + workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ProjectResponse: + _require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository) use_case = CreateProjectUseCase(project_repository) project = use_case.execute( CreateProjectCommand( diff --git a/apps/api/app/api/routes/upload.py b/apps/api/app/api/routes/upload.py index 663a90486..737605d2c 100644 --- a/apps/api/app/api/routes/upload.py +++ b/apps/api/app/api/routes/upload.py @@ -1,27 +1,57 @@ from typing import Any from uuid import uuid4 +from app.auth import AuthenticatedUser, get_current_user from app.core.celery_app import celery_app from app.core.storage import OSSStorageService, get_storage_service -from app.dependencies import get_ingest_job_repository +from app.dependencies import ( + get_asset_library_repository, + get_ingest_job_repository, + get_project_repository, + get_workspace_member_repository, +) from app.schemas.upload import UploadAssetResponse -from fastapi import APIRouter, Depends, File, Form, UploadFile +from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status -from packages.application import SubmitIngestJobCommand, SubmitIngestJobUseCase +from packages.application import GetProjectUseCase, SubmitIngestJobCommand, SubmitIngestJobUseCase +from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() +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=status.HTTP_403_FORBIDDEN, detail="Workspace access denied") + + @router.post("", response_model=UploadAssetResponse) async def upload_asset( file: UploadFile = File(..., description="要上传的文件(视频、音频、图片等)"), workspace_id: str = Form(..., description="工作空间 ID"), project_id: str = Form(..., description="项目 ID"), library_id: str = Form(..., description="素材库 ID"), + authenticated_user: AuthenticatedUser = Depends(get_current_user), ingest_job_repository: Any = Depends(get_ingest_job_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), storage_service: OSSStorageService = Depends(get_storage_service), ) -> UploadAssetResponse: """上传素材文件并触发导入流水线。""" + project = GetProjectUseCase(project_repository).execute(project_id) + if project is None or project.workspace_id != workspace_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + _require_workspace_member(workspace_id, authenticated_user, workspace_member_repository) + + libraries = asset_library_repository.list_by_project(project_id) + if not any(item.id == library_id and item.workspace_id == workspace_id for item in libraries): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset library not found") + file_id = uuid4().hex[:8] storage_key = f"uploads/{file_id}/{file.filename}"