From aec3e4511f5127a8e7852c01a3e2eafc100272a7 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 26 Nov 2025 08:00:54 -0600 Subject: [PATCH] update all endpoint access guards --- frigate/api/app.py | 46 ++++++++++++++++++++++++++----------------- frigate/api/auth.py | 4 +++- frigate/api/camera.py | 14 +++++++++---- frigate/api/event.py | 3 ++- frigate/api/media.py | 44 ++++++++++++++++++++++++++++++----------- frigate/api/review.py | 43 +++++++++++++++++++++++++++++++++------- 6 files changed, 112 insertions(+), 42 deletions(-) diff --git a/frigate/api/app.py b/frigate/api/app.py index 3ef054fc0..cded3428b 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -23,7 +23,7 @@ from markupsafe import escape from peewee import SQL, fn, operator from pydantic import ValidationError -from frigate.api.auth import require_role +from frigate.api.auth import allow_any_authenticated, allow_public, require_role from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters from frigate.api.defs.request.app_body import AppConfigSetBody from frigate.api.defs.tags import Tags @@ -56,29 +56,33 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.app]) -@router.get("/", response_class=PlainTextResponse) +@router.get( + "/", response_class=PlainTextResponse, dependencies=[Depends(allow_public())] +) def is_healthy(): return "Frigate is running. Alive and healthy!" -@router.get("/config/schema.json") +@router.get("/config/schema.json", dependencies=[Depends(allow_public())]) def config_schema(request: Request): return Response( content=request.app.frigate_config.schema_json(), media_type="application/json" ) -@router.get("/version", response_class=PlainTextResponse) +@router.get( + "/version", response_class=PlainTextResponse, dependencies=[Depends(allow_public())] +) def version(): return VERSION -@router.get("/stats") +@router.get("/stats", dependencies=[Depends(allow_any_authenticated())]) def stats(request: Request): return JSONResponse(content=request.app.stats_emitter.get_latest_stats()) -@router.get("/stats/history") +@router.get("/stats/history", dependencies=[Depends(allow_any_authenticated())]) def stats_history(request: Request, keys: str = None): if keys: keys = keys.split(",") @@ -86,7 +90,7 @@ def stats_history(request: Request, keys: str = None): return JSONResponse(content=request.app.stats_emitter.get_stats_history(keys)) -@router.get("/metrics") +@router.get("/metrics", dependencies=[Depends(allow_public())]) def metrics(request: Request): """Expose Prometheus metrics endpoint and update metrics with latest stats""" # Retrieve the latest statistics and update the Prometheus metrics @@ -103,7 +107,7 @@ def metrics(request: Request): return Response(content=content, media_type=content_type) -@router.get("/config") +@router.get("/config", dependencies=[Depends(allow_any_authenticated())]) def config(request: Request): config_obj: FrigateConfig = request.app.frigate_config config: dict[str, dict[str, Any]] = config_obj.model_dump( @@ -209,7 +213,7 @@ def config_raw_paths(request: Request): return JSONResponse(content=raw_paths) -@router.get("/config/raw") +@router.get("/config/raw", dependencies=[Depends(allow_any_authenticated())]) def config_raw(): config_file = find_config_file() @@ -452,7 +456,7 @@ def config_set(request: Request, body: AppConfigSetBody): ) -@router.get("/vainfo") +@router.get("/vainfo", dependencies=[Depends(allow_any_authenticated())]) def vainfo(): vainfo = vainfo_hwaccel() return JSONResponse( @@ -472,12 +476,16 @@ def vainfo(): ) -@router.get("/nvinfo") +@router.get("/nvinfo", dependencies=[Depends(allow_any_authenticated())]) def nvinfo(): return JSONResponse(content=get_nvidia_driver_info()) -@router.get("/logs/{service}", tags=[Tags.logs]) +@router.get( + "/logs/{service}", + tags=[Tags.logs], + dependencies=[Depends(allow_any_authenticated())], +) async def logs( service: str = Path(enum=["frigate", "nginx", "go2rtc"]), download: Optional[str] = None, @@ -585,7 +593,7 @@ def restart(): ) -@router.get("/labels") +@router.get("/labels", dependencies=[Depends(allow_any_authenticated())]) def get_labels(camera: str = ""): try: if camera: @@ -603,7 +611,7 @@ def get_labels(camera: str = ""): return JSONResponse(content=labels) -@router.get("/sub_labels") +@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())]) def get_sub_labels(split_joined: Optional[int] = None): try: events = Event.select(Event.sub_label).distinct() @@ -634,7 +642,7 @@ def get_sub_labels(split_joined: Optional[int] = None): return JSONResponse(content=sub_labels) -@router.get("/plus/models") +@router.get("/plus/models", dependencies=[Depends(allow_any_authenticated())]) def plusModels(request: Request, filterByCurrentModelDetector: bool = False): if not request.app.frigate_config.plus_api.is_active(): return JSONResponse( @@ -676,7 +684,9 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False): return JSONResponse(content=validModels) -@router.get("/recognized_license_plates") +@router.get( + "/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())] +) def get_recognized_license_plates(split_joined: Optional[int] = None): try: query = ( @@ -710,7 +720,7 @@ def get_recognized_license_plates(split_joined: Optional[int] = None): return JSONResponse(content=recognized_license_plates) -@router.get("/timeline") +@router.get("/timeline", dependencies=[Depends(allow_any_authenticated())]) def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None): clauses = [] @@ -747,7 +757,7 @@ def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = N return JSONResponse(content=[t for t in timeline]) -@router.get("/timeline/hourly") +@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())]) def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()): """Get hourly summary for timeline.""" cameras = params.cameras diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 7aefdb355..08055f488 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -677,7 +677,9 @@ def delete_user(request: Request, username: str): return JSONResponse(content={"success": True}) -@router.put("/users/{username}/password") +@router.put( + "/users/{username}/password", dependencies=[Depends(allow_any_authenticated())] +) async def update_password( request: Request, username: str, diff --git a/frigate/api/camera.py b/frigate/api/camera.py index ef55a283e..936a0bb09 100644 --- a/frigate/api/camera.py +++ b/frigate/api/camera.py @@ -15,7 +15,11 @@ from onvif import ONVIFCamera, ONVIFError from zeep.exceptions import Fault, TransportError from zeep.transports import AsyncTransport -from frigate.api.auth import require_role +from frigate.api.auth import ( + allow_any_authenticated, + require_camera_access, + require_role, +) from frigate.api.defs.tags import Tags from frigate.config.config import FrigateConfig from frigate.util.builtin import clean_camera_user_pass @@ -50,7 +54,7 @@ def _is_valid_host(host: str) -> bool: return False -@router.get("/go2rtc/streams") +@router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())]) def go2rtc_streams(): r = requests.get("http://127.0.0.1:1984/api/streams") if not r.ok: @@ -66,7 +70,9 @@ def go2rtc_streams(): return JSONResponse(content=stream_data) -@router.get("/go2rtc/streams/{camera_name}") +@router.get( + "/go2rtc/streams/{camera_name}", dependencies=[Depends(require_camera_access)] +) def go2rtc_camera_stream(request: Request, camera_name: str): r = requests.get( f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=allµphone" @@ -161,7 +167,7 @@ def go2rtc_delete_stream(stream_name: str): ) -@router.get("/ffprobe") +@router.get("/ffprobe", dependencies=[Depends(require_role(["admin"]))]) def ffprobe(request: Request, paths: str = "", detailed: bool = False): path_param = paths diff --git a/frigate/api/event.py b/frigate/api/event.py index c084a8971..e85758181 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -22,6 +22,7 @@ from peewee import JOIN, DoesNotExist, fn, operator from playhouse.shortcuts import model_to_dict from frigate.api.auth import ( + allow_any_authenticated, get_allowed_cameras_for_filter, require_camera_access, require_role, @@ -808,7 +809,7 @@ def events_search( return JSONResponse(content=processed_events) -@router.get("/events/summary") +@router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())]) def events_summary( params: EventsSummaryQueryParams = Depends(), allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), diff --git a/frigate/api/media.py b/frigate/api/media.py index 372404b5a..4dc5c7714 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -22,7 +22,11 @@ from pathvalidate import sanitize_filename from peewee import DoesNotExist, fn, operator from tzlocal import get_localzone_name -from frigate.api.auth import get_allowed_cameras_for_filter, require_camera_access +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, +) from frigate.api.defs.query.media_query_parameters import ( Extension, MediaEventsSnapshotQueryParams, @@ -393,7 +397,7 @@ async def submit_recording_snapshot_to_plus( ) -@router.get("/recordings/storage") +@router.get("/recordings/storage", dependencies=[Depends(allow_any_authenticated())]) def get_recordings_storage_usage(request: Request): recording_stats = request.app.stats_emitter.get_latest_stats()["service"][ "storage" @@ -417,7 +421,7 @@ def get_recordings_storage_usage(request: Request): return JSONResponse(content=camera_usages) -@router.get("/recordings/summary") +@router.get("/recordings/summary", dependencies=[Depends(allow_any_authenticated())]) def all_recordings_summary( request: Request, params: MediaRecordingsSummaryQueryParams = Depends(), @@ -635,7 +639,11 @@ async def recordings( return JSONResponse(content=list(recordings)) -@router.get("/recordings/unavailable", response_model=list[dict]) +@router.get( + "/recordings/unavailable", + response_model=list[dict], + dependencies=[Depends(allow_any_authenticated())], +) async def no_recordings( request: Request, params: MediaRecordingsAvailabilityQueryParams = Depends(), @@ -1053,7 +1061,10 @@ async def event_snapshot( ) -@router.get("/events/{event_id}/thumbnail.{extension}") +@router.get( + "/events/{event_id}/thumbnail.{extension}", + dependencies=[Depends(require_camera_access)], +) async def event_thumbnail( request: Request, event_id: str, @@ -1251,7 +1262,10 @@ def grid_snapshot( ) -@router.get("/events/{event_id}/snapshot-clean.webp") +@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): webp_bytes = None try: @@ -1375,7 +1389,9 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False ) -@router.get("/events/{event_id}/clip.mp4") +@router.get( + "/events/{event_id}/clip.mp4", dependencies=[Depends(require_camera_access)] +) async def event_clip( request: Request, event_id: str, @@ -1403,7 +1419,9 @@ async def event_clip( ) -@router.get("/events/{event_id}/preview.gif") +@router.get( + "/events/{event_id}/preview.gif", dependencies=[Depends(require_camera_access)] +) def event_preview(request: Request, event_id: str): try: event: Event = Event.get(Event.id == event_id) @@ -1756,7 +1774,7 @@ def preview_mp4( ) -@router.get("/review/{event_id}/preview") +@router.get("/review/{event_id}/preview", dependencies=[Depends(require_camera_access)]) def review_preview( request: Request, event_id: str, @@ -1782,8 +1800,12 @@ def review_preview( return preview_mp4(request, review.camera, start_ts, end_ts) -@router.get("/preview/{file_name}/thumbnail.jpg") -@router.get("/preview/{file_name}/thumbnail.webp") +@router.get( + "/preview/{file_name}/thumbnail.jpg", dependencies=[Depends(require_camera_access)] +) +@router.get( + "/preview/{file_name}/thumbnail.webp", dependencies=[Depends(require_camera_access)] +) def preview_thumbnail(file_name: str): """Get a thumbnail from the cached preview frames.""" if len(file_name) > 1000: diff --git a/frigate/api/review.py b/frigate/api/review.py index 300255663..b19f10f2b 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -14,6 +14,7 @@ from peewee import Case, DoesNotExist, IntegrityError, fn, operator from playhouse.shortcuts import model_to_dict from frigate.api.auth import ( + allow_any_authenticated, get_allowed_cameras_for_filter, get_current_user, require_camera_access, @@ -43,7 +44,11 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.review]) -@router.get("/review", response_model=list[ReviewSegmentResponse]) +@router.get( + "/review", + response_model=list[ReviewSegmentResponse], + dependencies=[Depends(allow_any_authenticated())], +) async def review( params: ReviewQueryParams = Depends(), current_user: dict = Depends(get_current_user), @@ -152,7 +157,11 @@ async def review( return JSONResponse(content=[r for r in review_query]) -@router.get("/review_ids", response_model=list[ReviewSegmentResponse]) +@router.get( + "/review_ids", + response_model=list[ReviewSegmentResponse], + dependencies=[Depends(allow_any_authenticated())], +) async def review_ids(request: Request, ids: str): ids = ids.split(",") @@ -186,7 +195,11 @@ async def review_ids(request: Request, ids: str): ) -@router.get("/review/summary", response_model=ReviewSummaryResponse) +@router.get( + "/review/summary", + response_model=ReviewSummaryResponse, + dependencies=[Depends(allow_any_authenticated())], +) async def review_summary( params: ReviewSummaryQueryParams = Depends(), current_user: dict = Depends(get_current_user), @@ -461,7 +474,11 @@ async def review_summary( return JSONResponse(content=data) -@router.post("/reviews/viewed", response_model=GenericResponse) +@router.post( + "/reviews/viewed", + response_model=GenericResponse, + dependencies=[Depends(allow_any_authenticated())], +) async def set_multiple_reviewed( request: Request, body: ReviewModifyMultipleBody, @@ -644,7 +661,11 @@ def motion_activity( return JSONResponse(content=normalized) -@router.get("/review/event/{event_id}", response_model=ReviewSegmentResponse) +@router.get( + "/review/event/{event_id}", + response_model=ReviewSegmentResponse, + dependencies=[Depends(allow_any_authenticated())], +) async def get_review_from_event(request: Request, event_id: str): try: review = ReviewSegment.get( @@ -659,7 +680,11 @@ async def get_review_from_event(request: Request, event_id: str): ) -@router.get("/review/{review_id}", response_model=ReviewSegmentResponse) +@router.get( + "/review/{review_id}", + response_model=ReviewSegmentResponse, + dependencies=[Depends(allow_any_authenticated())], +) async def get_review(request: Request, review_id: str): try: review = ReviewSegment.get(ReviewSegment.id == review_id) @@ -672,7 +697,11 @@ async def get_review(request: Request, review_id: str): ) -@router.delete("/review/{review_id}/viewed", response_model=GenericResponse) +@router.delete( + "/review/{review_id}/viewed", + response_model=GenericResponse, + dependencies=[Depends(allow_any_authenticated())], +) async def set_not_reviewed( review_id: str, current_user: dict = Depends(get_current_user),