fix(P1): 成片库越权漏洞修复-全链路加user_id校验 #573
@@ -93,7 +93,7 @@ def get_video(
|
||||
):
|
||||
"""获取单个成片详情。"""
|
||||
use_case = GetGeneratedVideoUseCase(repo)
|
||||
item = use_case.execute(video_id)
|
||||
item = use_case.execute(video_id, user_id=current_user.user.id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="Video not found")
|
||||
return _to_video_response(item, storage)
|
||||
@@ -109,7 +109,7 @@ def update_video_review_status(
|
||||
):
|
||||
"""更新成片复核状态:pending_review / approved / rejected。"""
|
||||
use_case = UpdateVideoReviewStatusUseCase(repo)
|
||||
item = use_case.execute(video_id, request.review_status)
|
||||
item = use_case.execute(video_id, request.review_status, user_id=current_user.user.id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="Video not found")
|
||||
logger.info(
|
||||
@@ -127,7 +127,7 @@ def delete_video(
|
||||
):
|
||||
"""删除单个成片(硬删除)。"""
|
||||
use_case = GetGeneratedVideoUseCase(repo)
|
||||
item = use_case.execute(video_id)
|
||||
item = use_case.execute(video_id, user_id=current_user.user.id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="Video not found")
|
||||
|
||||
@@ -139,7 +139,7 @@ def delete_video(
|
||||
except Exception:
|
||||
logger.warning("删除 OSS 视频文件失败,跳过: video_id=%s", video_id)
|
||||
|
||||
repo.delete(video_id)
|
||||
repo.delete(video_id, user_id=current_user.user.id)
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ def batch_delete_videos(
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
):
|
||||
"""批量删除成片。"""
|
||||
videos = repo.get_by_ids(request.video_ids)
|
||||
videos = repo.get_by_ids(request.video_ids, user_id=current_user.user.id)
|
||||
existing_ids = {v.id for v in videos}
|
||||
failed_ids = [vid for vid in request.video_ids if vid not in existing_ids]
|
||||
failed_details = {vid: "Video not found" for vid in failed_ids}
|
||||
@@ -165,7 +165,7 @@ def batch_delete_videos(
|
||||
except Exception:
|
||||
logger.warning("批量删除 OSS 视频文件失败,跳过: video_id=%s", video.id)
|
||||
|
||||
success_count = repo.batch_delete(list(existing_ids))
|
||||
success_count = repo.batch_delete(list(existing_ids), user_id=current_user.user.id)
|
||||
return BatchOperationResponse(
|
||||
success_count=success_count,
|
||||
failed_ids=failed_ids,
|
||||
@@ -188,9 +188,9 @@ def batch_download_videos(
|
||||
if len(request.video_ids) > 50:
|
||||
raise HTTPException(status_code=400, detail="Maximum 50 videos per batch download")
|
||||
|
||||
# 校验视频都存在
|
||||
# 校验视频都存在且属于当前用户
|
||||
use_case = GetVideosByIdsUseCase(repo)
|
||||
videos = use_case.execute(request.video_ids)
|
||||
videos = use_case.execute(request.video_ids, user_id=current_user.user.id)
|
||||
if len(videos) != len(request.video_ids):
|
||||
raise HTTPException(status_code=404, detail="Some videos not found")
|
||||
|
||||
|
||||
@@ -37,8 +37,11 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
self.session.commit()
|
||||
return video
|
||||
|
||||
def get(self, video_id: str) -> GeneratedVideo | None:
|
||||
model = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id).first()
|
||||
def get(self, video_id: str, *, user_id: str | None = None) -> GeneratedVideo | None:
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id)
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
model = query.first()
|
||||
if model is None:
|
||||
return None
|
||||
return GeneratedVideo(
|
||||
@@ -135,9 +138,14 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
|
||||
return [self._to_domain(model) for model in models], total
|
||||
|
||||
def update_review_status(self, video_id: str, review_status: str) -> GeneratedVideo | None:
|
||||
def update_review_status(
|
||||
self, video_id: str, review_status: str, *, user_id: str | None = None
|
||||
) -> GeneratedVideo | None:
|
||||
"""更新成片复核状态。"""
|
||||
model = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id).first()
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id)
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
model = query.first()
|
||||
if model is None:
|
||||
return None
|
||||
model.review_status = review_status
|
||||
@@ -155,31 +163,36 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
def get_by_ids(self, video_ids: list[str]) -> list[GeneratedVideo]:
|
||||
def get_by_ids(self, video_ids: list[str], *, user_id: str | None = None) -> list[GeneratedVideo]:
|
||||
"""批量获取成片记录。"""
|
||||
if not video_ids:
|
||||
return []
|
||||
models = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id.in_(video_ids)).all()
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id.in_(video_ids))
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
models = query.all()
|
||||
return [self._to_domain(model) for model in models]
|
||||
|
||||
def delete(self, video_id: str) -> bool:
|
||||
def delete(self, video_id: str, *, user_id: str | None = None) -> bool:
|
||||
"""删除单个成片记录(硬删除)。"""
|
||||
model = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id).first()
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id)
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
model = query.first()
|
||||
if model is None:
|
||||
return False
|
||||
self.session.delete(model)
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
def batch_delete(self, video_ids: list[str]) -> int:
|
||||
def batch_delete(self, video_ids: list[str], *, user_id: str | None = None) -> int:
|
||||
"""批量删除成片记录,返回成功删除数量。"""
|
||||
if not video_ids:
|
||||
return 0
|
||||
result = (
|
||||
self.session.query(GeneratedVideoModel)
|
||||
.filter(GeneratedVideoModel.id.in_(video_ids))
|
||||
.delete(synchronize_session=False)
|
||||
)
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id.in_(video_ids))
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
result = query.delete(synchronize_session=False)
|
||||
self.session.commit()
|
||||
return result
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ class GetGeneratedVideoUseCase:
|
||||
def __init__(self, generated_video_repository: GeneratedVideoRepository):
|
||||
self.generated_video_repository = generated_video_repository
|
||||
|
||||
def execute(self, video_id: str) -> GeneratedVideo | None:
|
||||
return self.generated_video_repository.get(video_id)
|
||||
def execute(self, video_id: str, *, user_id: str | None = None) -> GeneratedVideo | None:
|
||||
return self.generated_video_repository.get(video_id, user_id=user_id)
|
||||
|
||||
|
||||
class ListGeneratedVideosByTaskUseCase:
|
||||
@@ -64,8 +64,8 @@ class GetGeneratedVideoDownloadUrlUseCase:
|
||||
def __init__(self, generated_video_repository: GeneratedVideoRepository):
|
||||
self.generated_video_repository = generated_video_repository
|
||||
|
||||
def execute(self, video_id: str) -> str | None:
|
||||
item = self.generated_video_repository.get(video_id)
|
||||
def execute(self, video_id: str, *, user_id: str | None = None) -> str | None:
|
||||
item = self.generated_video_repository.get(video_id, user_id=user_id)
|
||||
if item is None:
|
||||
return None
|
||||
return item.file_url
|
||||
@@ -75,17 +75,17 @@ class UpdateVideoReviewStatusUseCase:
|
||||
def __init__(self, generated_video_repository: GeneratedVideoRepository):
|
||||
self.generated_video_repository = generated_video_repository
|
||||
|
||||
def execute(self, video_id: str, review_status: str) -> GeneratedVideo | None:
|
||||
def execute(self, video_id: str, review_status: str, *, user_id: str | None = None) -> GeneratedVideo | None:
|
||||
if not video_id.strip():
|
||||
raise ValueError("video_id 不能为空")
|
||||
if review_status not in ("pending_review", "approved", "rejected"):
|
||||
raise ValueError(f"无效的 review_status: {review_status}")
|
||||
return self.generated_video_repository.update_review_status(video_id.strip(), review_status)
|
||||
return self.generated_video_repository.update_review_status(video_id.strip(), review_status, user_id=user_id)
|
||||
|
||||
|
||||
class GetVideosByIdsUseCase:
|
||||
def __init__(self, generated_video_repository: GeneratedVideoRepository):
|
||||
self.generated_video_repository = generated_video_repository
|
||||
|
||||
def execute(self, video_ids: list[str]) -> list[GeneratedVideo]:
|
||||
return self.generated_video_repository.get_by_ids(video_ids)
|
||||
def execute(self, video_ids: list[str], *, user_id: str | None = None) -> list[GeneratedVideo]:
|
||||
return self.generated_video_repository.get_by_ids(video_ids, user_id=user_id)
|
||||
|
||||
@@ -8,7 +8,7 @@ from packages.domain import GeneratedVideo
|
||||
class GeneratedVideoRepository(Protocol):
|
||||
def create(self, video: GeneratedVideo) -> GeneratedVideo: ...
|
||||
|
||||
def get(self, video_id: str) -> GeneratedVideo | None: ...
|
||||
def get(self, video_id: str, *, user_id: str | None = None) -> GeneratedVideo | None: ...
|
||||
|
||||
def list_by_project(self, project_id: str) -> list[GeneratedVideo]: ...
|
||||
|
||||
@@ -27,12 +27,14 @@ class GeneratedVideoRepository(Protocol):
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[GeneratedVideo], int]: ...
|
||||
|
||||
def update_review_status(self, video_id: str, review_status: str) -> GeneratedVideo | None: ...
|
||||
def update_review_status(
|
||||
self, video_id: str, review_status: str, *, user_id: str | None = None
|
||||
) -> GeneratedVideo | None: ...
|
||||
|
||||
def update_thumbnail(self, video_id: str, thumbnail_url: str) -> bool: ...
|
||||
|
||||
def get_by_ids(self, video_ids: list[str]) -> list[GeneratedVideo]: ...
|
||||
def get_by_ids(self, video_ids: list[str], *, user_id: str | None = None) -> list[GeneratedVideo]: ...
|
||||
|
||||
def delete(self, video_id: str) -> bool: ...
|
||||
def delete(self, video_id: str, *, user_id: str | None = None) -> bool: ...
|
||||
|
||||
def batch_delete(self, video_ids: list[str]) -> int: ...
|
||||
def batch_delete(self, video_ids: list[str], *, user_id: str | None = None) -> int: ...
|
||||
|
||||
@@ -211,7 +211,7 @@ class StubGeneratedVideoRepository:
|
||||
self._videos[video.id] = video
|
||||
return video
|
||||
|
||||
def get(self, video_id: str) -> GeneratedVideo | None:
|
||||
def get(self, video_id: str, **kwargs) -> GeneratedVideo | None:
|
||||
return self._videos.get(video_id)
|
||||
|
||||
def list_by_project(self, project_id: str) -> list[GeneratedVideo]:
|
||||
|
||||
@@ -35,7 +35,7 @@ class DummyGeneratedVideoRepository:
|
||||
self.items[video.id] = video
|
||||
return video
|
||||
|
||||
def get(self, video_id):
|
||||
def get(self, video_id, **kwargs):
|
||||
return self.items.get(video_id)
|
||||
|
||||
def list_by_project(self, project_id):
|
||||
|
||||
@@ -255,7 +255,7 @@ class TestGeneratedVideoUseCases:
|
||||
use_case = GetGeneratedVideoUseCase(repo)
|
||||
result = use_case.execute("v1")
|
||||
|
||||
repo.get.assert_called_once_with("v1")
|
||||
repo.get.assert_called_once_with("v1", user_id=None)
|
||||
assert result.id == "v1"
|
||||
|
||||
def test_list_by_task_use_case(self):
|
||||
|
||||
Reference in New Issue
Block a user