mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 22:57:40 +03:00
feat: add role-based visibility for camera groups
Add optional `roles` field to camera groups config to control which user roles can see each group. Groups without roles are visible only to admins. Admin users always see all groups. Backend filters groups in GET /config based on remote-role header. Frontend adds roles multiselect in group editor (admin only). https://claude.ai/code/session_011sp9kHQfM39JvVxKHFh1Xq
This commit is contained in:
parent
e8b9f50bc9
commit
8f990817b4
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 && (
|
||||
<>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("group.roles.label", { defaultValue: "Roles" })}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("group.roles.desc", {
|
||||
defaultValue:
|
||||
"Select which roles can see this camera group. If no roles selected, group is only visible to admins.",
|
||||
})}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
{Object.keys(config.auth.roles)
|
||||
.filter((role) => role !== "admin")
|
||||
.map((role) => (
|
||||
<FormControl key={role}>
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="mx-2 w-full cursor-pointer text-primary smart-capitalize">
|
||||
{role}
|
||||
</span>
|
||||
<Switch
|
||||
id={`role-${role}`}
|
||||
checked={field.value?.includes(role) ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
const updatedRoles = checked
|
||||
? [...(field.value || []), role]
|
||||
: (field.value || []).filter((r) => r !== role);
|
||||
form.setValue("roles", updatedRoles);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
))}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<div className="flex flex-row gap-2 py-5 md:pb-0">
|
||||
|
||||
@ -345,6 +345,7 @@ export type CameraGroupConfig = {
|
||||
cameras: string[];
|
||||
icon: IconName;
|
||||
order: number;
|
||||
roles?: string[];
|
||||
};
|
||||
|
||||
export type StreamType = "no-streaming" | "smart" | "continuous";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user