mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-06 21:22:23 +01:00
fix(security): constrain API token chat authority
This commit is contained in:
+36
-5
@@ -40,7 +40,12 @@ from src.foreground_model_routing import (
|
||||
from src.session_search import search_session_messages
|
||||
from src.prompt_security import untrusted_context_message
|
||||
from core.exceptions import SessionNotFoundError
|
||||
from src.auth_helpers import effective_user, get_current_user
|
||||
from src.auth_helpers import (
|
||||
effective_user,
|
||||
enforce_api_token_chat_controls,
|
||||
get_current_user,
|
||||
require_chat_scope,
|
||||
)
|
||||
from routes.session_routes import _verify_session_owner
|
||||
from routes.document_helpers import _owner_session_filter
|
||||
from core.database import SessionLocal, get_session_mode, set_session_mode
|
||||
@@ -113,6 +118,7 @@ def _mark_tool_approval_resolved(sess, approval_id: Any, decision: Any) -> bool:
|
||||
if str(ask_user.get("approval_id") or "") != approval_key:
|
||||
continue
|
||||
ask_user["resolved"] = normalized_decision
|
||||
ask_user["approved_by_interactive_session"] = True
|
||||
message_id = metadata.get("_db_id")
|
||||
resolved_metadata = {
|
||||
key: value for key, value in metadata.items() if key != "_db_id"
|
||||
@@ -737,6 +743,7 @@ def setup_chat_routes(
|
||||
# ------------------------------------------------------------------ #
|
||||
@router.post("/api/chat", response_model=Dict[str, Any])
|
||||
async def chat_endpoint(request: Request, chat_request: ChatRequest) -> Dict[str, Any]:
|
||||
require_chat_scope(request)
|
||||
_set_user_time_from_request(request)
|
||||
|
||||
message = chat_request.message
|
||||
@@ -776,7 +783,10 @@ def setup_chat_routes(
|
||||
_enforce_chat_privileges(request, sess)
|
||||
|
||||
tool_policy = build_effective_tool_policy(last_user_message=message)
|
||||
allow_tool_preprocessing = not tool_policy.block_all_tool_calls
|
||||
allow_tool_preprocessing = (
|
||||
not getattr(request.state, "api_token", False)
|
||||
and not tool_policy.block_all_tool_calls
|
||||
)
|
||||
|
||||
# Inline memory command
|
||||
memory_response = None
|
||||
@@ -927,6 +937,7 @@ def setup_chat_routes(
|
||||
# ------------------------------------------------------------------ #
|
||||
@router.post("/api/chat_stream")
|
||||
async def chat_stream(request: Request) -> StreamingResponse:
|
||||
require_chat_scope(request)
|
||||
body = None
|
||||
try:
|
||||
if request.headers.get("content-type", "").startswith("application/json"):
|
||||
@@ -964,7 +975,7 @@ def setup_chat_routes(
|
||||
compare_mode = str(form_data.get("compare_mode", "")).lower() == "true"
|
||||
incognito = str(form_data.get("incognito", "")).lower() == "true"
|
||||
plan_mode = str(form_data.get("plan_mode") or (body or {}).get("plan_mode") or "").lower() == "true"
|
||||
chat_mode = str(form_data.get("mode", "")).lower() # 'chat' or 'agent'
|
||||
chat_mode = str(form_data.get("mode") or (body or {}).get("mode") or "chat").lower()
|
||||
tool_approval_id = (
|
||||
form_data.get("tool_approval_id")
|
||||
or (body or {}).get("tool_approval_id")
|
||||
@@ -978,6 +989,13 @@ def setup_chat_routes(
|
||||
retired_tool_approval_taint = False
|
||||
external_untrusted_context_seen = False
|
||||
tool_approval_continuation = False
|
||||
api_token_request = enforce_api_token_chat_controls(
|
||||
request,
|
||||
mode=chat_mode,
|
||||
plan_mode=plan_mode,
|
||||
approval_id=tool_approval_id,
|
||||
allow_bash=allow_bash,
|
||||
)
|
||||
# Workspace: confine the agent's file/shell tools to this folder.
|
||||
workspace, workspace_rejected = _resolve_request_workspace(
|
||||
request, form_data.get("workspace")
|
||||
@@ -1263,6 +1281,16 @@ def setup_chat_routes(
|
||||
except (ValueError, ValidationError):
|
||||
raise HTTPException(400, "Invalid request parameters")
|
||||
|
||||
# API tokens are integration credentials, not interactive humans. They
|
||||
# may stream ordinary chat responses, but intent detection, contextual
|
||||
# follow-ups, and workspace parsing must never promote them into the
|
||||
# agent/research execution branch.
|
||||
if api_token_request:
|
||||
chat_mode = "chat"
|
||||
auto_escalated = False
|
||||
workspace = None
|
||||
use_research = "false"
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Privilege gates that must fire BEFORE any LLM work / token spend.
|
||||
# 1. allowed_models — reject if session.model isn't in the user's
|
||||
@@ -1284,7 +1312,7 @@ def setup_chat_routes(
|
||||
not tool_approval_continuation
|
||||
and str(use_research).lower() == "true"
|
||||
)
|
||||
if not do_research and not tool_approval_continuation:
|
||||
if not api_token_request and not do_research and not tool_approval_continuation:
|
||||
if get_session_mode(session) == 'research_pending':
|
||||
do_research = True
|
||||
logger.info(f"Session {session} in research_pending — auto-triggering research")
|
||||
@@ -1311,7 +1339,10 @@ def setup_chat_routes(
|
||||
pre_context_tool_policy = build_effective_tool_policy(
|
||||
last_user_message=message,
|
||||
)
|
||||
allow_tool_preprocessing = not pre_context_tool_policy.block_all_tool_calls
|
||||
allow_tool_preprocessing = (
|
||||
not api_token_request
|
||||
and not pre_context_tool_policy.block_all_tool_calls
|
||||
)
|
||||
foreground_policy = resolve_foreground_model_policy(
|
||||
owner=owner,
|
||||
allowed_models=_allowed_models_for_request(request),
|
||||
|
||||
@@ -6,11 +6,12 @@ import logging
|
||||
import re
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException
|
||||
|
||||
from core.models import ChatMessage
|
||||
from core.database import SessionLocal, ChatMessage as DbChatMessage, Session as DbSession
|
||||
from src.auth_helpers import effective_user
|
||||
from src.auth_helpers import effective_user, require_chat_scope
|
||||
from src.message_metadata import sanitize_client_message_metadata
|
||||
from src.topic_analyzer import analyze_topics
|
||||
from src.upload_handler import reserve_message_upload_references
|
||||
from routes.session_routes import (
|
||||
@@ -101,7 +102,7 @@ def _merge_continue_rows_to_delete(db_messages, db1, db2):
|
||||
|
||||
|
||||
def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
router = APIRouter(tags=["history"])
|
||||
router = APIRouter(tags=["history"], dependencies=[Depends(require_chat_scope)])
|
||||
|
||||
def _reserve_message_uploads(
|
||||
request: Request,
|
||||
@@ -268,7 +269,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
content = body.get("content", "")
|
||||
if not content:
|
||||
raise HTTPException(400, "content is required")
|
||||
metadata = body.get("metadata")
|
||||
metadata = sanitize_client_message_metadata(body.get("metadata"))
|
||||
_reserve_message_uploads(request, content, metadata)
|
||||
msg = ChatMessage(role=role, content=content, metadata=metadata)
|
||||
session_manager.add_message(session_id, msg)
|
||||
@@ -455,7 +456,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
_verify_session_owner(request, session_id)
|
||||
try:
|
||||
body = await request.json()
|
||||
meta_update = body.get("metadata", {})
|
||||
meta_update = sanitize_client_message_metadata(body.get("metadata", {}))
|
||||
session = session_manager.get_session(session_id)
|
||||
|
||||
# Update in-memory
|
||||
|
||||
@@ -4,14 +4,15 @@ import html
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Form, HTTPException, Response, Request
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Response, Request
|
||||
import logging
|
||||
|
||||
from core.session_manager import SessionManager
|
||||
from core.models import ChatMessage
|
||||
from src.request_models import SessionResponse
|
||||
from core.database import Session as DbSession, SessionLocal, Document, GalleryImage, utcnow_naive
|
||||
from src.auth_helpers import effective_user, _auth_disabled, owner_filter
|
||||
from src.auth_helpers import effective_user, _auth_disabled, owner_filter, require_chat_scope
|
||||
from src.message_metadata import sanitize_client_message_metadata
|
||||
from src.session_image_cleanup import _generated_image_path_for_cleanup, session_image_refs
|
||||
from src.session_actions import is_session_recently_active
|
||||
from src.upload_handler import reserve_message_upload_references
|
||||
@@ -124,7 +125,11 @@ def _verify_session_owner(request: Request, session_id: str, session_manager=Non
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["sessions"])
|
||||
router = APIRouter(
|
||||
prefix="/api",
|
||||
tags=["sessions"],
|
||||
dependencies=[Depends(require_chat_scope)],
|
||||
)
|
||||
|
||||
def _current_user_is_admin(request: Request, user: str | None) -> bool:
|
||||
if not user:
|
||||
@@ -554,7 +559,7 @@ def setup_session_routes(
|
||||
upload_handler,
|
||||
owner,
|
||||
message.get("content"),
|
||||
message.get("metadata"),
|
||||
sanitize_client_message_metadata(message.get("metadata")),
|
||||
)
|
||||
if missing_id:
|
||||
raise HTTPException(
|
||||
@@ -564,7 +569,11 @@ def setup_session_routes(
|
||||
except (AttributeError, TypeError, ValueError) as exc:
|
||||
raise HTTPException(400, "Invalid message attachment metadata") from exc
|
||||
for m in messages:
|
||||
sess.add_message(ChatMessage(m["role"], m["content"], metadata=m.get("metadata")))
|
||||
sess.add_message(ChatMessage(
|
||||
m["role"],
|
||||
m["content"],
|
||||
metadata=sanitize_client_message_metadata(m.get("metadata")),
|
||||
))
|
||||
session_manager.save_sessions()
|
||||
return {"ok": True, "count": len(messages)}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Request, File, UploadFile, HTTPException, Form
|
||||
from fastapi import APIRouter, Depends, Request, File, UploadFile, HTTPException, Form
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
from core.middleware import require_admin
|
||||
@@ -21,7 +21,7 @@ from core.database import (
|
||||
Note,
|
||||
Session as DbSession,
|
||||
)
|
||||
from src.auth_helpers import effective_user
|
||||
from src.auth_helpers import effective_user, require_chat_scope
|
||||
from src.attachment_refs import attachment_refs_from_metadata
|
||||
from src.constants import GENERATED_IMAGES_DIR
|
||||
from src.upload_handler import (
|
||||
@@ -32,7 +32,11 @@ from src.upload_handler import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/upload", tags=["upload"])
|
||||
router = APIRouter(
|
||||
prefix="/api/upload",
|
||||
tags=["upload"],
|
||||
dependencies=[Depends(require_chat_scope)],
|
||||
)
|
||||
UPLOAD_RESPONSE_HEADERS = {"X-Content-Type-Options": "nosniff"}
|
||||
|
||||
def _upload_ids_from_persisted_text(value: object) -> set[str]:
|
||||
|
||||
Reference in New Issue
Block a user