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:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user