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>
58 lines
2.0 KiB
Python
58 lines
2.0 KiB
Python
"""
|
|
Tests for verify_browser_request — the session/proxy auth used by the
|
|
Wiki.js integration endpoints (no secret in the browser).
|
|
"""
|
|
import pytest
|
|
from types import SimpleNamespace
|
|
from fastapi import HTTPException
|
|
from starlette.requests import Request
|
|
|
|
from src.core.dependencies import verify_browser_request
|
|
|
|
|
|
def _request(headers: dict) -> Request:
|
|
raw = [(k.lower().encode(), v.encode()) for k, v in headers.items()]
|
|
return Request({"type": "http", "method": "POST", "path": "/ingest/page", "headers": raw})
|
|
|
|
|
|
SETTINGS = SimpleNamespace(library_api_key="server-secret-key")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_proxy_marker_with_authentik_identity_is_accepted():
|
|
req = _request({"X-Library-Desk-Proxy": "1", "X-Authentik-Email": "user@example.com"})
|
|
assert await verify_browser_request(req, SETTINGS) == "user@example.com"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_proxy_marker_on_lan_bypass_falls_back_to_lan():
|
|
# LAN bypass: proxy marker present, no Authentik identity headers.
|
|
req = _request({"X-Library-Desk-Proxy": "1"})
|
|
assert await verify_browser_request(req, SETTINGS) == "lan"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_valid_api_key_is_accepted_for_machine_callers():
|
|
req = _request({"Authorization": "Bearer server-secret-key"})
|
|
assert await verify_browser_request(req, SETTINGS) == "server-secret-key"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_marker_and_no_key_is_rejected():
|
|
with pytest.raises(HTTPException) as exc:
|
|
await verify_browser_request(_request({}), SETTINGS)
|
|
assert exc.value.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_forged_marker_value_is_rejected():
|
|
# Only the exact NPM-set value "1" is trusted.
|
|
with pytest.raises(HTTPException):
|
|
await verify_browser_request(_request({"X-Library-Desk-Proxy": "yes"}), SETTINGS)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wrong_api_key_is_rejected():
|
|
with pytest.raises(HTTPException):
|
|
await verify_browser_request(_request({"Authorization": "Bearer wrong"}), SETTINGS)
|