mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-15 03:22:10 +03:00
protect event endpoints
This commit is contained in:
parent
2a9d5f5e51
commit
a984227c9b
@ -8,6 +8,7 @@ import random
|
||||
import string
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from urllib.parse import unquote
|
||||
|
||||
import cv2
|
||||
@ -19,7 +20,11 @@ from pathvalidate import sanitize_filename
|
||||
from peewee import JOIN, DoesNotExist, fn, operator
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.api.auth import require_role
|
||||
from frigate.api.auth import (
|
||||
get_allowed_cameras_for_filter,
|
||||
require_camera_access,
|
||||
require_role,
|
||||
)
|
||||
from frigate.api.defs.query.events_query_parameters import (
|
||||
DEFAULT_TIME_RANGE,
|
||||
EventsQueryParams,
|
||||
@ -61,7 +66,10 @@ router = APIRouter(tags=[Tags.events])
|
||||
|
||||
|
||||
@router.get("/events", response_model=list[EventResponse])
|
||||
def events(params: EventsQueryParams = Depends()):
|
||||
def events(
|
||||
params: EventsQueryParams = Depends(),
|
||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||
):
|
||||
camera = params.camera
|
||||
cameras = params.cameras
|
||||
|
||||
@ -135,8 +143,14 @@ def events(params: EventsQueryParams = Depends()):
|
||||
clauses.append((Event.camera == camera))
|
||||
|
||||
if cameras != "all":
|
||||
camera_list = cameras.split(",")
|
||||
clauses.append((Event.camera << camera_list))
|
||||
requested = set(cameras.split(","))
|
||||
filtered = requested.intersection(allowed_cameras)
|
||||
if not filtered:
|
||||
return JSONResponse(content=[])
|
||||
camera_list = list(filtered)
|
||||
else:
|
||||
camera_list = allowed_cameras
|
||||
clauses.append((Event.camera << camera_list))
|
||||
|
||||
if labels != "all":
|
||||
label_list = labels.split(",")
|
||||
@ -321,9 +335,17 @@ def events(params: EventsQueryParams = Depends()):
|
||||
|
||||
|
||||
@router.get("/events/explore", response_model=list[EventResponse])
|
||||
def events_explore(limit: int = 10):
|
||||
def events_explore(
|
||||
limit: int = 10,
|
||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||
):
|
||||
# get distinct labels for all events
|
||||
distinct_labels = Event.select(Event.label).distinct().order_by(Event.label)
|
||||
distinct_labels = (
|
||||
Event.select(Event.label)
|
||||
.where(Event.camera << allowed_cameras)
|
||||
.distinct()
|
||||
.order_by(Event.label)
|
||||
)
|
||||
|
||||
label_counts = {}
|
||||
|
||||
@ -334,14 +356,18 @@ def events_explore(limit: int = 10):
|
||||
# get most recent events for this label
|
||||
label_events = (
|
||||
Event.select()
|
||||
.where(Event.label == label)
|
||||
.where((Event.label == label) & (Event.camera << allowed_cameras))
|
||||
.order_by(Event.start_time.desc())
|
||||
.limit(limit)
|
||||
.iterator()
|
||||
)
|
||||
|
||||
# count total events for this label
|
||||
label_counts[label] = Event.select().where(Event.label == label).count()
|
||||
label_counts[label] = (
|
||||
Event.select()
|
||||
.where((Event.label == label) & (Event.camera << allowed_cameras))
|
||||
.count()
|
||||
)
|
||||
|
||||
yield from label_events
|
||||
|
||||
@ -403,6 +429,16 @@ def event_ids(ids: str):
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
for event_id in ids:
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": f"Event {event_id} not found"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
try:
|
||||
events = Event.select().where(Event.id << ids).dicts().iterator()
|
||||
return JSONResponse(list(events))
|
||||
@ -413,7 +449,11 @@ def event_ids(ids: str):
|
||||
|
||||
|
||||
@router.get("/events/search")
|
||||
def events_search(request: Request, params: EventsSearchQueryParams = Depends()):
|
||||
def events_search(
|
||||
request: Request,
|
||||
params: EventsSearchQueryParams = Depends(),
|
||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||
):
|
||||
query = params.query
|
||||
search_type = params.search_type
|
||||
include_thumbnails = params.include_thumbnails
|
||||
@ -486,7 +526,13 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
||||
event_filters = []
|
||||
|
||||
if cameras != "all":
|
||||
event_filters.append((Event.camera << cameras.split(",")))
|
||||
requested = set(cameras.split(","))
|
||||
filtered = requested.intersection(allowed_cameras)
|
||||
if not filtered:
|
||||
return JSONResponse(content=[])
|
||||
event_filters.append((Event.camera << list(filtered)))
|
||||
else:
|
||||
event_filters.append((Event.camera << allowed_cameras))
|
||||
|
||||
if labels != "all":
|
||||
event_filters.append((Event.label << labels.split(",")))
|
||||
@ -739,7 +785,10 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
||||
|
||||
|
||||
@router.get("/events/summary")
|
||||
def events_summary(params: EventsSummaryQueryParams = Depends()):
|
||||
def events_summary(
|
||||
params: EventsSummaryQueryParams = Depends(),
|
||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||
):
|
||||
tz_name = params.timezone
|
||||
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
|
||||
has_clip = params.has_clip
|
||||
@ -771,7 +820,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
|
||||
Event.zones,
|
||||
fn.COUNT(Event.id).alias("count"),
|
||||
)
|
||||
.where(reduce(operator.and_, clauses))
|
||||
.where(reduce(operator.and_, clauses) & (Event.camera << allowed_cameras))
|
||||
.group_by(
|
||||
Event.camera,
|
||||
Event.label,
|
||||
@ -788,7 +837,9 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
|
||||
@router.get("/events/{event_id}", response_model=EventResponse)
|
||||
def event(event_id: str):
|
||||
try:
|
||||
return model_to_dict(Event.get(Event.id == event_id))
|
||||
event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
return model_to_dict(event)
|
||||
except DoesNotExist:
|
||||
return JSONResponse(content="Event not found", status_code=404)
|
||||
|
||||
@ -801,6 +852,7 @@ def event(event_id: str):
|
||||
def set_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
@ -835,6 +887,7 @@ def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None):
|
||||
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
message = f"Event {event_id} not found"
|
||||
logger.error(message)
|
||||
@ -945,6 +998,7 @@ def false_positive(request: Request, event_id: str):
|
||||
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
message = f"Event {event_id} not found"
|
||||
logger.error(message)
|
||||
@ -1025,6 +1079,7 @@ def false_positive(request: Request, event_id: str):
|
||||
def delete_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
@ -1052,6 +1107,7 @@ def set_sub_label(
|
||||
):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
event = None
|
||||
|
||||
@ -1106,6 +1162,7 @@ def set_plate(
|
||||
):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
event = None
|
||||
|
||||
@ -1161,6 +1218,7 @@ def set_description(
|
||||
):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
@ -1210,6 +1268,7 @@ def regenerate_description(
|
||||
):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
@ -1283,6 +1342,7 @@ def generate_description_embedding(
|
||||
def delete_single_event(event_id: str, request: Request) -> dict:
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
except DoesNotExist:
|
||||
return {"success": False, "message": f"Event {event_id} not found"}
|
||||
|
||||
@ -1351,7 +1411,10 @@ def delete_events(request: Request, body: EventsDeleteBody):
|
||||
@router.post(
|
||||
"/events/{camera_name}/{label}/create",
|
||||
response_model=EventCreateResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
dependencies=[
|
||||
Depends(lambda: require_role(["admin"])),
|
||||
Depends(require_camera_access),
|
||||
],
|
||||
)
|
||||
def create_event(
|
||||
request: Request,
|
||||
@ -1412,6 +1475,8 @@ def create_event(
|
||||
)
|
||||
def end_event(request: Request, event_id: str, body: EventsEndBody):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
require_camera_access(event.camera)
|
||||
end_time = body.end_time or datetime.datetime.now().timestamp()
|
||||
request.app.event_metadata_updater.publish(
|
||||
(event_id, end_time), EventMetadataTypeEnum.manual_event_end.value
|
||||
@ -1433,12 +1498,15 @@ def end_event(request: Request, event_id: str, body: EventsEndBody):
|
||||
@router.post(
|
||||
"/trigger/embedding",
|
||||
response_model=dict,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
dependencies=[
|
||||
Depends(lambda: require_role(["admin"])),
|
||||
Depends(require_camera_access),
|
||||
],
|
||||
)
|
||||
def create_trigger_embedding(
|
||||
request: Request,
|
||||
body: TriggerEmbeddingBody,
|
||||
camera: str,
|
||||
camera_name: str,
|
||||
name: str,
|
||||
):
|
||||
try:
|
||||
@ -1454,13 +1522,13 @@ def create_trigger_embedding(
|
||||
# Check if trigger already exists
|
||||
if (
|
||||
Trigger.select()
|
||||
.where(Trigger.camera == camera, Trigger.name == name)
|
||||
.where(Trigger.camera == camera_name, Trigger.name == name)
|
||||
.exists()
|
||||
):
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
"message": f"Trigger {camera}:{name} already exists",
|
||||
"message": f"Trigger {camera_name}:{name} already exists",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
@ -1530,28 +1598,29 @@ def create_trigger_embedding(
|
||||
# Save image to the triggers directory
|
||||
try:
|
||||
os.makedirs(
|
||||
os.path.join(TRIGGER_DIR, sanitize_filename(camera)), exist_ok=True
|
||||
os.path.join(TRIGGER_DIR, sanitize_filename(camera_name)),
|
||||
exist_ok=True,
|
||||
)
|
||||
with open(
|
||||
os.path.join(
|
||||
TRIGGER_DIR,
|
||||
sanitize_filename(camera),
|
||||
sanitize_filename(camera_name),
|
||||
f"{sanitize_filename(body.data)}.webp",
|
||||
),
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(thumbnail)
|
||||
logger.debug(
|
||||
f"Writing thumbnail for trigger with data {body.data} in {camera}."
|
||||
f"Writing thumbnail for trigger with data {body.data} in {camera_name}."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(e.with_traceback())
|
||||
logger.error(
|
||||
f"Failed to write thumbnail for trigger with data {body.data} in {camera}"
|
||||
f"Failed to write thumbnail for trigger with data {body.data} in {camera_name}"
|
||||
)
|
||||
|
||||
Trigger.create(
|
||||
camera=camera,
|
||||
camera=camera_name,
|
||||
name=name,
|
||||
type=body.type,
|
||||
data=body.data,
|
||||
@ -1565,7 +1634,7 @@ def create_trigger_embedding(
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"message": f"Trigger created successfully for {camera}:{name}",
|
||||
"message": f"Trigger created successfully for {camera_name}:{name}",
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
@ -1584,11 +1653,14 @@ def create_trigger_embedding(
|
||||
@router.put(
|
||||
"/trigger/embedding/{camera}/{name}",
|
||||
response_model=dict,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
dependencies=[
|
||||
Depends(lambda: require_role(["admin"])),
|
||||
Depends(require_camera_access),
|
||||
],
|
||||
)
|
||||
def update_trigger_embedding(
|
||||
request: Request,
|
||||
camera: str,
|
||||
camera_name: str,
|
||||
name: str,
|
||||
body: TriggerEmbeddingBody,
|
||||
):
|
||||
@ -1609,7 +1681,9 @@ def update_trigger_embedding(
|
||||
embedding = context.generate_description_embedding(body.data)
|
||||
elif body.type == "thumbnail":
|
||||
webp_file = sanitize_filename(body.data) + ".webp"
|
||||
webp_path = os.path.join(TRIGGER_DIR, sanitize_filename(camera), webp_file)
|
||||
webp_path = os.path.join(
|
||||
TRIGGER_DIR, sanitize_filename(camera_name), webp_file
|
||||
)
|
||||
|
||||
try:
|
||||
event: Event = Event.get(Event.id == body.data)
|
||||
@ -1656,7 +1730,9 @@ def update_trigger_embedding(
|
||||
)
|
||||
|
||||
# Check if trigger exists for upsert
|
||||
trigger = Trigger.get_or_none(Trigger.camera == camera, Trigger.name == name)
|
||||
trigger = Trigger.get_or_none(
|
||||
Trigger.camera == camera_name, Trigger.name == name
|
||||
)
|
||||
|
||||
if trigger:
|
||||
# Update existing trigger
|
||||
@ -1665,17 +1741,17 @@ def update_trigger_embedding(
|
||||
os.remove(
|
||||
os.path.join(
|
||||
TRIGGER_DIR,
|
||||
sanitize_filename(camera),
|
||||
sanitize_filename(camera_name),
|
||||
f"{trigger.data}.webp",
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
f"Deleted thumbnail for trigger with data {trigger.data} in {camera}."
|
||||
f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(e.with_traceback())
|
||||
logger.error(
|
||||
f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera}"
|
||||
f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera_name}"
|
||||
)
|
||||
|
||||
Trigger.update(
|
||||
@ -1685,11 +1761,11 @@ def update_trigger_embedding(
|
||||
threshold=body.threshold,
|
||||
triggering_event_id="",
|
||||
last_triggered=None,
|
||||
).where(Trigger.camera == camera, Trigger.name == name).execute()
|
||||
).where(Trigger.camera == camera_name, Trigger.name == name).execute()
|
||||
else:
|
||||
# Create new trigger (for rename case)
|
||||
Trigger.create(
|
||||
camera=camera,
|
||||
camera=camera_name,
|
||||
name=name,
|
||||
type=body.type,
|
||||
data=body.data,
|
||||
@ -1703,7 +1779,7 @@ def update_trigger_embedding(
|
||||
if body.type == "thumbnail":
|
||||
# Save image to the triggers directory
|
||||
try:
|
||||
camera_path = os.path.join(TRIGGER_DIR, sanitize_filename(camera))
|
||||
camera_path = os.path.join(TRIGGER_DIR, sanitize_filename(camera_name))
|
||||
os.makedirs(camera_path, exist_ok=True)
|
||||
with open(
|
||||
os.path.join(camera_path, f"{sanitize_filename(body.data)}.webp"),
|
||||
@ -1711,18 +1787,18 @@ def update_trigger_embedding(
|
||||
) as f:
|
||||
f.write(thumbnail)
|
||||
logger.debug(
|
||||
f"Writing thumbnail for trigger with data {body.data} in {camera}."
|
||||
f"Writing thumbnail for trigger with data {body.data} in {camera_name}."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(e.with_traceback())
|
||||
logger.error(
|
||||
f"Failed to write thumbnail for trigger with data {body.data} in {camera}"
|
||||
f"Failed to write thumbnail for trigger with data {body.data} in {camera_name}"
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"message": f"Trigger updated successfully for {camera}:{name}",
|
||||
"message": f"Trigger updated successfully for {camera_name}:{name}",
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
@ -1741,34 +1817,39 @@ def update_trigger_embedding(
|
||||
@router.delete(
|
||||
"/trigger/embedding/{camera}/{name}",
|
||||
response_model=dict,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
dependencies=[
|
||||
Depends(lambda: require_role(["admin"])),
|
||||
Depends(require_camera_access),
|
||||
],
|
||||
)
|
||||
def delete_trigger_embedding(
|
||||
request: Request,
|
||||
camera: str,
|
||||
camera_name: str,
|
||||
name: str,
|
||||
):
|
||||
try:
|
||||
trigger = Trigger.get_or_none(Trigger.camera == camera, Trigger.name == name)
|
||||
trigger = Trigger.get_or_none(
|
||||
Trigger.camera == camera_name, Trigger.name == name
|
||||
)
|
||||
if trigger is None:
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
"message": f"Trigger {camera}:{name} not found",
|
||||
"message": f"Trigger {camera_name}:{name} not found",
|
||||
},
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
deleted = (
|
||||
Trigger.delete()
|
||||
.where(Trigger.camera == camera, Trigger.name == name)
|
||||
.where(Trigger.camera == camera_name, Trigger.name == name)
|
||||
.execute()
|
||||
)
|
||||
if deleted == 0:
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
"message": f"Error deleting trigger {camera}:{name}",
|
||||
"message": f"Error deleting trigger {camera_name}:{name}",
|
||||
},
|
||||
status_code=401,
|
||||
)
|
||||
@ -1776,22 +1857,22 @@ def delete_trigger_embedding(
|
||||
try:
|
||||
os.remove(
|
||||
os.path.join(
|
||||
TRIGGER_DIR, sanitize_filename(camera), f"{trigger.data}.webp"
|
||||
TRIGGER_DIR, sanitize_filename(camera_name), f"{trigger.data}.webp"
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
f"Deleted thumbnail for trigger with data {trigger.data} in {camera}."
|
||||
f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(e.with_traceback())
|
||||
logger.error(
|
||||
f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera}"
|
||||
f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera_name}"
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"message": f"Trigger deleted successfully for {camera}:{name}",
|
||||
"message": f"Trigger deleted successfully for {camera_name}:{name}",
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
@ -1810,7 +1891,10 @@ def delete_trigger_embedding(
|
||||
@router.get(
|
||||
"/triggers/status/{camera_name}",
|
||||
response_model=dict,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
dependencies=[
|
||||
Depends(lambda: require_role(["admin"])),
|
||||
Depends(require_camera_access),
|
||||
],
|
||||
)
|
||||
def get_triggers_status(
|
||||
camera_name: str,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user