feat!: require explicit user on every tenant-data endpoint

Remove the implicit jpmschweitzer default tenant (DEFAULT_USER) from
src/core/multi_tenancy.py and every endpoint and request model that
inherited it (~40 endpoints across /query, /wiki, /vector, /graph,
/ingest, /volatile, /documents, /stats, /rag).

- Add validate_required_user() + RequiredUser pydantic type in
  multi_tenancy and a shared require_user FastAPI dependency
  (RequiredUserQuery) that rejects missing, empty, and whitespace-only
  users with 422, following the /maintenance/* pattern.
- Wiki page create / smart-create / dossier request models now require
  user (no fallback in wiki_service).
- /maintenance/cleanup/test-data derives the tenant from the page path
  instead of using the production tenant collection.
- Wiki.js change listener skips changes when no tenant user can be
  derived from the notification email instead of defaulting to the
  production tenant.
- Consolidation service internal helpers no longer default to the
  production tenant.
- Tool catalog marks user as required with honest descriptions.
- OpenAPI descriptions updated honestly; CHANGELOG notes that callers
  (tatlock, Scheduler ingest tasks) must now send explicit user.
- Offline tests: 422 coverage for query/body endpoints, required-user
  validator tests; updated legacy tests that assumed a default tenant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 11:06:11 +02:00
co-authored by Claude Fable 5
parent a66d801abd
commit 84e9185371
27 changed files with 481 additions and 153 deletions
+30 -1
View File
@@ -10,7 +10,7 @@ Provides FastAPI dependencies for service clients with:
from functools import lru_cache
from typing import Annotated
from fastapi import Depends
from fastapi import Depends, HTTPException, Query
import logging
import redis.asyncio as aioredis
@@ -38,6 +38,35 @@ logger = logging.getLogger(__name__)
SettingsDep = Annotated[Settings, Depends(get_settings)]
# Tenant user dependency
def require_user(
user: str = Query(
...,
description=(
"User identifier (tenant). Required — every operation is scoped to "
"this tenant's namespace (Qdrant collection, Neo4j labels, wiki path, "
"Redis keys). Requests without an explicit non-empty user are "
"rejected with 422. There is no default tenant."
),
)
) -> str:
"""
FastAPI dependency: required tenant user query parameter.
Rejects missing (FastAPI returns 422 automatically), empty, and
whitespace-only user values. Use via the RequiredUserQuery alias.
"""
from src.core.multi_tenancy import validate_required_user
try:
return validate_required_user(user)
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
RequiredUserQuery = Annotated[str, Depends(require_user)]
# Client factory functions with @lru_cache for singletons
@lru_cache
def get_neo4j_client() -> Neo4jClient:
+41 -2
View File
@@ -8,9 +8,9 @@ Provides utilities for user namespace management across:
"""
import re
from typing import Annotated
# Default user for all operations
DEFAULT_USER = "jpmschweitzer"
from pydantic import AfterValidator
def sanitize_user_id(user_id: str) -> str:
@@ -186,6 +186,45 @@ def validate_user_id(user_id: str) -> bool:
return True
def validate_required_user(user: str) -> str:
"""
Validate that a tenant user identifier is present and usable.
There is NO default tenant: every operation that touches tenant data
must receive an explicit user. Empty or whitespace-only values are
rejected, as are values that fail :func:`validate_user_id`.
Args:
user: Raw user identifier from a request
Returns:
The stripped user identifier
Raises:
ValueError: If the user is missing, blank, or invalid
Examples:
>>> validate_required_user("llm_tester")
'llm_tester'
>>> validate_required_user(" ")
Traceback (most recent call last):
...
ValueError: user is required and must be a non-empty, non-whitespace string
"""
if user is None or not str(user).strip():
raise ValueError(
"user is required and must be a non-empty, non-whitespace string"
)
stripped = str(user).strip()
if not validate_user_id(stripped):
raise ValueError(f"Invalid user identifier: {user!r}")
return stripped
# Pydantic annotated type for request models: a required, validated tenant user.
RequiredUser = Annotated[str, AfterValidator(validate_required_user)]
def is_path_in_user_namespace(path: str, user_id: str) -> bool:
"""
Check if a Wiki.js path belongs to user's namespace.