diff --git a/frigate/api/app.py b/frigate/api/app.py index 60905be632..401c5cbeaa 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -403,6 +403,19 @@ def config(request: Request): config["go2rtc"]["streams"][stream_name] = cleaned + for section in ("rtsp", "api"): + go2rtc_section = config["go2rtc"].get(section) + if isinstance(go2rtc_section, dict): + redact_credential(go2rtc_section, "username") + redact_credential(go2rtc_section, "password") + + webrtc = config["go2rtc"].get("webrtc") + if isinstance(webrtc, dict): + for ice_server in webrtc.get("ice_servers") or []: + if isinstance(ice_server, dict): + redact_credential(ice_server, "username") + redact_credential(ice_server, "credential") + config["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()} for index, model in enumerate(config_obj.models): @@ -788,9 +801,11 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo _restore_masked_camera_paths(body.config_data, request.app.frigate_config) updates = flatten_config_data(body.config_data) updates = {k: ("" if v is None else v) for k, v in updates.items()} - # Drop any field whose value is still the redaction sentinel + # Drop any field whose value still holds the redaction sentinel updates = { - k: v for k, v in updates.items() if v != REDACTED_CREDENTIAL_SENTINEL + k: v + for k, v in updates.items() + if REDACTED_CREDENTIAL_SENTINEL not in str(v) } if not updates: @@ -948,11 +963,12 @@ def config_set(request: Request, body: AppConfigSetBody): # Convert None values to empty strings for deletion (e.g., when deleting masks) updates = {k: ("" if v is None else v) for k, v in updates.items()} # Drop sentinel-valued fields so untouched credential - # placeholders don't clobber the saved YAML value. + # placeholders don't clobber the saved YAML value. Lists + # are leaf values, so check for a sentinel nested inside. updates = { k: v for k, v in updates.items() - if v != REDACTED_CREDENTIAL_SENTINEL + if REDACTED_CREDENTIAL_SENTINEL not in str(v) } if not updates: diff --git a/frigate/test/http_api/test_http_config_set.py b/frigate/test/http_api/test_http_config_set.py index 6d9374e76d..c1af38ba4e 100644 --- a/frigate/test/http_api/test_http_config_set.py +++ b/frigate/test/http_api/test_http_config_set.py @@ -14,6 +14,7 @@ from frigate.config.camera.updater import ( CameraConfigUpdateTopic, ) from frigate.config.holder import ConfigHolder +from frigate.const import REDACTED_CREDENTIAL_SENTINEL from frigate.models import Event, Recordings, ReviewSegment from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp @@ -568,6 +569,46 @@ class TestConfigSetWildcardPropagation(BaseTestHttp): finally: os.unlink(config_path) + @patch("frigate.api.app.find_config_file") + def test_go2rtc_credentials_redacted_and_round_trip(self, mock_find_config): + self._add_credentials() + config_path = self._write_config_file() + mock_find_config.return_value = config_path + + try: + app, _ = self._create_app_with_publisher() + + with AuthTestClient(app) as client: + go2rtc = client.get("/config").json()["go2rtc"] + ice_server = go2rtc["webrtc"]["ice_servers"][0] + + for section in ("rtsp", "api"): + self.assertEqual( + go2rtc[section]["username"], REDACTED_CREDENTIAL_SENTINEL + ) + self.assertEqual( + go2rtc[section]["password"], REDACTED_CREDENTIAL_SENTINEL + ) + self.assertEqual(ice_server["credential"], REDACTED_CREDENTIAL_SENTINEL) + self.assertEqual(ice_server["username"], REDACTED_CREDENTIAL_SENTINEL) + self.assertEqual(go2rtc["webrtc"]["candidates"], ["10.0.0.5:8555"]) + + resp = client.put( + "/config/set", + json={"config_data": {"go2rtc": go2rtc}, "requires_restart": 0}, + ) + self.assertEqual(resp.status_code, 200) + + raw, saved = self._read_config_file(config_path) + self.assertNotIn(REDACTED_CREDENTIAL_SENTINEL, raw) + self.assertEqual(saved["go2rtc"]["rtsp"]["password"], "rtsppass") + self.assertEqual(saved["go2rtc"]["api"]["password"], "apipass") + self.assertEqual( + saved["go2rtc"]["webrtc"]["ice_servers"][0]["credential"], "turnpass" + ) + finally: + os.unlink(config_path) + if __name__ == "__main__": unittest.main()