Security fixes

auth.py

secrets.compare_digest replaces == for service key comparison (timing-safe)
Added require_service_key — bot-only endpoints (trip/event join/leave)
Added require_service_key_or_admin — node commands/config (bot via service key OR dashboard admin via Firebase)
Added _RateLimiter with three shared instances: trip_chat_limiter (20/5min per user), summarize_limiter (5/10min per incident), bootstrap_limiter (2/hr per system)
nodes.py

send_command and assign_system now require require_service_key_or_admin — the Discord bot can still call them via service key, but regular Firebase users are blocked
tokens.py

add_token, flush_tokens, set_preferred_system, delete_token all require require_admin_token
Token masking changed from token[:10] + "…" + token[-4:] to "•••" + token[-4:]
systems.py

All write endpoints (create, update, delete, ai-flags, ten-codes, vocabulary writes, bootstrap) now require require_admin_token
bootstrap_vocabulary also calls bootstrap_limiter.check(system_id)
incidents.py

POST /incidents/summarize (bulk) now requires require_admin_token
POST /incidents/{id}/summarize now calls summarize_limiter.check(incident_id)
trips.py

join_trip, leave_trip, join_event, leave_event require require_service_key — only the Discord bot can set Discord attendee identity
delete_trip, delete_event require require_service_key_or_admin
trip_chat rate-limited per caller UID, history stripped to user/assistant roles only, user message truncated to 2000 chars, Maps query strings capped at 200 chars
upload.py

Rejects files larger than settings.upload_max_bytes (default 100MB) with 413
storage.py

_safe_audio_filename() derives GCS object name from call_id + allowlisted extension, completely ignoring the client-supplied filename
config.py

Added upload_max_bytes: int = 100 * 1024 * 1024
Both Dockerfiles — python:3.14-slim → python:3.12-slim
This commit is contained in:
Logan
2026-06-21 13:40:08 -04:00
parent f0a0ea508a
commit 18d96193ab
11 changed files with 235 additions and 41 deletions
+11 -6
View File
@@ -1,9 +1,10 @@
import uuid
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional
from datetime import datetime, timezone
from app.internal import firestore as fstore
from app.internal.auth import require_admin_token
router = APIRouter(prefix="/tokens", tags=["tokens"])
@@ -22,13 +23,13 @@ async def list_tokens():
"""List all tokens. The actual token string is masked for safety."""
tokens = await fstore.collection_list("bot_tokens")
return [
{**t, "token": t["token"][:10] + "" + t["token"][-4:]}
{**t, "token": "•••" + t["token"][-4:]}
for t in tokens
]
@router.post("", status_code=201)
async def add_token(body: TokenCreate):
async def add_token(body: TokenCreate, _: dict = Depends(require_admin_token)):
token_id = str(uuid.uuid4())
doc = {
"token_id": token_id,
@@ -43,7 +44,7 @@ async def add_token(body: TokenCreate):
@router.post("/flush", status_code=200)
async def flush_tokens():
async def flush_tokens(_: dict = Depends(require_admin_token)):
"""Force-release all in-use tokens (admin utility — use when tokens get orphaned)."""
def _find():
from app.internal.firestore import db
@@ -61,7 +62,11 @@ async def flush_tokens():
@router.put("/{token_id}/prefer/{system_id}", status_code=200)
async def set_preferred_system(token_id: str, system_id: str):
async def set_preferred_system(
token_id: str,
system_id: str,
_: dict = Depends(require_admin_token),
):
"""
Mark this token as the preferred bot for a system.
When a discord_join is issued for any node in that system, this token
@@ -89,7 +94,7 @@ async def set_preferred_system(token_id: str, system_id: str):
@router.delete("/{token_id}", status_code=204)
async def delete_token(token_id: str):
async def delete_token(token_id: str, _: dict = Depends(require_admin_token)):
existing = await fstore.doc_get("bot_tokens", token_id)
if not existing:
raise HTTPException(404, "Token not found.")