feat: add optional API key authentication to REST API server (#4442)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
utkarsh240799
Claude Opus 4.6
parent
f05e50d940
commit
2520edb404
@@ -10,3 +10,6 @@ POSTGRES_DB=
|
||||
POSTGRES_USER=
|
||||
POSTGRES_PASSWORD=
|
||||
POSTGRES_COLLECTION_NAME=
|
||||
|
||||
# Optional: set to enable API key authentication on all endpoints
|
||||
ADMIN_API_KEY=
|
||||
|
||||
+55
-10
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from fastapi.security import APIKeyHeader
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from mem0 import Memory
|
||||
@@ -14,6 +16,22 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
ADMIN_API_KEY = os.environ.get("ADMIN_API_KEY", "")
|
||||
|
||||
MIN_KEY_LENGTH = 16
|
||||
|
||||
if not ADMIN_API_KEY:
|
||||
logging.warning(
|
||||
"ADMIN_API_KEY not set - API endpoints are UNSECURED! "
|
||||
"Set ADMIN_API_KEY environment variable for production use."
|
||||
)
|
||||
else:
|
||||
if len(ADMIN_API_KEY) < MIN_KEY_LENGTH:
|
||||
logging.warning(
|
||||
"ADMIN_API_KEY is shorter than %d characters - consider using a longer key for production.",
|
||||
MIN_KEY_LENGTH,
|
||||
)
|
||||
logging.info("API key authentication enabled")
|
||||
|
||||
POSTGRES_HOST = os.environ.get("POSTGRES_HOST", "postgres")
|
||||
POSTGRES_PORT = os.environ.get("POSTGRES_PORT", "5432")
|
||||
@@ -60,10 +78,35 @@ MEMORY_INSTANCE = Memory.from_config(DEFAULT_CONFIG)
|
||||
|
||||
app = FastAPI(
|
||||
title="Mem0 REST APIs",
|
||||
description="A REST API for managing and searching memories for your AI Agents and Apps.",
|
||||
description=(
|
||||
"A REST API for managing and searching memories for your AI Agents and Apps.\n\n"
|
||||
"## Authentication\n"
|
||||
"When the ADMIN_API_KEY environment variable is set, all endpoints require "
|
||||
"the `X-API-Key` header for authentication."
|
||||
),
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||
|
||||
|
||||
async def verify_api_key(api_key: Optional[str] = Depends(api_key_header)):
|
||||
"""Validate the API key when ADMIN_API_KEY is configured. No-op otherwise."""
|
||||
if ADMIN_API_KEY:
|
||||
if api_key is None:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="X-API-Key header is required.",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
if not secrets.compare_digest(api_key, ADMIN_API_KEY):
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid API key.",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
return api_key
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
role: str = Field(..., description="Role of the message (user or assistant).")
|
||||
@@ -87,7 +130,7 @@ class SearchRequest(BaseModel):
|
||||
|
||||
|
||||
@app.post("/configure", summary="Configure Mem0")
|
||||
def set_config(config: Dict[str, Any]):
|
||||
def set_config(config: Dict[str, Any], _api_key: Optional[str] = Depends(verify_api_key)):
|
||||
"""Set memory configuration."""
|
||||
global MEMORY_INSTANCE
|
||||
MEMORY_INSTANCE = Memory.from_config(config)
|
||||
@@ -95,7 +138,7 @@ def set_config(config: Dict[str, Any]):
|
||||
|
||||
|
||||
@app.post("/memories", summary="Create memories")
|
||||
def add_memory(memory_create: MemoryCreate):
|
||||
def add_memory(memory_create: MemoryCreate, _api_key: Optional[str] = Depends(verify_api_key)):
|
||||
"""Store new memories."""
|
||||
if not any([memory_create.user_id, memory_create.agent_id, memory_create.run_id]):
|
||||
raise HTTPException(status_code=400, detail="At least one identifier (user_id, agent_id, run_id) is required.")
|
||||
@@ -114,6 +157,7 @@ def get_all_memories(
|
||||
user_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
_api_key: Optional[str] = Depends(verify_api_key),
|
||||
):
|
||||
"""Retrieve stored memories."""
|
||||
if not any([user_id, run_id, agent_id]):
|
||||
@@ -129,7 +173,7 @@ def get_all_memories(
|
||||
|
||||
|
||||
@app.get("/memories/{memory_id}", summary="Get a memory")
|
||||
def get_memory(memory_id: str):
|
||||
def get_memory(memory_id: str, _api_key: Optional[str] = Depends(verify_api_key)):
|
||||
"""Retrieve a specific memory by ID."""
|
||||
try:
|
||||
return MEMORY_INSTANCE.get(memory_id)
|
||||
@@ -139,7 +183,7 @@ def get_memory(memory_id: str):
|
||||
|
||||
|
||||
@app.post("/search", summary="Search memories")
|
||||
def search_memories(search_req: SearchRequest):
|
||||
def search_memories(search_req: SearchRequest, _api_key: Optional[str] = Depends(verify_api_key)):
|
||||
"""Search for memories based on a query."""
|
||||
try:
|
||||
params = {k: v for k, v in search_req.model_dump().items() if v is not None and k != "query"}
|
||||
@@ -150,7 +194,7 @@ def search_memories(search_req: SearchRequest):
|
||||
|
||||
|
||||
@app.put("/memories/{memory_id}", summary="Update a memory")
|
||||
def update_memory(memory_id: str, updated_memory: Dict[str, Any]):
|
||||
def update_memory(memory_id: str, updated_memory: Dict[str, Any], _api_key: Optional[str] = Depends(verify_api_key)):
|
||||
"""Update an existing memory with new content.
|
||||
|
||||
Args:
|
||||
@@ -168,7 +212,7 @@ def update_memory(memory_id: str, updated_memory: Dict[str, Any]):
|
||||
|
||||
|
||||
@app.get("/memories/{memory_id}/history", summary="Get memory history")
|
||||
def memory_history(memory_id: str):
|
||||
def memory_history(memory_id: str, _api_key: Optional[str] = Depends(verify_api_key)):
|
||||
"""Retrieve memory history."""
|
||||
try:
|
||||
return MEMORY_INSTANCE.history(memory_id=memory_id)
|
||||
@@ -178,7 +222,7 @@ def memory_history(memory_id: str):
|
||||
|
||||
|
||||
@app.delete("/memories/{memory_id}", summary="Delete a memory")
|
||||
def delete_memory(memory_id: str):
|
||||
def delete_memory(memory_id: str, _api_key: Optional[str] = Depends(verify_api_key)):
|
||||
"""Delete a specific memory by ID."""
|
||||
try:
|
||||
MEMORY_INSTANCE.delete(memory_id=memory_id)
|
||||
@@ -193,6 +237,7 @@ def delete_all_memories(
|
||||
user_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
_api_key: Optional[str] = Depends(verify_api_key),
|
||||
):
|
||||
"""Delete all memories for a given identifier."""
|
||||
if not any([user_id, run_id, agent_id]):
|
||||
@@ -209,7 +254,7 @@ def delete_all_memories(
|
||||
|
||||
|
||||
@app.post("/reset", summary="Reset all memories")
|
||||
def reset_memory():
|
||||
def reset_memory(_api_key: Optional[str] = Depends(verify_api_key)):
|
||||
"""Completely reset stored memories."""
|
||||
try:
|
||||
MEMORY_INSTANCE.reset()
|
||||
|
||||
Reference in New Issue
Block a user