diff --git a/frigate/api/app.py b/frigate/api/app.py index d1120afee..d954a74f3 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -732,7 +732,12 @@ def get_recognized_license_plates( @router.get("/timeline", dependencies=[Depends(allow_any_authenticated())]) -def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None): +def timeline( + camera: str = "all", + limit: int = 100, + source_id: Optional[str] = None, + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): clauses = [] selected_columns = [ @@ -754,6 +759,9 @@ def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = N else: clauses.append((Timeline.source_id.in_(source_ids))) + # Enforce per-camera access control + clauses.append((Timeline.camera << allowed_cameras)) + if len(clauses) == 0: clauses.append((True)) @@ -769,7 +777,10 @@ def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = N @router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())]) -def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()): +def hourly_timeline( + params: AppTimelineHourlyQueryParameters = Depends(), + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): """Get hourly summary for timeline.""" cameras = params.cameras labels = params.labels @@ -787,6 +798,9 @@ def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()): camera_list = cameras.split(",") clauses.append((Timeline.camera << camera_list)) + # Enforce per-camera access control + clauses.append((Timeline.camera << allowed_cameras)) + if labels != "all": label_list = labels.split(",") clauses.append((Timeline.data["label"] << label_list)) diff --git a/frigate/api/media.py b/frigate/api/media.py index bfce8b10c..03aa8ea5b 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -1142,7 +1142,6 @@ async def event_snapshot( @router.get( "/events/{event_id}/thumbnail.{extension}", - dependencies=[Depends(require_camera_access)], ) async def event_thumbnail( request: Request, @@ -1344,12 +1343,12 @@ def grid_snapshot( @router.get( "/events/{event_id}/snapshot-clean.webp", - dependencies=[Depends(require_camera_access)], ) -def event_snapshot_clean(request: Request, event_id: str, download: bool = False): +async def event_snapshot_clean(request: Request, event_id: str, download: bool = False): webp_bytes = None try: event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) snapshot_config = request.app.frigate_config.cameras[event.camera].snapshots if not (snapshot_config.enabled and event.has_snapshot): return JSONResponse( @@ -1470,7 +1469,7 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False @router.get( - "/events/{event_id}/clip.mp4", dependencies=[Depends(require_camera_access)] + "/events/{event_id}/clip.mp4", ) async def event_clip( request: Request, @@ -1484,6 +1483,8 @@ async def event_clip( content={"success": False, "message": "Event not found"}, status_code=404 ) + await require_camera_access(event.camera, request=request) + if not event.has_clip: return JSONResponse( content={"success": False, "message": "Clip not available"}, status_code=404 @@ -1500,9 +1501,9 @@ async def event_clip( @router.get( - "/events/{event_id}/preview.gif", dependencies=[Depends(require_camera_access)] + "/events/{event_id}/preview.gif", ) -def event_preview(request: Request, event_id: str): +async def event_preview(request: Request, event_id: str): try: event: Event = Event.get(Event.id == event_id) except DoesNotExist: @@ -1510,6 +1511,8 @@ def event_preview(request: Request, event_id: str): content={"success": False, "message": "Event not found"}, status_code=404 ) + await require_camera_access(event.camera, request=request) + start_ts = event.start_time end_ts = start_ts + ( min(event.end_time - event.start_time, 20) if event.end_time else 20 @@ -1854,8 +1857,8 @@ def preview_mp4( ) -@router.get("/review/{event_id}/preview", dependencies=[Depends(require_camera_access)]) -def review_preview( +@router.get("/review/{event_id}/preview") +async def review_preview( request: Request, event_id: str, format: str = Query(default="gif", enum=["gif", "mp4"]), @@ -1868,6 +1871,8 @@ def review_preview( status_code=404, ) + await require_camera_access(review.camera, request=request) + padding = 8 start_ts = review.start_time - padding end_ts = ( @@ -1881,10 +1886,12 @@ def review_preview( @router.get( - "/preview/{file_name}/thumbnail.jpg", dependencies=[Depends(require_camera_access)] + "/preview/{file_name}/thumbnail.jpg", + dependencies=[Depends(allow_any_authenticated())], ) @router.get( - "/preview/{file_name}/thumbnail.webp", dependencies=[Depends(require_camera_access)] + "/preview/{file_name}/thumbnail.webp", + dependencies=[Depends(allow_any_authenticated())], ) def preview_thumbnail(file_name: str): """Get a thumbnail from the cached preview frames."""