diff --git a/frigate/api/auth.py b/frigate/api/auth.py index e0a6ec924..3dccfbf14 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -986,15 +986,23 @@ async def require_camera_access( current_user = await get_current_user(request) if isinstance(current_user, JSONResponse): - return current_user + detail = "Authentication required" + try: + error_payload = json.loads(current_user.body) + detail = ( + error_payload.get("message") or error_payload.get("detail") or detail + ) + except Exception: + pass + + raise HTTPException(status_code=current_user.status_code, detail=detail) role = current_user["role"] all_camera_names = set(request.app.frigate_config.cameras.keys()) roles_dict = request.app.frigate_config.auth.roles allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names) - # Admin or full access bypasses - if role == "admin" or not roles_dict.get(role): + if role == "admin": return if camera_name not in allowed_cameras: @@ -1004,6 +1012,60 @@ async def require_camera_access( ) +def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]: + owner_cameras: set[str] = set() + + for camera_name, camera in request.app.frigate_config.cameras.items(): + if stream_name == camera_name: + owner_cameras.add(camera_name) + continue + + if stream_name in camera.live.streams.values(): + owner_cameras.add(camera_name) + + return owner_cameras + + +async def require_go2rtc_stream_access( + stream_name: Optional[str] = None, + request: Request = None, +): + """Dependency to enforce go2rtc stream access based on owning camera access.""" + if stream_name is None: + return + + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + detail = "Authentication required" + try: + error_payload = json.loads(current_user.body) + detail = ( + error_payload.get("message") or error_payload.get("detail") or detail + ) + except Exception: + pass + + raise HTTPException(status_code=current_user.status_code, detail=detail) + + role = current_user["role"] + all_camera_names = set(request.app.frigate_config.cameras.keys()) + roles_dict = request.app.frigate_config.auth.roles + allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names) + + if role == "admin": + return + + owner_cameras = _get_stream_owner_cameras(request, stream_name) + + if owner_cameras & set(allowed_cameras): + return + + raise HTTPException( + status_code=403, + detail=f"Access denied to camera '{stream_name}'. Allowed: {allowed_cameras}", + ) + + async def get_allowed_cameras_for_filter(request: Request): """Dependency to get allowed_cameras for filtering lists.""" current_user = await get_current_user(request) diff --git a/frigate/api/camera.py b/frigate/api/camera.py index 488ec1e1f..a94486d8c 100644 --- a/frigate/api/camera.py +++ b/frigate/api/camera.py @@ -17,7 +17,7 @@ from zeep.transports import AsyncTransport from frigate.api.auth import ( allow_any_authenticated, - require_camera_access, + require_go2rtc_stream_access, require_role, ) from frigate.api.defs.tags import Tags @@ -71,14 +71,27 @@ def go2rtc_streams(): @router.get( - "/go2rtc/streams/{camera_name}", dependencies=[Depends(require_camera_access)] + "/go2rtc/streams/{stream_name}", + dependencies=[Depends(require_go2rtc_stream_access)], ) -def go2rtc_camera_stream(request: Request, camera_name: str): +def go2rtc_camera_stream(request: Request, stream_name: str): r = requests.get( - f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=allµphone" + "http://127.0.0.1:1984/api/streams", + params={ + "src": stream_name, + "video": "all", + "audio": "all", + "microphone": "", + }, ) if not r.ok: - camera_config = request.app.frigate_config.cameras.get(camera_name) + camera_config = request.app.frigate_config.cameras.get(stream_name) + + if camera_config is None: + for camera_name, camera in request.app.frigate_config.cameras.items(): + if stream_name in camera.live.streams.values(): + camera_config = request.app.frigate_config.cameras.get(camera_name) + break if camera_config and camera_config.enabled: logger.error("Failed to fetch streams from go2rtc") diff --git a/web/src/hooks/use-camera-live-mode.ts b/web/src/hooks/use-camera-live-mode.ts index 288c0ea09..5264d1a34 100644 --- a/web/src/hooks/use-camera-live-mode.ts +++ b/web/src/hooks/use-camera-live-mode.ts @@ -18,18 +18,25 @@ export default function useCameraLiveMode( const streamNames = new Set(); cameras.forEach((camera) => { - const isRestreamed = Object.keys(config.go2rtc.streams || {}).includes( - Object.values(camera.live.streams)[0], - ); + if (activeStreams && activeStreams[camera.name]) { + const selectedStreamName = activeStreams[camera.name]; + const isRestreamed = Object.keys(config.go2rtc.streams || {}).includes( + selectedStreamName, + ); - if (isRestreamed) { - if (activeStreams && activeStreams[camera.name]) { - streamNames.add(activeStreams[camera.name]); - } else { - Object.values(camera.live.streams).forEach((streamName) => { - streamNames.add(streamName); - }); + if (isRestreamed) { + streamNames.add(selectedStreamName); } + } else { + Object.values(camera.live.streams).forEach((streamName) => { + const isRestreamed = Object.keys( + config.go2rtc.streams || {}, + ).includes(streamName); + + if (isRestreamed) { + streamNames.add(streamName); + } + }); } }); @@ -66,11 +73,11 @@ export default function useCameraLiveMode( } = {}; cameras.forEach((camera) => { + const selectedStreamName = + activeStreams?.[camera.name] ?? Object.values(camera.live.streams)[0]; const isRestreamed = config && - Object.keys(config.go2rtc.streams || {}).includes( - Object.values(camera.live.streams)[0], - ); + Object.keys(config.go2rtc.streams || {}).includes(selectedStreamName); newIsRestreamedStates[camera.name] = isRestreamed ?? false; @@ -101,14 +108,21 @@ export default function useCameraLiveMode( setPreferredLiveModes(newPreferredLiveModes); setIsRestreamedStates(newIsRestreamedStates); setSupportsAudioOutputStates(newSupportsAudioOutputStates); - }, [cameras, config, windowVisible, streamMetadata]); + }, [activeStreams, cameras, config, windowVisible, streamMetadata]); const resetPreferredLiveMode = useCallback( (cameraName: string) => { const mseSupported = "MediaSource" in window || "ManagedMediaSource" in window; + const cameraConfig = cameras.find((camera) => camera.name === cameraName); + const selectedStreamName = + activeStreams?.[cameraName] ?? + (cameraConfig + ? Object.values(cameraConfig.live.streams)[0] + : cameraName); const isRestreamed = - config && Object.keys(config.go2rtc.streams || {}).includes(cameraName); + config && + Object.keys(config.go2rtc.streams || {}).includes(selectedStreamName); setPreferredLiveModes((prevModes) => { const newModes = { ...prevModes }; @@ -122,7 +136,7 @@ export default function useCameraLiveMode( return newModes; }); }, - [config], + [activeStreams, cameras, config], ); return {