blob: d7b47fd70a411acf103336fb0cf4c5cd3e6f1a3b [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from urllib.parse import urlencode
import httpx
import pytest
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.routing import Route
from doris_mcp_server.auth.token_handlers import TokenHandlers
from doris_mcp_server.auth.token_security_middleware import TokenSecurityMiddleware
from doris_mcp_server.utils.config import DorisConfig
ADMIN_TOKEN = "J8pR4mX2vN7qL5sT9kW3cF6hY1uD8aB0eG4iC2oP"
def _config() -> DorisConfig:
config = DorisConfig()
config.security.enable_http_token_management = True
config.security.require_admin_auth = True
config.security.token_management_admin_token = ADMIN_TOKEN
config.security.token_management_allowed_ips = ["127.0.0.1"]
return config
def _request(
path: str = "/token/stats",
*,
headers: dict[str, str] | None = None,
query: dict[str, str] | None = None,
client_ip: str = "127.0.0.1",
) -> Request:
encoded_headers = [
(name.lower().encode(), value.encode())
for name, value in (headers or {}).items()
]
return Request(
{
"type": "http",
"method": "GET",
"scheme": "http",
"server": ("127.0.0.1", 3000),
"client": (client_ip, 50000),
"path": path,
"raw_path": path.encode(),
"query_string": urlencode(query or {}).encode(),
"headers": encoded_headers,
}
)
@pytest.mark.asyncio
async def test_admin_query_token_is_rejected_and_headers_are_accepted():
middleware = TokenSecurityMiddleware(_config())
query_only = await middleware.check_token_management_access(
_request(query={"admin_token": ADMIN_TOKEN})
)
assert query_only is not None
assert query_only.status_code == 401
bad_header_with_query = await middleware.check_token_management_access(
_request(
headers={"Authorization": "Bearer wrong-token"},
query={"admin_token": ADMIN_TOKEN},
)
)
assert bad_header_with_query is not None
assert bad_header_with_query.status_code == 401
assert (
await middleware.check_token_management_access(
_request(headers={"Authorization": f"Bearer {ADMIN_TOKEN}"})
)
is None
)
assert (
await middleware.check_token_management_access(
_request(headers={"X-Admin-Token": ADMIN_TOKEN})
)
is None
)
@pytest.mark.asyncio
async def test_proxy_headers_cannot_spoof_token_management_allowlist():
config = _config()
config.security.token_management_allowed_ips = ["203.0.113.7"]
middleware = TokenSecurityMiddleware(config)
denied = await middleware.check_token_management_access(
_request(
headers={
"Authorization": f"Bearer {ADMIN_TOKEN}",
"X-Forwarded-For": "203.0.113.7",
"X-Real-IP": "203.0.113.7",
},
client_ip="198.51.100.9",
)
)
assert denied is not None
assert denied.status_code == 403
assert b'"client_ip":"198.51.100.9"' in denied.body
class _AuthProvider:
token_manager = object()
class _SecurityManager:
auth_provider = _AuthProvider()
@staticmethod
def get_token_stats():
return {
"total_tokens": 0,
"active_tokens": 0,
"expired_tokens": 0,
"expiry_enabled": True,
"default_expiry_hours": 24,
}
@pytest.mark.asyncio
async def test_http_management_route_requires_admin_header():
handlers = TokenHandlers(_SecurityManager(), _config())
app = Starlette(
routes=[Route("/token/stats", handlers.handle_token_stats, methods=["GET"])]
)
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app),
base_url="http://127.0.0.1:3000",
) as client:
denied = await client.get(
"/token/stats",
params={"admin_token": ADMIN_TOKEN},
)
accepted = await client.get(
"/token/stats",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
assert denied.status_code == 401
assert accepted.status_code == 200
assert accepted.json()["success"] is True
@pytest.mark.asyncio
async def test_management_page_uses_header_only_and_never_builds_token_urls():
handlers = TokenHandlers(_SecurityManager(), _config())
response = await handlers.handle_management_page(
_request(
path="/token/management",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
)
body = response.body.decode()
assert response.status_code == 200
assert "admin_token" not in body
assert "URLSearchParams" not in body
assert "getAuthURL" not in body
assert "Authorization" in body
assert 'type="password" id="adminToken"' in body