Compare commits

...
Author SHA1 Message Date
Josh Hawkins 743fef2782 add test 2026-09-30 09:33:37 -05:00
Josh Hawkins 17e1d5e094 don't let camera names waive the admin check on non-camera routes
The global admin guard skipped the admin check for any request whose first path segment matched a configured camera name, without looking at which route actually handled it. A camera named `faces`, `lpr`, `audio`, or `classification` let viewers reach the face, LPR, audio transcription, and classification endpoints that rely only on the global guard. The exemption now also requires the matched route to be a `/{camera_name}` route, so camera routes behave exactly as before.
2026-09-30 09:15:27 -05:00
3 changed files with 35 additions and 19 deletions
+9 -14
View File
@@ -127,23 +127,18 @@ def require_admin_by_default():
if path.startswith(EXEMPT_PREFIXES): if path.startswith(EXEMPT_PREFIXES):
return return
# Dynamic camera path exemption: # Camera routes enforce per-camera access via route-level dependencies
# Any path whose first segment matches a configured camera name should # (e.g. require_camera_access). Match on the route template, not the raw
# bypass the global admin requirement. These endpoints enforce access # path, so a camera named like another namespace (e.g. "faces") can't
# via route-level dependencies (e.g. require_camera_access) to ensure # waive the admin check for that namespace's routes.
# per-camera authorization. This allows non-admin authenticated users route = request.scope.get("route")
# (e.g. viewer role) to access camera-specific resources without
# needing admin privileges.
try:
if path.startswith("/"):
first_segment = path.split("/", 2)[1]
if ( if (
first_segment route is not None
and first_segment in request.app.frigate_config.cameras and route.path.startswith("/{camera_name}")
and request.path_params.get("camera_name")
in request.app.frigate_config.cameras
): ):
return return
except Exception:
pass
# For all other paths, require admin role # For all other paths, require admin role
# Internal port requests have admin role set automatically # Internal port requests have admin role set automatically
+4 -2
View File
@@ -144,7 +144,9 @@ class BaseTestHttp(unittest.TestCase):
except OSError: except OSError:
pass pass
def create_app(self, stats=None, event_metadata_publisher=None): def create_app(
self, stats=None, event_metadata_publisher=None, enforce_default_admin=False
):
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
app = create_fastapi_app( app = create_fastapi_app(
@@ -158,7 +160,7 @@ class BaseTestHttp(unittest.TestCase):
event_metadata_publisher, event_metadata_publisher,
None, None,
DebugReplayManager(), DebugReplayManager(),
enforce_default_admin=False, enforce_default_admin=enforce_default_admin,
) )
# Default test mocks for authentication # Default test mocks for authentication
+19
View File
@@ -47,6 +47,25 @@ class TestHttpApp(BaseTestHttp):
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["front_door"]["usage_percent"] == 25.0 assert response.json()["front_door"]["usage_percent"] == 25.0
def test_camera_name_collision_keeps_admin_default(self):
self.minimal_config["cameras"]["faces"] = self.minimal_config["cameras"].pop(
"front_door"
)
app = super().create_app(enforce_default_admin=True)
viewer = {"remote-user": "viewer", "remote-role": "viewer"}
with AuthTestClient(app) as client:
assert client.get("/faces", headers=viewer).status_code == 403
assert client.get("/faces").status_code == 200
assert (
client.post("/faces/train/person/classify", headers=viewer).status_code
== 403
)
# Camera routes for the same name stay reachable by viewers
response = client.get("/faces/recordings/summary", headers=viewer)
assert response.status_code == 200
def test_config_set_in_memory_replaces_objects_track_list(self): def test_config_set_in_memory_replaces_objects_track_list(self):
self.minimal_config["cameras"]["front_door"]["objects"] = { self.minimal_config["cameras"]["front_door"]["objects"] = {
"track": ["person", "car"], "track": ["person", "car"],