Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e00815589 | |||
| e2fd8dc7a8 | |||
| d19cde4a15 | |||
| 565a76960b | |||
| d987243584 | |||
| 4ca67801c0 | |||
| 739402946c | |||
| 240168aefe | |||
| 07b9ad519d | |||
| 6ba334ef94 | |||
| 8bd7b913c6 | |||
| 367512b525 | |||
| 79e04c56f6 | |||
| 7b109d90de | |||
| f687951b1a |
@@ -131,7 +131,7 @@ Auth is on by default. Protected endpoints require either a JWT (from the dashbo
|
||||
| Mode | How to send it | When to use it |
|
||||
|---|---|---|
|
||||
| Bearer JWT | `Authorization: Bearer <access_token>` | Dashboard sessions; tokens come from `POST /auth/login` and refresh via `POST /auth/refresh` |
|
||||
| Per-user API key | `X-API-Key: m0sk_...` | Programmatic access scoped to a single dashboard user |
|
||||
| Per-user API key | `X-API-Key: m0sk_...` | Programmatic access tied to the single admin user today. Per-user scoping arrives with the upcoming multi-user invite flow |
|
||||
| Legacy `ADMIN_API_KEY` | `X-API-Key: <env value>` | Back-compat for deployments that set the `ADMIN_API_KEY` env var |
|
||||
| `AUTH_DISABLED=true` | — | Local development only; bypasses auth entirely |
|
||||
|
||||
@@ -182,7 +182,7 @@ curl -X POST http://localhost:8888/memories \
|
||||
}'
|
||||
```
|
||||
|
||||
Per-user keys inherit the creating user's scope. List or revoke them via `GET /api-keys` and `DELETE /api-keys/{id}`.
|
||||
List or revoke per-user keys via `GET /api-keys` and `DELETE /api-keys/{id}`. The dashboard user issued the key remains its owner; admin-only routes such as `/configure`, `/reset`, `/entities`, and `/requests` require the issuing user to have the admin role.
|
||||
|
||||
### Legacy `ADMIN_API_KEY`
|
||||
|
||||
@@ -246,8 +246,8 @@ The OSS REST server exposes the following endpoints. None use the `/v1/` prefix.
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `POST` | `/configure` | Set memory configuration. Rejects unbundled providers with a 400 |
|
||||
| `GET` | `/configure` | Get the current memory configuration |
|
||||
| `POST` | `/configure` | **Admin only.** Set memory configuration. Rejects unbundled providers with a 400 |
|
||||
| `GET` | `/configure` | **Admin only.** Get the current memory configuration |
|
||||
| `GET` | `/configure/providers` | List the LLM and embedder providers bundled in the container |
|
||||
| `POST` | `/memories` | Create memories |
|
||||
| `GET` | `/memories` | Get all memories (filter by `user_id`, `agent_id`, or `run_id`) |
|
||||
@@ -257,14 +257,14 @@ The OSS REST server exposes the following endpoints. None use the `/v1/` prefix.
|
||||
| `DELETE` | `/memories` | Delete all memories for an identifier |
|
||||
| `GET` | `/memories/{memory_id}/history` | Get memory history |
|
||||
| `POST` | `/search` | Search memories |
|
||||
| `POST` | `/reset` | Reset all memories |
|
||||
| `POST` | `/reset` | **Admin only.** Reset all memories |
|
||||
|
||||
### Authentication
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/auth/setup-status` | Returns `{needsSetup: bool}`. Open, no auth required |
|
||||
| `POST` | `/auth/register` | Register the first admin. Registration closes after the first admin is created; additional accounts are provisioned by the existing admin. |
|
||||
| `POST` | `/auth/register` | Register the first admin. Registration closes after the first admin is created; additional accounts will be provisioned by the existing admin (multi-user invite flow coming soon). |
|
||||
| `POST` | `/auth/login` | Exchange email and password for access and refresh JWTs |
|
||||
| `POST` | `/auth/refresh` | Exchange a refresh token for a new access token |
|
||||
| `GET` | `/auth/me` | Get the current authenticated user (JWT required) |
|
||||
@@ -285,14 +285,14 @@ All `/api-keys` endpoints require a JWT.
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/requests?limit=N` | Recent API call log (JWT or admin key) |
|
||||
| `GET` | `/requests?limit=N` | **Admin only.** Recent API call log |
|
||||
|
||||
### Entities
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/entities` | Distinct `user_id` / `agent_id` / `run_id` values with memory counts |
|
||||
| `DELETE` | `/entities/{entity_type}/{entity_id}` | Cascade-delete all memories for an entity; `entity_type` is `user`, `agent`, or `run` |
|
||||
| `GET` | `/entities` | **Admin only.** Distinct `user_id` / `agent_id` / `run_id` values with memory counts |
|
||||
| `DELETE` | `/entities/{entity_type}/{entity_id}` | **Admin only.** Cascade-delete all memories for an entity; `entity_type` is `user`, `agent`, or `run` |
|
||||
|
||||
The `/auth/*`, `/api-keys`, `/requests`, and `/entities` routes are new to the self-hosted server and primarily back the dashboard, but you can call them directly from your own tooling.
|
||||
|
||||
|
||||
@@ -184,3 +184,27 @@ async def require_auth(
|
||||
return default_user
|
||||
raise HTTPException(status_code=401, detail="Authentication required.")
|
||||
return user
|
||||
|
||||
|
||||
def _ensure_admin(request: Request, user: User | None) -> None:
|
||||
"""Single source of truth for admin gating.
|
||||
|
||||
Allows: legacy ADMIN_API_KEY env (X-API-Key header), AUTH_DISABLED=true,
|
||||
or a registered User with role == 'admin'. Raises 403 otherwise.
|
||||
|
||||
Pure function — reusable from FastAPI deps and inline route guards.
|
||||
"""
|
||||
auth_type = getattr(request.state, "auth_type", "none")
|
||||
if auth_type in {"admin_api_key", "disabled"}:
|
||||
return
|
||||
if user is not None and user.role == "admin":
|
||||
return
|
||||
raise HTTPException(status_code=403, detail="Admin role required.")
|
||||
|
||||
|
||||
async def require_admin(
|
||||
request: Request,
|
||||
user: User | None = Depends(verify_auth),
|
||||
) -> None:
|
||||
"""FastAPI dependency. Admin-only gate; returns nothing."""
|
||||
_ensure_admin(request, user)
|
||||
|
||||
+14
-6
@@ -13,7 +13,7 @@ from slowapi import _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from auth import ADMIN_API_KEY, AUTH_DISABLED, JWT_SECRET, verify_auth
|
||||
from auth import ADMIN_API_KEY, AUTH_DISABLED, JWT_SECRET, _ensure_admin, require_admin, verify_auth
|
||||
from errors import (
|
||||
UpstreamError,
|
||||
install_request_id_logging,
|
||||
@@ -301,7 +301,7 @@ async def log_requests(request: Request, call_next):
|
||||
|
||||
|
||||
@app.get("/configure", summary="Get current Mem0 configuration")
|
||||
def get_config(_auth=Depends(verify_auth)):
|
||||
def get_config(_admin=Depends(require_admin)):
|
||||
return _redact_config(get_current_config())
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ def list_bundled_providers(_auth=Depends(verify_auth)):
|
||||
|
||||
|
||||
@app.post("/configure", summary="Configure Mem0")
|
||||
def set_config(config: Dict[str, Any], _auth=Depends(verify_auth)):
|
||||
def set_config(config: Dict[str, Any], _admin=Depends(require_admin)):
|
||||
"""Set memory configuration."""
|
||||
_validate_bundled_providers(config)
|
||||
update_config(config)
|
||||
@@ -386,19 +386,27 @@ def _list_all_memories(limit: int = ALL_MEMORIES_LIMIT) -> Dict[str, Any]:
|
||||
|
||||
@app.get("/memories", summary="Get memories")
|
||||
def get_all_memories(
|
||||
request: Request,
|
||||
user_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
_auth=Depends(verify_auth),
|
||||
user: User | None = Depends(verify_auth),
|
||||
):
|
||||
"""Retrieve stored memories. Lists all memories when no identifier is provided."""
|
||||
"""Retrieve stored memories. Lists all memories when no identifier is provided.
|
||||
|
||||
Note: the unfiltered listing branch is admin-only — it would otherwise leak
|
||||
every payload in the vector store across tenants once multi-user lands.
|
||||
Filtered queries remain available to any authenticated caller."""
|
||||
try:
|
||||
if not any([user_id, run_id, agent_id]):
|
||||
_ensure_admin(request, user)
|
||||
return _list_all_memories()
|
||||
filters = {
|
||||
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v is not None
|
||||
}
|
||||
return get_memory_instance().get_all(filters=filters)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
raise upstream_error()
|
||||
|
||||
@@ -473,7 +481,7 @@ def delete_all_memories(
|
||||
|
||||
|
||||
@app.post("/reset", summary="Reset all memories")
|
||||
def reset_memory(_auth=Depends(verify_auth)):
|
||||
def reset_memory(_admin=Depends(require_admin)):
|
||||
"""Completely reset stored memories."""
|
||||
try:
|
||||
get_memory_instance().reset()
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, Literal, Optional
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
|
||||
from auth import verify_auth
|
||||
from auth import require_admin
|
||||
from errors import upstream_error
|
||||
from schemas import MessageResponse
|
||||
from server_state import get_memory_instance
|
||||
@@ -42,7 +42,7 @@ def _parse_timestamp(value: Any) -> Optional[datetime]:
|
||||
|
||||
|
||||
@router.get("", response_model=list[Entity])
|
||||
def list_entities(_auth=Depends(verify_auth)):
|
||||
def list_entities(_admin=Depends(require_admin)):
|
||||
buckets: dict[tuple[EntityType, str], dict[str, Any]] = defaultdict(
|
||||
lambda: {"total_memories": 0, "created_at": None, "updated_at": None}
|
||||
)
|
||||
@@ -69,7 +69,7 @@ def list_entities(_auth=Depends(verify_auth)):
|
||||
|
||||
|
||||
@router.delete("/{entity_type}/{entity_id}", response_model=MessageResponse)
|
||||
def delete_entity(entity_type: EntityType, entity_id: str, _auth=Depends(verify_auth)):
|
||||
def delete_entity(entity_type: EntityType, entity_id: str, _admin=Depends(require_admin)):
|
||||
try:
|
||||
get_memory_instance().delete_all(**{TYPE_TO_FIELD[entity_type]: entity_id})
|
||||
except Exception:
|
||||
|
||||
@@ -6,9 +6,9 @@ from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from auth import require_auth
|
||||
from auth import require_admin
|
||||
from db import get_db
|
||||
from models import RequestLog, User
|
||||
from models import RequestLog
|
||||
|
||||
router = APIRouter(prefix="/requests", tags=["requests"])
|
||||
|
||||
@@ -30,7 +30,7 @@ API_KEY_AUTH_TYPES = ("api_key", "admin_api_key")
|
||||
|
||||
@router.get("", response_model=list[RequestLogItem])
|
||||
def list_requests(
|
||||
user: User = Depends(require_auth),
|
||||
_admin=Depends(require_admin),
|
||||
db: Session = Depends(get_db),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user