Enhance user roles to limit camera access (#20024)

* update config for roles and add validator

* ensure admin and viewer are never overridden

* add class method to user to retrieve all allowed cameras

* enforce config roles in auth api endpoints

* add camera access api dependency functions

* protect review endpoints

* protect preview endpoints

* rename param name for better fastapi injection matching

* remove unneeded

* protect export endpoints

* protect event endpoints

* protect media endpoints

* update auth hook for allowed cameras

* update default app view

* ensure anonymous user always returns all cameras

* limit cameras in explore

* cameras is already a list

* limit cameras in review/history

* limit cameras in live view

* limit cameras in camera groups

* only show face library and classification in sidebar for admin

* remove check in delete reviews

since admin role is required, no need to check camera access. fixes failing test

* pass request with camera access for tests

* more async

* camera access tests

* fix proxy auth tests

* allowed cameras for review tests

* combine event tests and refactor for camera access

* fix post validation for roles

* don't limit roles in create user dialog

* fix triggers endpoints

no need to run require camera access dep since the required role is admin

* fix type

* create and edit role dialogs

* delete role dialog

* fix role change dialog

* update settings view for roles

* i18n changes

* minor spacing tweaks

* docs

* use badges and camera name label component

* clarify docs

* display all cameras badge for admin and viewer

* i18n fix

* use validator to prevent reserved and empty roles from being assigned

* split users and roles into separate tabs in settings

* tweak docs

* clarify docs

* change icon

* don't memoize roles

always recalculate on component render
This commit is contained in:
Josh Hawkins
2025-09-12 05:19:29 -06:00
committed by GitHub
parent ba650af6f2
commit ed1e3a7c9a
41 changed files with 2286 additions and 739 deletions
+43 -7
View File
@@ -3,7 +3,8 @@ import { createContext, useEffect, useState } from "react";
import useSWR from "swr";
interface AuthState {
user: { username: string; role: "admin" | "viewer" | null } | null;
user: { username: string; role: string | null } | null;
allowedCameras: string[];
isLoading: boolean;
isAuthenticated: boolean; // true if auth is required
}
@@ -15,7 +16,12 @@ interface AuthContextType {
}
export const AuthContext = createContext<AuthContextType>({
auth: { user: null, isLoading: true, isAuthenticated: false },
auth: {
user: null,
allowedCameras: [],
isLoading: true,
isAuthenticated: false,
},
login: () => {},
logout: () => {},
});
@@ -23,6 +29,7 @@ export const AuthContext = createContext<AuthContextType>({
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [auth, setAuth] = useState<AuthState>({
user: null,
allowedCameras: [],
isLoading: true,
isAuthenticated: false,
});
@@ -38,7 +45,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
// auth required but not logged in
setAuth({ user: null, isLoading: false, isAuthenticated: true });
setAuth({
user: null,
allowedCameras: [],
isLoading: false,
isAuthenticated: true,
});
}
return;
}
@@ -49,20 +61,44 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
username: profile.username,
role: profile.role || "viewer",
};
setAuth({ user: newUser, isLoading: false, isAuthenticated: true });
const allowedCameras = Array.isArray(profile.allowed_cameras)
? profile.allowed_cameras
: [];
setAuth({
user: newUser,
allowedCameras,
isLoading: false,
isAuthenticated: true,
});
} else {
// Unauthenticated mode (anonymous)
setAuth({ user: null, isLoading: false, isAuthenticated: false });
setAuth({
user: null,
allowedCameras: [],
isLoading: false,
isAuthenticated: false,
});
}
}
}, [profile, error]);
const login = (user: AuthState["user"]) => {
setAuth({ user, isLoading: false, isAuthenticated: true });
setAuth((current) => ({
...current,
user,
isLoading: false,
isAuthenticated: true,
}));
};
const logout = () => {
setAuth({ user: null, isLoading: false, isAuthenticated: true });
setAuth({
user: null,
allowedCameras: [],
isLoading: false,
isAuthenticated: true,
});
axios.get("/logout", { withCredentials: true });
};