blakeblackshear.frigate/frigate/test/http_api/test_http_camera_access.py
Josh Hawkins ed1e3a7c9a
Enhance user roles to limit camera access (#20024)
* update config for roles and add validator

* ensure admin and viewer are never overridden

* add class method to user to retrieve all allowed cameras

* enforce config roles in auth api endpoints

* add camera access api dependency functions

* protect review endpoints

* protect preview endpoints

* rename param name for better fastapi injection matching

* remove unneeded

* protect export endpoints

* protect event endpoints

* protect media endpoints

* update auth hook for allowed cameras

* update default app view

* ensure anonymous user always returns all cameras

* limit cameras in explore

* cameras is already a list

* limit cameras in review/history

* limit cameras in live view

* limit cameras in camera groups

* only show face library and classification in sidebar for admin

* remove check in delete reviews

since admin role is required, no need to check camera access. fixes failing test

* pass request with camera access for tests

* more async

* camera access tests

* fix proxy auth tests

* allowed cameras for review tests

* combine event tests and refactor for camera access

* fix post validation for roles

* don't limit roles in create user dialog

* fix triggers endpoints

no need to run require camera access dep since the required role is admin

* fix type

* create and edit role dialogs

* delete role dialog

* fix role change dialog

* update settings view for roles

* i18n changes

* minor spacing tweaks

* docs

* use badges and camera name label component

* clarify docs

* display all cameras badge for admin and viewer

* i18n fix

* use validator to prevent reserved and empty roles from being assigned

* split users and roles into separate tabs in settings

* tweak docs

* clarify docs

* change icon

* don't memoize roles

always recalculate on component render
2025-09-12 05:19:29 -06:00

170 lines
6.4 KiB
Python

from unittest.mock import patch
from fastapi import HTTPException, Request
from fastapi.testclient import TestClient
from frigate.api.auth import (
get_allowed_cameras_for_filter,
get_current_user,
)
from frigate.models import Event, Recordings, ReviewSegment
from frigate.test.http_api.base_http_test import BaseTestHttp
class TestCameraAccessEventReview(BaseTestHttp):
def setUp(self):
super().setUp([Event, ReviewSegment, Recordings])
self.app = super().create_app()
# Mock get_current_user to return valid user for all tests
async def mock_get_current_user():
return {"username": "test_user", "role": "user"}
self.app.dependency_overrides[get_current_user] = mock_get_current_user
def tearDown(self):
self.app.dependency_overrides.clear()
super().tearDown()
def test_event_camera_access(self):
super().insert_mock_event("event1", camera="front_door")
super().insert_mock_event("event2", camera="back_door")
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
with TestClient(self.app) as client:
resp = client.get("/events")
assert resp.status_code == 200
ids = [e["id"] for e in resp.json()]
assert "event1" in ids
assert "event2" not in ids
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door",
"back_door",
]
with TestClient(self.app) as client:
resp = client.get("/events")
assert resp.status_code == 200
ids = [e["id"] for e in resp.json()]
assert "event1" in ids and "event2" in ids
def test_review_camera_access(self):
super().insert_mock_review_segment("rev1", camera="front_door")
super().insert_mock_review_segment("rev2", camera="back_door")
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
with TestClient(self.app) as client:
resp = client.get("/review")
assert resp.status_code == 200
ids = [r["id"] for r in resp.json()]
assert "rev1" in ids
assert "rev2" not in ids
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door",
"back_door",
]
with TestClient(self.app) as client:
resp = client.get("/review")
assert resp.status_code == 200
ids = [r["id"] for r in resp.json()]
assert "rev1" in ids and "rev2" in ids
def test_event_single_access(self):
super().insert_mock_event("event1", camera="front_door")
# Allowed
async def mock_require_allowed(camera: str, request: Request = None):
if camera == "front_door":
return
raise HTTPException(status_code=403, detail="Access denied")
with patch("frigate.api.event.require_camera_access", mock_require_allowed):
with TestClient(self.app) as client:
resp = client.get("/events/event1")
assert resp.status_code == 200
assert resp.json()["id"] == "event1"
# Disallowed
async def mock_require_disallowed(camera: str, request: Request = None):
raise HTTPException(status_code=403, detail="Access denied")
with patch("frigate.api.event.require_camera_access", mock_require_disallowed):
with TestClient(self.app) as client:
resp = client.get("/events/event1")
assert resp.status_code == 403
def test_review_single_access(self):
super().insert_mock_review_segment("rev1", camera="front_door")
# Allowed
async def mock_require_allowed(camera: str, request: Request = None):
if camera == "front_door":
return
raise HTTPException(status_code=403, detail="Access denied")
with patch("frigate.api.review.require_camera_access", mock_require_allowed):
with TestClient(self.app) as client:
resp = client.get("/review/rev1")
assert resp.status_code == 200
assert resp.json()["id"] == "rev1"
# Disallowed
async def mock_require_disallowed(camera: str, request: Request = None):
raise HTTPException(status_code=403, detail="Access denied")
with patch("frigate.api.review.require_camera_access", mock_require_disallowed):
with TestClient(self.app) as client:
resp = client.get("/review/rev1")
assert resp.status_code == 403
def test_event_search_access(self):
super().insert_mock_event("event1", camera="front_door")
super().insert_mock_event("event2", camera="back_door")
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
with TestClient(self.app) as client:
resp = client.get("/events", params={"cameras": "all"})
assert resp.status_code == 200
ids = [e["id"] for e in resp.json()]
assert "event1" in ids
assert "event2" not in ids
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door",
"back_door",
]
with TestClient(self.app) as client:
resp = client.get("/events", params={"cameras": "all"})
assert resp.status_code == 200
ids = [e["id"] for e in resp.json()]
assert "event1" in ids and "event2" in ids
def test_event_summary_access(self):
super().insert_mock_event("event1", camera="front_door")
super().insert_mock_event("event2", camera="back_door")
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
with TestClient(self.app) as client:
resp = client.get("/events/summary")
assert resp.status_code == 200
summary_list = resp.json()
assert len(summary_list) == 1
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door",
"back_door",
]
with TestClient(self.app) as client:
resp = client.get("/events/summary")
summary_list = resp.json()
assert len(summary_list) == 2