The wikijs-integration.js embedded a full-privilege API key that was served to every wiki visitor — it unlocked all 66 authenticated endpoints, including page/vector deletes and index purges. That key has been rotated out of service. The two browser endpoints (/ingest/page, /entity-linking/link-page) now authenticate via the NPM /library-desk/ proxy location instead of a key: Authentik forward-auth for external users, LAN bypass for internal, verified by a trusted proxy marker header. This is safe because library-desk binds loopback-only, so NPM is the sole path that can set that header. The browser holds no secret; the script calls same-origin with credentials. Machine callers (the Scheduler) keep the Bearer key on the container-network endpoints. verify_api_key now compares in constant time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
120 lines
4.5 KiB
Python
120 lines
4.5 KiB
Python
"""
|
|
Offline unit tests: every tenant-data endpoint must REQUIRE an explicit user.
|
|
|
|
A request without a user (query param or body field) must be rejected with
|
|
422 before any service is touched. Empty/whitespace users are also rejected.
|
|
|
|
No external services are contacted: validation failures short-circuit the
|
|
request before the endpoint body executes.
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.main import app
|
|
from src.core.dependencies import verify_api_key, verify_browser_request
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def client():
|
|
"""TestClient with auth stubbed out (no lifespan startup)."""
|
|
app.dependency_overrides[verify_api_key] = lambda: "test-key"
|
|
app.dependency_overrides[verify_browser_request] = lambda: "test-user"
|
|
try:
|
|
# No context manager: startup/lifespan events are NOT triggered,
|
|
# so no connections to external services are attempted.
|
|
yield TestClient(app)
|
|
finally:
|
|
app.dependency_overrides.pop(verify_api_key, None)
|
|
app.dependency_overrides.pop(verify_browser_request, None)
|
|
|
|
|
|
QUERY_PARAM_ENDPOINTS = [
|
|
("GET", "/stats"),
|
|
("POST", "/query/semantic?query=test"),
|
|
("POST", "/query/graph?query=MATCH%20(n)%20RETURN%20n"),
|
|
("GET", "/wiki/pages"),
|
|
("GET", "/wiki/pages/1"),
|
|
("PUT", "/wiki/pages/1"),
|
|
("DELETE", "/wiki/pages/1"),
|
|
("GET", "/wiki/search?q=test"),
|
|
("GET", "/wiki/dossiers"),
|
|
("POST", "/vector/update-from-page/1"),
|
|
("DELETE", "/vector/pages/1"),
|
|
("GET", "/graph/nodes"),
|
|
("POST", "/graph/update-from-page/1"),
|
|
("POST", "/graph/generate-entity-pages"),
|
|
("POST", "/ingest/all"),
|
|
("GET", "/volatile/stats"),
|
|
("GET", "/volatile/search?q=test"),
|
|
("POST", "/volatile/store?namespace=weather&key=test"),
|
|
("GET", "/volatile/weather/rotterdam"),
|
|
("DELETE", "/volatile/weather/rotterdam"),
|
|
("POST", "/documents/webhook-simple?doc_url=http://x/documents/1/"),
|
|
]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestUserQueryParamRequired:
|
|
"""Endpoints with a user query parameter must 422 without it."""
|
|
|
|
@pytest.mark.parametrize("method,path", QUERY_PARAM_ENDPOINTS)
|
|
def test_missing_user_is_422(self, client, method, path):
|
|
response = client.request(method, path, json={})
|
|
assert response.status_code == 422, (
|
|
f"{method} {path} returned {response.status_code}, expected 422"
|
|
)
|
|
|
|
@pytest.mark.parametrize("blank", ["", " ", "%20%20"])
|
|
def test_blank_user_is_422(self, client, blank):
|
|
response = client.get(f"/wiki/pages?user={blank}")
|
|
assert response.status_code == 422
|
|
|
|
def test_hybrid_query_missing_user_is_422(self, client):
|
|
response = client.post("/query/hybrid", json={"query": "test"})
|
|
assert response.status_code == 422
|
|
|
|
def test_hybrid_query_whitespace_user_is_422(self, client):
|
|
response = client.post("/query/hybrid?user=%20", json={"query": "test"})
|
|
assert response.status_code == 422
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestUserBodyFieldRequired:
|
|
"""Request models with a user field must reject missing/blank values."""
|
|
|
|
def test_ingest_page_missing_user_is_422(self, client):
|
|
response = client.post("/ingest/page", json={"page_id": 1})
|
|
assert response.status_code == 422
|
|
|
|
def test_ingest_page_blank_user_is_422(self, client):
|
|
response = client.post("/ingest/page", json={"page_id": 1, "user": " "})
|
|
assert response.status_code == 422
|
|
|
|
def test_ingest_batch_missing_user_is_422(self, client):
|
|
response = client.post("/ingest/batch", json={"page_ids": [1]})
|
|
assert response.status_code == 422
|
|
|
|
def test_wiki_create_page_missing_user_is_422(self, client):
|
|
response = client.post(
|
|
"/wiki/pages",
|
|
json={"title": "T", "path": "/t", "content": "c"},
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
def test_wiki_smart_create_missing_user_is_422(self, client):
|
|
response = client.post("/wiki/pages/smart-create", json={"topic": "T"})
|
|
assert response.status_code == 422
|
|
|
|
def test_vector_search_missing_user_is_422(self, client):
|
|
response = client.post("/vector/search", json={"query": "test"})
|
|
assert response.status_code == 422
|
|
|
|
def test_graph_query_missing_user_is_422(self, client):
|
|
response = client.post("/graph/query", json={"query": "MATCH (n) RETURN n"})
|
|
assert response.status_code == 422
|
|
|
|
def test_rag_search_missing_user_is_422(self, client):
|
|
response = client.post("/rag/search", json={"query": "test"})
|
|
assert response.status_code == 422
|