Compare 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 |
|
| 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` |
|
| 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 |
|
| 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 |
|
| `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`
|
### Legacy `ADMIN_API_KEY`
|
||||||
|
|
||||||
@@ -246,8 +246,8 @@ The OSS REST server exposes the following endpoints. None use the `/v1/` prefix.
|
|||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `POST` | `/configure` | Set memory configuration. Rejects unbundled providers with a 400 |
|
| `POST` | `/configure` | **Admin only.** Set memory configuration. Rejects unbundled providers with a 400 |
|
||||||
| `GET` | `/configure` | Get the current memory configuration |
|
| `GET` | `/configure` | **Admin only.** Get the current memory configuration |
|
||||||
| `GET` | `/configure/providers` | List the LLM and embedder providers bundled in the container |
|
| `GET` | `/configure/providers` | List the LLM and embedder providers bundled in the container |
|
||||||
| `POST` | `/memories` | Create memories |
|
| `POST` | `/memories` | Create memories |
|
||||||
| `GET` | `/memories` | Get all memories (filter by `user_id`, `agent_id`, or `run_id`) |
|
| `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 |
|
| `DELETE` | `/memories` | Delete all memories for an identifier |
|
||||||
| `GET` | `/memories/{memory_id}/history` | Get memory history |
|
| `GET` | `/memories/{memory_id}/history` | Get memory history |
|
||||||
| `POST` | `/search` | Search memories |
|
| `POST` | `/search` | Search memories |
|
||||||
| `POST` | `/reset` | Reset all memories |
|
| `POST` | `/reset` | **Admin only.** Reset all memories |
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `GET` | `/auth/setup-status` | Returns `{needsSetup: bool}`. Open, no auth required |
|
| `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/login` | Exchange email and password for access and refresh JWTs |
|
||||||
| `POST` | `/auth/refresh` | Exchange a refresh token for a new access token |
|
| `POST` | `/auth/refresh` | Exchange a refresh token for a new access token |
|
||||||
| `GET` | `/auth/me` | Get the current authenticated user (JWT required) |
|
| `GET` | `/auth/me` | Get the current authenticated user (JWT required) |
|
||||||
@@ -285,14 +285,14 @@ All `/api-keys` endpoints require a JWT.
|
|||||||
|
|
||||||
| Method | Path | Description |
|
| 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
|
### Entities
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `GET` | `/entities` | Distinct `user_id` / `agent_id` / `run_id` values with memory counts |
|
| `GET` | `/entities` | **Admin only.** 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` |
|
| `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.
|
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
|
return default_user
|
||||||
raise HTTPException(status_code=401, detail="Authentication required.")
|
raise HTTPException(status_code=401, detail="Authentication required.")
|
||||||
return user
|
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 slowapi.errors import RateLimitExceeded
|
||||||
from sqlalchemy import func, select
|
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 (
|
from errors import (
|
||||||
UpstreamError,
|
UpstreamError,
|
||||||
install_request_id_logging,
|
install_request_id_logging,
|
||||||
@@ -301,7 +301,7 @@ async def log_requests(request: Request, call_next):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/configure", summary="Get current Mem0 configuration")
|
@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())
|
return _redact_config(get_current_config())
|
||||||
|
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ def list_bundled_providers(_auth=Depends(verify_auth)):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/configure", summary="Configure Mem0")
|
@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."""
|
"""Set memory configuration."""
|
||||||
_validate_bundled_providers(config)
|
_validate_bundled_providers(config)
|
||||||
update_config(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")
|
@app.get("/memories", summary="Get memories")
|
||||||
def get_all_memories(
|
def get_all_memories(
|
||||||
|
request: Request,
|
||||||
user_id: Optional[str] = None,
|
user_id: Optional[str] = None,
|
||||||
run_id: Optional[str] = None,
|
run_id: Optional[str] = None,
|
||||||
agent_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:
|
try:
|
||||||
if not any([user_id, run_id, agent_id]):
|
if not any([user_id, run_id, agent_id]):
|
||||||
|
_ensure_admin(request, user)
|
||||||
return _list_all_memories()
|
return _list_all_memories()
|
||||||
filters = {
|
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
|
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)
|
return get_memory_instance().get_all(filters=filters)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
raise upstream_error()
|
raise upstream_error()
|
||||||
|
|
||||||
@@ -473,7 +481,7 @@ def delete_all_memories(
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/reset", summary="Reset 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."""
|
"""Completely reset stored memories."""
|
||||||
try:
|
try:
|
||||||
get_memory_instance().reset()
|
get_memory_instance().reset()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any, Literal, Optional
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from auth import verify_auth
|
from auth import require_admin
|
||||||
from errors import upstream_error
|
from errors import upstream_error
|
||||||
from schemas import MessageResponse
|
from schemas import MessageResponse
|
||||||
from server_state import get_memory_instance
|
from server_state import get_memory_instance
|
||||||
@@ -42,7 +42,7 @@ def _parse_timestamp(value: Any) -> Optional[datetime]:
|
|||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[Entity])
|
@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(
|
buckets: dict[tuple[EntityType, str], dict[str, Any]] = defaultdict(
|
||||||
lambda: {"total_memories": 0, "created_at": None, "updated_at": None}
|
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)
|
@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:
|
try:
|
||||||
get_memory_instance().delete_all(**{TYPE_TO_FIELD[entity_type]: entity_id})
|
get_memory_instance().delete_all(**{TYPE_TO_FIELD[entity_type]: entity_id})
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from pydantic import BaseModel
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from auth import require_auth
|
from auth import require_admin
|
||||||
from db import get_db
|
from db import get_db
|
||||||
from models import RequestLog, User
|
from models import RequestLog
|
||||||
|
|
||||||
router = APIRouter(prefix="/requests", tags=["requests"])
|
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])
|
@router.get("", response_model=list[RequestLogItem])
|
||||||
def list_requests(
|
def list_requests(
|
||||||
user: User = Depends(require_auth),
|
_admin=Depends(require_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
limit: int = Query(default=50, ge=1, le=200),
|
limit: int = Query(default=50, ge=1, le=200),
|
||||||
):
|
):
|
||||||
|
|||||||
Reference in New Issue
Block a user