diff --git a/frigate/config/auth.py b/frigate/config/auth.py index a202fb1af..ccfadd839 100644 --- a/frigate/config/auth.py +++ b/frigate/config/auth.py @@ -1,6 +1,6 @@ -from typing import Optional +from typing import Dict, List, Optional -from pydantic import Field +from pydantic import Field, field_validator from .base import FrigateBaseModel @@ -34,3 +34,23 @@ class AuthConfig(FrigateBaseModel): ) # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 hash_iterations: int = Field(default=600000, title="Password hash iterations") + roles: Dict[str, List[str]] = Field( + default_factory=dict, + title="Role to camera mappings. Empty list grants access to all cameras.", + ) + + @field_validator("roles") + @classmethod + def validate_roles(cls, v: Dict[str, List[str]]) -> Dict[str, List[str]]: + # Ensure role names are valid (alphanumeric with underscores) + for role in v.keys(): + if not role.replace("_", "").isalnum(): + raise ValueError( + f"Invalid role name '{role}'. Must be alphanumeric with underscores." + ) + # Default admin and viewer to empty lists if not present + if "admin" not in v: + v["admin"] = [] + if "viewer" not in v: + v["viewer"] = [] + return v diff --git a/frigate/config/config.py b/frigate/config/config.py index dd84639d3..494f297a3 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -730,6 +730,27 @@ class FrigateConfig(FrigateBaseModel): raise ValueError("Zones cannot share names with cameras") return v + @field_validator("auth") + @classmethod + def validate_auth_roles(cls, v: AuthConfig, info: ValidationInfo) -> AuthConfig: + # Access cameras from the validated model + frigate_config = info.data.get("cameras", {}) + camera_names = ( + set(frigate_config.keys()) if isinstance(frigate_config, dict) else set() + ) + + for role, allowed_cameras in v.roles.items(): + invalid_cameras = [ + cam for cam in allowed_cameras if cam not in camera_names + ] + if invalid_cameras: + logger.warning( + f"Role '{role}' references non-existent cameras: {invalid_cameras}. " + f"These will grant no access until cameras are added." + ) + + return v + @classmethod def load(cls, **kwargs): """Loads the Frigate config file, runs migrations, and creates the config object."""