mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-01 06:18:21 +00:00
fix(security): allow disabling API docs in production via GATEWAY_ENABLE_DOCS (#2651)
* fix(security): allow disabling API docs in production via GATEWAY_ENABLE_DOCS Expose /docs, /redoc, and /openapi.json only when GATEWAY_ENABLE_DOCS=true (default). Setting GATEWAY_ENABLE_DOCS=false disables all three endpoints, preventing unauthorized API surface discovery in production deployments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(security): add unit tests and docs for GATEWAY_ENABLE_DOCS Add 7 tests covering default behavior, env var parsing (case-insensitive, fail-closed), endpoint visibility, and health endpoint independence. Update CONFIGURATION.md and CLAUDE.md with the new toggle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(security): apply ruff formatting to gateway app.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
f7b10d42e4
commit
0691c4dda3
@ -40,3 +40,6 @@ INFOQUEST_API_KEY=your-infoquest-api-key
|
||||
#
|
||||
# WECOM_BOT_ID=your-wecom-bot-id
|
||||
# WECOM_BOT_SECRET=your-wecom-bot-secret
|
||||
|
||||
# Set to "false" to disable Swagger UI, ReDoc, and OpenAPI schema in production
|
||||
# GATEWAY_ENABLE_DOCS=false
|
||||
|
||||
@ -205,7 +205,7 @@ Configuration priority:
|
||||
|
||||
### Gateway API (`app/gateway/`)
|
||||
|
||||
FastAPI application on port 8001 with health check at `GET /health`.
|
||||
FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled).
|
||||
|
||||
**Routers**:
|
||||
|
||||
|
||||
@ -216,6 +216,8 @@ def create_app() -> FastAPI:
|
||||
Returns:
|
||||
Configured FastAPI application instance.
|
||||
"""
|
||||
config = get_gateway_config()
|
||||
docs_kwargs = {"docs_url": "/docs", "redoc_url": "/redoc", "openapi_url": "/openapi.json"} if config.enable_docs else {"docs_url": None, "redoc_url": None, "openapi_url": None}
|
||||
|
||||
app = FastAPI(
|
||||
title="DeerFlow API Gateway",
|
||||
@ -240,9 +242,7 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
|
||||
""",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
openapi_url="/openapi.json",
|
||||
**docs_kwargs,
|
||||
openapi_tags=[
|
||||
{
|
||||
"name": "models",
|
||||
|
||||
@ -9,6 +9,7 @@ class GatewayConfig(BaseModel):
|
||||
host: str = Field(default="0.0.0.0", description="Host to bind the gateway server")
|
||||
port: int = Field(default=8001, description="Port to bind the gateway server")
|
||||
cors_origins: list[str] = Field(default_factory=lambda: ["http://localhost:3000"], description="Allowed CORS origins")
|
||||
enable_docs: bool = Field(default=True, description="Enable Swagger/ReDoc/OpenAPI endpoints")
|
||||
|
||||
|
||||
_gateway_config: GatewayConfig | None = None
|
||||
@ -23,5 +24,6 @@ def get_gateway_config() -> GatewayConfig:
|
||||
host=os.getenv("GATEWAY_HOST", "0.0.0.0"),
|
||||
port=int(os.getenv("GATEWAY_PORT", "8001")),
|
||||
cors_origins=cors_origins_str.split(","),
|
||||
enable_docs=os.getenv("GATEWAY_ENABLE_DOCS", "true").lower() == "true",
|
||||
)
|
||||
return _gateway_config
|
||||
|
||||
@ -320,6 +320,7 @@ models:
|
||||
- `NOVITA_API_KEY` - Novita API key (OpenAI-compatible endpoint)
|
||||
- `TAVILY_API_KEY` - Tavily search API key
|
||||
- `DEER_FLOW_CONFIG_PATH` - Custom config file path
|
||||
- `GATEWAY_ENABLE_DOCS` - Set to `false` to disable Swagger UI (`/docs`), ReDoc (`/redoc`), and OpenAPI schema (`/openapi.json`) endpoints (default: `true`)
|
||||
|
||||
## Configuration Location
|
||||
|
||||
|
||||
124
backend/tests/test_gateway_docs_toggle.py
Normal file
124
backend/tests/test_gateway_docs_toggle.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""Tests for GATEWAY_ENABLE_DOCS configuration toggle.
|
||||
|
||||
Verifies that Swagger UI (/docs), ReDoc (/redoc), and the OpenAPI schema
|
||||
(/openapi.json) can be disabled via the GATEWAY_ENABLE_DOCS environment
|
||||
variable for production deployments.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def _reset_gateway_config():
|
||||
"""Reset the cached gateway config so env changes take effect."""
|
||||
import app.gateway.config as cfg
|
||||
|
||||
cfg._gateway_config = None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_config():
|
||||
"""Ensure gateway config cache is cleared before and after each test."""
|
||||
_reset_gateway_config()
|
||||
yield
|
||||
_reset_gateway_config()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_enable_docs_defaults_to_true():
|
||||
"""When GATEWAY_ENABLE_DOCS is not set, enable_docs should be True."""
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
if "GATEWAY_ENABLE_DOCS" in os.environ:
|
||||
del os.environ["GATEWAY_ENABLE_DOCS"]
|
||||
_reset_gateway_config()
|
||||
from app.gateway.config import get_gateway_config
|
||||
|
||||
config = get_gateway_config()
|
||||
assert config.enable_docs is True
|
||||
|
||||
|
||||
def test_enable_docs_false():
|
||||
"""GATEWAY_ENABLE_DOCS=false should disable docs."""
|
||||
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": "false"}):
|
||||
_reset_gateway_config()
|
||||
from app.gateway.config import get_gateway_config
|
||||
|
||||
config = get_gateway_config()
|
||||
assert config.enable_docs is False
|
||||
|
||||
|
||||
def test_enable_docs_case_insensitive():
|
||||
"""GATEWAY_ENABLE_DOCS is case-insensitive (FALSE, False, false)."""
|
||||
for value in ("FALSE", "False", "false"):
|
||||
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": value}):
|
||||
_reset_gateway_config()
|
||||
from app.gateway.config import get_gateway_config
|
||||
|
||||
config = get_gateway_config()
|
||||
assert config.enable_docs is False, f"Expected False for GATEWAY_ENABLE_DOCS={value}"
|
||||
|
||||
|
||||
def test_enable_docs_unexpected_value_disables():
|
||||
"""Any non-'true' value should disable docs (fail-closed)."""
|
||||
for value in ("0", "no", "off", "anything"):
|
||||
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": value}):
|
||||
_reset_gateway_config()
|
||||
from app.gateway.config import get_gateway_config
|
||||
|
||||
config = get_gateway_config()
|
||||
assert config.enable_docs is False, f"Expected False for GATEWAY_ENABLE_DOCS={value}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# App-level endpoint visibility
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_docs_endpoints_available_by_default():
|
||||
"""With enable_docs=True (default), /docs, /redoc, /openapi.json return 200."""
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
if "GATEWAY_ENABLE_DOCS" in os.environ:
|
||||
del os.environ["GATEWAY_ENABLE_DOCS"]
|
||||
_reset_gateway_config()
|
||||
from app.gateway.app import create_app
|
||||
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
assert client.get("/docs").status_code == 200
|
||||
assert client.get("/redoc").status_code == 200
|
||||
assert client.get("/openapi.json").status_code == 200
|
||||
|
||||
|
||||
def test_docs_endpoints_disabled_when_false():
|
||||
"""With GATEWAY_ENABLE_DOCS=false, /docs, /redoc, /openapi.json return 404."""
|
||||
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": "false"}):
|
||||
_reset_gateway_config()
|
||||
from app.gateway.app import create_app
|
||||
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
assert client.get("/docs").status_code == 404
|
||||
assert client.get("/redoc").status_code == 404
|
||||
assert client.get("/openapi.json").status_code == 404
|
||||
|
||||
|
||||
def test_health_still_works_when_docs_disabled():
|
||||
"""Disabling docs should NOT affect /health or other normal endpoints."""
|
||||
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": "false"}):
|
||||
_reset_gateway_config()
|
||||
from app.gateway.app import create_app
|
||||
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
resp = client.get("/health")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "healthy"
|
||||
Loading…
x
Reference in New Issue
Block a user