From 1f9904e5e41118ebed743a31b6b797ee41fe4f1d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:00:37 -0500 Subject: [PATCH] support env var substitution for notification email `notifications.email` is now an `EnvString`, so it can come from `secrets.yaml`, a Docker secret, or a container env var. `/api/config` returns the resolved value, so the email is now redacted for non-admin users, including each camera's inherited copy and the profile `base_config` copy. --- docs/docs/configuration/advanced/reference.md | 2 ++ frigate/api/app.py | 17 ++++++++++--- frigate/config/camera/notification.py | 3 ++- frigate/test/http_api/test_http_app.py | 25 +++++++++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docs/docs/configuration/advanced/reference.md b/docs/docs/configuration/advanced/reference.md index 89a0973f97..32656f2172 100644 --- a/docs/docs/configuration/advanced/reference.md +++ b/docs/docs/configuration/advanced/reference.md @@ -571,6 +571,8 @@ notifications: enabled: False # Optional: Email for push service to reach out to # NOTE: This is required to use notifications + # NOTE: Email can be specified with an environment variable or docker secrets that must begin with 'FRIGATE_'. + # e.g. email: '{FRIGATE_NOTIFICATION_EMAIL}' email: "admin@example.com" # Optional: Cooldown time for notifications in seconds (default: shown below) cooldown: 0 diff --git a/frigate/api/app.py b/frigate/api/app.py index 9cd1c8fe3b..82c70cb821 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -306,9 +306,12 @@ def config(request: Request): mode="json", warnings="none", exclude_none=True ) - # remove environment_vars for non-admin users - if request.headers.get("remote-role") != "admin": + is_admin = request.headers.get("remote-role") == "admin" + + # hide environment_vars and the notification email from non-admin users + if not is_admin: config.pop("environment_vars", None) + redact_credential(config["notifications"], "email") # redact mqtt credentials redact_credential(config["mqtt"], "password") @@ -365,7 +368,15 @@ def config(request: Request): camera_name ) if base_sections: - camera_dict["base_config"] = base_sections + # copy so redaction below can't alter the profile manager's cache + camera_dict["base_config"] = copy.deepcopy(base_sections) + + # cameras inherit the global notification email + if not is_admin: + redact_credential(camera_dict["notifications"], "email") + redact_credential( + camera_dict.get("base_config", {}).get("notifications", {}), "email" + ) # remove go2rtc stream passwords go2rtc: dict[str, Any] = config_obj.go2rtc.model_dump( diff --git a/frigate/config/camera/notification.py b/frigate/config/camera/notification.py index 7f5968193f..4f5e246b48 100644 --- a/frigate/config/camera/notification.py +++ b/frigate/config/camera/notification.py @@ -1,6 +1,7 @@ from pydantic import Field from ..base import FrigateBaseModel +from ..env import EnvString __all__ = ["NotificationConfig"] @@ -11,7 +12,7 @@ class NotificationConfig(FrigateBaseModel): title="Enable notifications", description="Enable or disable notifications for all cameras; can be overridden per-camera.", ) - email: str | None = Field( + email: EnvString | None = Field( default=None, title="Notification email", description="Email address used for push notifications or required by certain notification providers.", diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py index 3462a4f108..96f6ae7868 100644 --- a/frigate/test/http_api/test_http_app.py +++ b/frigate/test/http_api/test_http_app.py @@ -4,6 +4,7 @@ from unittest.mock import Mock, patch import frigate.genai from frigate.config import GenAIProviderEnum +from frigate.config.env import FRIGATE_ENV_VARS from frigate.const import MODEL_CACHE_DIR, REDACTED_CREDENTIAL_SENTINEL from frigate.genai import GenAIClient from frigate.models import Event, Recordings, ReviewSegment @@ -92,6 +93,30 @@ class TestHttpApp(BaseTestHttp): mqtt = response.json()["mqtt"] assert mqtt["password"] == REDACTED_CREDENTIAL_SENTINEL + def test_config_response_hides_notification_email_from_viewers(self): + self.minimal_config["notifications"] = {"email": "{FRIGATE_TEST_EMAIL}"} + + with patch.dict(FRIGATE_ENV_VARS, {"FRIGATE_TEST_EMAIL": "me@example.com"}): + app = super().create_app() + + assert app.frigate_config.notifications.email == "me@example.com" + + with AuthTestClient(app) as client: + response = client.get( + "/config", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + ) + assert response.status_code == 200 + config = response.json() + assert config["notifications"]["email"] == REDACTED_CREDENTIAL_SENTINEL + assert ( + config["cameras"]["front_door"]["notifications"]["email"] + == REDACTED_CREDENTIAL_SENTINEL + ) + + response = client.get("/config") + assert response.json()["notifications"]["email"] == "me@example.com" + def test_config_response_keeps_plus_model_reference(self): model_id = "test_plus_reference" model_path = os.path.join(MODEL_CACHE_DIR, model_id)