Compare commits

...

15 Commits

Author SHA1 Message Date
Mgeeeek 8e00815589 chore: drop test scaffold and CI workflow to keep PR minimal 2026-05-13 21:46:56 +05:30
Mgeeeek e2fd8dc7a8 chore: drop internal planning docs from public branch 2026-05-13 21:34:05 +05:30
Mgeeeek d19cde4a15 ci(server): add pytest workflow on PRs touching server/ 2026-05-13 21:11:20 +05:30
Mgeeeek 565a76960b docs(rest-api): document admin-only routes and clarify per-user-key scope 2026-05-13 21:10:23 +05:30
Mgeeeek d987243584 test(server): regression coverage for routes untouched by authz fix 2026-05-13 21:09:13 +05:30
Mgeeeek 4ca67801c0 feat(server): admin-gate unfiltered GET /memories branch 2026-05-13 21:06:53 +05:30
Mgeeeek 739402946c feat(server): admin-gate GET /requests 2026-05-13 21:03:38 +05:30
Mgeeeek 240168aefe feat(server): admin-gate GET /entities and DELETE /entities/{type}/{id} 2026-05-13 21:02:45 +05:30
Mgeeeek 07b9ad519d feat(server): admin-gate POST /reset 2026-05-13 21:02:02 +05:30
Mgeeeek 6ba334ef94 feat(server): admin-gate GET/POST /configure 2026-05-13 21:01:30 +05:30
Mgeeeek 8bd7b913c6 feat(server): add _ensure_admin helper and require_admin FastAPI dep 2026-05-13 20:52:41 +05:30
Mgeeeek 367512b525 test(server): document why StaticPool is required in test_engine fixture 2026-05-13 20:51:03 +05:30
Mgeeeek 79e04c56f6 test(server): add pytest scaffold with SQLite + mocked memory backend
Lays the groundwork for Tasks 2-10 of the REST API authorization fix plan.
Adds an in-memory SQLite test engine, a TestClient fixture that
short-circuits Memory.from_config() (pgvector is unavailable in tests),
and reusable fixtures for admin/member users, JWTs, the legacy
ADMIN_API_KEY env, and AUTH_DISABLED mode.
2026-05-13 20:47:17 +05:30
Mgeeeek 7b109d90de docs: add implementation plan for REST API authorization fix
Eleven-task TDD plan tracking the spec at
docs/superpowers/specs/2026-05-13-rest-api-authz-fix-design.md
(commit 8f285c75). Adds _ensure_admin helper, six admin-gated routes,
inside-route guard on GET /memories, server/ pytest scaffold (SQLite +
MagicMock), server-ci.yml workflow, and surgical docs edits. Each task
follows red/green/commit TDD; every gate covers the four supported
deployment modes (admin JWT, admin per-user key, ADMIN_API_KEY env,
AUTH_DISABLED).
2026-05-13 20:47:17 +05:30
Mgeeeek f687951b1a docs: add design spec for REST API authorization fix
Adds the design document for the B+ scope fix in response to the
2026-05-01 security report on per-user API key authorization. Spec
covers the require_admin helper, six admin-gated routes, the
inside-route guard on GET /memories, surgical docs edits, and a
minimal test scaffold for server/. owner_id work and multi-user invite
flow are explicitly deferred to a tracked follow-up issue.
2026-05-13 20:47:17 +05:30
5 changed files with 53 additions and 21 deletions
+9 -9
View File
@@ -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.
+24
View File
@@ -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
View File
@@ -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()
+3 -3
View File
@@ -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:
+3 -3
View File
@@ -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),
):