Fix go2rtc stream alias authorization and live audio gating for main/sub stream names

This commit is contained in:
Josh Hawkins 2026-02-23 09:25:02 -06:00
parent 4d51f7a1bb
commit 8e35a6cd92
3 changed files with 113 additions and 24 deletions

View File

@ -986,15 +986,23 @@ async def require_camera_access(
current_user = await get_current_user(request) current_user = await get_current_user(request)
if isinstance(current_user, JSONResponse): 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"] role = current_user["role"]
all_camera_names = set(request.app.frigate_config.cameras.keys()) all_camera_names = set(request.app.frigate_config.cameras.keys())
roles_dict = request.app.frigate_config.auth.roles roles_dict = request.app.frigate_config.auth.roles
allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names) allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names)
# Admin or full access bypasses if role == "admin":
if role == "admin" or not roles_dict.get(role):
return return
if camera_name not in allowed_cameras: 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): async def get_allowed_cameras_for_filter(request: Request):
"""Dependency to get allowed_cameras for filtering lists.""" """Dependency to get allowed_cameras for filtering lists."""
current_user = await get_current_user(request) current_user = await get_current_user(request)

View File

@ -17,7 +17,7 @@ from zeep.transports import AsyncTransport
from frigate.api.auth import ( from frigate.api.auth import (
allow_any_authenticated, allow_any_authenticated,
require_camera_access, require_go2rtc_stream_access,
require_role, require_role,
) )
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
@ -71,14 +71,27 @@ def go2rtc_streams():
@router.get( @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( r = requests.get(
f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=all&microphone" "http://127.0.0.1:1984/api/streams",
params={
"src": stream_name,
"video": "all",
"audio": "all",
"microphone": "",
},
) )
if not r.ok: 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: if camera_config and camera_config.enabled:
logger.error("Failed to fetch streams from go2rtc") logger.error("Failed to fetch streams from go2rtc")

View File

@ -18,18 +18,25 @@ export default function useCameraLiveMode(
const streamNames = new Set<string>(); const streamNames = new Set<string>();
cameras.forEach((camera) => { cameras.forEach((camera) => {
const isRestreamed = Object.keys(config.go2rtc.streams || {}).includes( if (activeStreams && activeStreams[camera.name]) {
Object.values(camera.live.streams)[0], const selectedStreamName = activeStreams[camera.name];
); const isRestreamed = Object.keys(config.go2rtc.streams || {}).includes(
selectedStreamName,
);
if (isRestreamed) { if (isRestreamed) {
if (activeStreams && activeStreams[camera.name]) { streamNames.add(selectedStreamName);
streamNames.add(activeStreams[camera.name]);
} else {
Object.values(camera.live.streams).forEach((streamName) => {
streamNames.add(streamName);
});
} }
} 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) => { cameras.forEach((camera) => {
const selectedStreamName =
activeStreams?.[camera.name] ?? Object.values(camera.live.streams)[0];
const isRestreamed = const isRestreamed =
config && config &&
Object.keys(config.go2rtc.streams || {}).includes( Object.keys(config.go2rtc.streams || {}).includes(selectedStreamName);
Object.values(camera.live.streams)[0],
);
newIsRestreamedStates[camera.name] = isRestreamed ?? false; newIsRestreamedStates[camera.name] = isRestreamed ?? false;
@ -101,14 +108,21 @@ export default function useCameraLiveMode(
setPreferredLiveModes(newPreferredLiveModes); setPreferredLiveModes(newPreferredLiveModes);
setIsRestreamedStates(newIsRestreamedStates); setIsRestreamedStates(newIsRestreamedStates);
setSupportsAudioOutputStates(newSupportsAudioOutputStates); setSupportsAudioOutputStates(newSupportsAudioOutputStates);
}, [cameras, config, windowVisible, streamMetadata]); }, [activeStreams, cameras, config, windowVisible, streamMetadata]);
const resetPreferredLiveMode = useCallback( const resetPreferredLiveMode = useCallback(
(cameraName: string) => { (cameraName: string) => {
const mseSupported = const mseSupported =
"MediaSource" in window || "ManagedMediaSource" in window; "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 = const isRestreamed =
config && Object.keys(config.go2rtc.streams || {}).includes(cameraName); config &&
Object.keys(config.go2rtc.streams || {}).includes(selectedStreamName);
setPreferredLiveModes((prevModes) => { setPreferredLiveModes((prevModes) => {
const newModes = { ...prevModes }; const newModes = { ...prevModes };
@ -122,7 +136,7 @@ export default function useCameraLiveMode(
return newModes; return newModes;
}); });
}, },
[config], [activeStreams, cameras, config],
); );
return { return {