diff --git a/frigate/api/app.py b/frigate/api/app.py index 379cc7278..9a66a6eda 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -226,6 +226,18 @@ def config(request: Request): request.app.frigate_config.model.merged_labelmap ) + # filter camera_groups by role + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + + if username and role and role != "admin": + filtered_groups = {} + for group_name, group_config in config.get("camera_groups", {}).items(): + group_roles = group_config.get("roles") + if group_roles and role in group_roles: + filtered_groups[group_name] = group_config + config["camera_groups"] = filtered_groups + return JSONResponse(content=config) diff --git a/frigate/config/camera_group.py b/frigate/config/camera_group.py index 65319001a..5c747cbd6 100644 --- a/frigate/config/camera_group.py +++ b/frigate/config/camera_group.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Optional, Union from pydantic import Field, field_validator @@ -23,6 +23,11 @@ class CameraGroupConfig(FrigateBaseModel): title="Sort order", description="Numeric order used to sort camera groups in the UI; larger numbers appear later.", ) + roles: Optional[list[str]] = Field( + default=None, + title="Roles", + description="List of roles that can see this camera group. If not set, only admin can see it.", + ) @field_validator("cameras", mode="before") @classmethod @@ -31,3 +36,11 @@ class CameraGroupConfig(FrigateBaseModel): return [v] return v + + @field_validator("roles", mode="before") + @classmethod + def validate_roles(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index 6d86c57f8..978846c8d 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -721,6 +721,7 @@ export function CameraGroupEdit({ .refine((value) => Object.keys(LuIcons).includes(value), { message: "Invalid icon", }), + roles: z.array(z.string()).optional(), }); const onSubmit = useCallback( @@ -756,10 +757,13 @@ export function CameraGroupEdit({ const cameraQueries = values.cameras .map((cam) => `&camera_groups.${values.name}.cameras=${cam}`) .join(""); + const roleQueries = (values.roles ?? []) + .map((role) => `&camera_groups.${values.name}.roles=${role}`) + .join(""); axios .put( - `config/set?${renamingQuery}${orderQuery}&${iconQuery}${cameraQueries}`, + `config/set?${renamingQuery}${orderQuery}&${iconQuery}${cameraQueries}${roleQueries}`, { requires_restart: 0, }, @@ -828,6 +832,7 @@ export function CameraGroupEdit({ name: (editingGroup && editingGroup[0]) ?? "", icon: editingGroup && (editingGroup[1].icon as IconName), cameras: editingGroup && editingGroup[1].cameras, + roles: (editingGroup && editingGroup[1].roles) ?? [], }, }); @@ -970,6 +975,49 @@ export function CameraGroupEdit({ )} /> + {isAdmin && config?.auth?.roles && ( + <> + + ( + + {t("group.roles.label", { defaultValue: "Roles" })} + + {t("group.roles.desc", { + defaultValue: + "Select which roles can see this camera group. If no roles selected, group is only visible to admins.", + })} + + + {Object.keys(config.auth.roles) + .filter((role) => role !== "admin") + .map((role) => ( + +
+ + {role} + + { + const updatedRoles = checked + ? [...(field.value || []), role] + : (field.value || []).filter((r) => r !== role); + form.setValue("roles", updatedRoles); + }} + /> +
+
+ ))} +
+ )} + /> + + )} +
diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index 3d7f18ddd..ac1e44f6a 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -345,6 +345,7 @@ export type CameraGroupConfig = { cameras: string[]; icon: IconName; order: number; + roles?: string[]; }; export type StreamType = "no-streaming" | "smart" | "continuous";