fix(api): enforce workspace access on project media routes

This commit is contained in:
Xiaoxia AI
2026-06-22 19:37:57 +08:00
parent fe7a8ee6fb
commit ed77d736ec
3 changed files with 84 additions and 6 deletions
+29 -2
View File
@@ -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(
+22 -1
View File
@@ -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(
+33 -3
View File
@@ -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}"