Files
wehub-resource-sync 0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

231 lines
6.6 KiB
Python

"""
Chat Router - API endpoints for the CloudSwag chatbot
"""
from typing import List, Optional, Union
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, field_validator
from ..auth import UserContext, validate_token
from ..chat_handler import chat
from ..config import SECURITY_AUTH
# Note: validate_token now requires real JWT tokens (no more mock mode)
router = APIRouter(prefix="/chat", tags=["chat"])
# In-memory session storage (for demo - use Redis in production)
_sessions: dict[str, list] = {}
class ChatRequest(BaseModel):
"""Request body for chat messages."""
message: Union[
str, List[dict]
] # Accept string or array of messages (for promptfoo multi-turn)
token: str # JWT token or demo user name
session_id: Optional[str] = None # Optional session ID for conversation continuity
# DEBUG PARAMETER - Should have been removed before production!
# At SECURITY_AUTH level 1-2, this allows overriding the user context
user_id_override: Optional[str] = None # For "testing" - VULNERABILITY!
@field_validator("message", mode="before")
@classmethod
def extract_message_content(cls, v):
"""Extract string content from array format if needed."""
if isinstance(v, list):
# promptfoo multi-turn format: [{"role": "user", "content": "..."}]
# Extract the last user message content
for msg in reversed(v):
if isinstance(msg, dict) and msg.get("role") == "user":
return msg.get("content", "")
# Fallback: concatenate all content
return " ".join(
msg.get("content", "") if isinstance(msg, dict) else str(msg)
for msg in v
)
return v
class ChatResponse(BaseModel):
"""Response from chat endpoint."""
response: str
user_id: str
user_name: str
session_id: str
class ToolCall(BaseModel):
"""Tool call information for debugging."""
tool: str
input: dict
@router.post("/", response_model=ChatResponse)
async def send_message(request: ChatRequest):
"""
Send a message to the CloudSwag bot.
The token can be:
- A demo user name (alice, bob, charlie, diana, eve) in mock mode
- A valid JWT token
Example:
POST /chat/
{
"message": "What are my orders?",
"token": "alice"
}
"""
# Validate token and get user context
try:
user = validate_token(request.token)
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=401, detail=f"Authentication failed: {str(e)}")
# VULNERABILITY: At security levels 1-2, allow user_id_override
# This was a "debug feature" that should have been removed!
# Level 1 & 2: Override accepted without validation
# Level 3: Override ignored (secure)
if request.user_id_override and SECURITY_AUTH < 3:
# Override the user_id in the context - DANGEROUS!
# The attacker can impersonate any user by providing their valid token
# but setting user_id_override to a different user
user = UserContext(
user_id=request.user_id_override,
name=f"Override ({request.user_id_override})",
email=None,
department=None,
office_location=None,
)
# Get or create session
session_id = request.session_id or f"{user.user_id}_default"
if session_id not in _sessions:
_sessions[session_id] = []
conversation_history = _sessions[session_id]
# Process message
try:
response_text, updated_history = await chat(
message=request.message,
user=user,
conversation_history=conversation_history,
)
# Update session
_sessions[session_id] = updated_history
return ChatResponse(
response=response_text,
user_id=user.user_id,
user_name=user.name,
session_id=session_id,
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Chat error: {str(e)}")
@router.delete("/session/{session_id}")
async def clear_session(session_id: str, token: str):
"""
Clear a chat session's history.
Args:
session_id: The session ID to clear
token: User's authentication token
"""
# Validate token
try:
user = validate_token(token)
except HTTPException as e:
raise e
# Check session belongs to user
if not session_id.startswith(user.user_id):
raise HTTPException(
status_code=403, detail="Cannot clear another user's session"
)
if session_id in _sessions:
del _sessions[session_id]
return {"message": f"Session {session_id} cleared"}
return {"message": "Session not found (already cleared)"}
@router.get("/session/{session_id}/history")
async def get_session_history(session_id: str, token: str):
"""
Get conversation history for a session.
Args:
session_id: The session ID
token: User's authentication token
"""
# Validate token
try:
user = validate_token(token)
except HTTPException as e:
raise e
# Check session belongs to user
if not session_id.startswith(user.user_id):
raise HTTPException(
status_code=403, detail="Cannot view another user's session"
)
history = _sessions.get(session_id, [])
# Simplify history for display
simplified = []
for msg in history:
role = msg.get("role", "unknown")
content = msg.get("content", "")
if isinstance(content, str):
simplified.append({"role": role, "content": content[:500]})
elif isinstance(content, list):
# Tool interactions
simplified.append({"role": role, "content": "[tool interaction]"})
return {
"session_id": session_id,
"message_count": len(history),
"history": simplified,
}
@router.get("/sessions")
async def list_user_sessions(token: str):
"""
List all sessions for the authenticated user.
Args:
token: User's authentication token
"""
# Validate token
try:
user = validate_token(token)
except HTTPException as e:
raise e
# Find user's sessions
user_sessions = []
for session_id, history in _sessions.items():
if session_id.startswith(user.user_id):
user_sessions.append(
{"session_id": session_id, "message_count": len(history)}
)
return {"user_id": user.user_id, "sessions": user_sessions}