mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-18 18:01:14 +03:00
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* Catch faces that become empty after cropping * don't drop batched camera add/remove config updates TrackedObjectProcessor drained all pending camera config updates at once but handled them in a mutually exclusive if/elif on enabled/add/remove, so only one topic was processed per drain. When an add arrived in the same batch as an enabled update, the add was skipped and the new camera never got a camera state. Adding a camera reliably produced that batch: config_set now re-applies runtime overrides, which republishes an enabled update for every previously toggled camera immediately before the add, in the same request. The dashboard and camera capture still saw the camera (the maintainer does not subscribe to enabled, so it got a clean add-only batch), but object_processing did not, and disabling the camera then crashed with a KeyError on the unguarded camera_states lookup. Handle add and remove independently instead of as exclusive branches so a batched add is no longer dropped, and guard the remove lookup so a missing state is skipped rather than raising. Drop the enabled branch entirely: it only ever set prev_enabled when it was None, but prev_enabled is seeded to a bool at camera state creation and is never None (mypy flags the body as unreachable), and the actual enable/disable transition is already driven by the disabled-state loop from config.enabled. * Don't stay on motion search page when user cancels flow * fix notification test button being blocked by websocket auth * fix overflowing model names in settings genai widget * add note about auth debugging --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
309 lines
9.4 KiB
Python
309 lines
9.4 KiB
Python
"""Tests for WebSocket authorization checks."""
|
|
|
|
import unittest
|
|
|
|
from frigate.comms.ws import _check_ws_authorization
|
|
from frigate.const import INSERT_MANY_RECORDINGS, UPDATE_CAMERA_ACTIVITY
|
|
|
|
|
|
class TestCheckWsAuthorization(unittest.TestCase):
|
|
"""Tests for the _check_ws_authorization pure function."""
|
|
|
|
DEFAULT_SEPARATOR = ","
|
|
|
|
# admin/viewer are reserved and always map to all cameras (empty list);
|
|
# custom roles map to a specific set of cameras.
|
|
ROLES_CONFIG = {
|
|
"admin": [],
|
|
"viewer": [],
|
|
"yard": ["front_door", "backyard"],
|
|
"garage_only": ["garage"],
|
|
}
|
|
CAMERA_NAMES = {"front_door", "backyard", "garage"}
|
|
|
|
# --- IPC topic blocking (unconditional, regardless of role) ---
|
|
|
|
def test_ipc_topic_blocked_for_admin(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
INSERT_MANY_RECORDINGS, "admin", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
def test_ipc_topic_blocked_for_viewer(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
UPDATE_CAMERA_ACTIVITY, "viewer", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
def test_ipc_topic_blocked_when_no_role(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
INSERT_MANY_RECORDINGS, None, self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
# --- Viewer allowed topics ---
|
|
|
|
def test_viewer_can_send_on_connect(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization("onConnect", "viewer", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_viewer_can_send_model_state(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization("modelState", "viewer", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_viewer_can_send_audio_transcription_state(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization(
|
|
"audioTranscriptionState", "viewer", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
def test_viewer_can_send_birdseye_layout(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization("birdseyeLayout", "viewer", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_viewer_can_send_embeddings_reindex_progress(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization(
|
|
"embeddingsReindexProgress", "viewer", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
# --- Viewer blocked from admin topics ---
|
|
|
|
def test_viewer_blocked_from_restart(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization("restart", "viewer", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_viewer_blocked_from_camera_detect_set(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
"front_door/detect/set", "viewer", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
def test_viewer_blocked_from_camera_ptz(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization("front_door/ptz", "viewer", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_viewer_blocked_from_global_notifications_set(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
"notifications/set", "viewer", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
def test_viewer_blocked_from_camera_notifications_suspend(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
"front_door/notifications/suspend", "viewer", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
def test_viewer_blocked_from_arbitrary_unknown_topic(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
"some_random_topic", "viewer", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
def test_viewer_blocked_from_notification_test(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
"notification_test", "viewer", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
# --- Admin access ---
|
|
|
|
def test_admin_can_send_restart(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization("restart", "admin", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_admin_can_send_camera_detect_set(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization(
|
|
"front_door/detect/set", "admin", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
def test_admin_can_send_camera_ptz(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization("front_door/ptz", "admin", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_admin_can_send_notification_test(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization(
|
|
"notification_test", "admin", self.DEFAULT_SEPARATOR
|
|
)
|
|
)
|
|
|
|
# --- Comma-separated roles ---
|
|
|
|
def test_comma_separated_admin_viewer_grants_admin(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization("restart", "admin,viewer", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_comma_separated_viewer_admin_grants_admin(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization("restart", "viewer,admin", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_comma_separated_with_spaces(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization("restart", "viewer, admin", self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
# --- Custom separator ---
|
|
|
|
def test_pipe_separator(self):
|
|
self.assertTrue(_check_ws_authorization("restart", "viewer|admin", "|"))
|
|
|
|
def test_pipe_separator_no_admin(self):
|
|
self.assertFalse(_check_ws_authorization("restart", "viewer|editor", "|"))
|
|
|
|
# --- No role header (fail-closed) ---
|
|
|
|
def test_no_role_header_blocks_admin_topics(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization("restart", None, self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
def test_no_role_header_allows_viewer_topics(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization("onConnect", None, self.DEFAULT_SEPARATOR)
|
|
)
|
|
|
|
# --- Camera-scoped PTZ access (non-admin with camera access) ---
|
|
|
|
def test_viewer_can_ptz_camera_with_access(self):
|
|
# viewer maps to all cameras, so PTZ is allowed
|
|
self.assertTrue(
|
|
_check_ws_authorization(
|
|
"front_door/ptz",
|
|
"viewer",
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
def test_custom_role_can_ptz_assigned_camera(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization(
|
|
"front_door/ptz",
|
|
"yard",
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
def test_custom_role_blocked_from_ptz_unassigned_camera(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
"garage/ptz",
|
|
"yard",
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
def test_multiple_roles_union_camera_access_for_ptz(self):
|
|
# "yard" covers front_door/backyard, "garage_only" covers garage
|
|
self.assertTrue(
|
|
_check_ws_authorization(
|
|
"garage/ptz",
|
|
"yard,garage_only",
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
def test_unknown_role_blocked_from_ptz(self):
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
"front_door/ptz",
|
|
"nonexistent",
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
def test_no_role_header_treated_as_viewer_for_ptz(self):
|
|
# proxy-only / auth-disabled setups default to the viewer role
|
|
self.assertTrue(
|
|
_check_ws_authorization(
|
|
"front_door/ptz",
|
|
None,
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
def test_camera_access_does_not_grant_set_commands(self):
|
|
# camera access enables PTZ only, not config-changing "set" commands
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
"front_door/detect/set",
|
|
"yard",
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
def test_ptz_autotracker_stays_admin_only(self):
|
|
# ptz_autotracker is a config toggle, not a live-view action
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
"front_door/ptz_autotracker/set",
|
|
"viewer",
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
def test_admin_can_ptz_any_camera_with_config(self):
|
|
self.assertTrue(
|
|
_check_ws_authorization(
|
|
"garage/ptz",
|
|
"admin",
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
def test_ipc_topic_still_blocked_with_camera_access(self):
|
|
# IPC topics are blocked unconditionally, even with camera access
|
|
self.assertFalse(
|
|
_check_ws_authorization(
|
|
UPDATE_CAMERA_ACTIVITY,
|
|
"viewer",
|
|
self.DEFAULT_SEPARATOR,
|
|
self.ROLES_CONFIG,
|
|
self.CAMERA_NAMES,
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|