blob: 4cab88abdd93fe140595e61d0de3bad6d70b30fb [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.
import json
import os
from pathlib import Path
import pytest
from doris_mcp_server.auth.token_manager import TokenManager
from doris_mcp_server.utils.config import (
AuthConfigError,
DorisConfig,
_mark_source,
normalize_effective_auth_config,
)
from doris_mcp_server.utils.secret_policy import (
is_static_token_environment_variable,
)
STATIC_TOKEN = "V4nK8qR2mT7xP5cL9sD3hF6jY1uB0eG4iW8aN2zQ"
MANAGEMENT_TOKEN = "Q7cM2vR9kL4xT8pD5sF1hJ6uB3eG0iW9aN2zY8qP"
def _config(tmp_path: Path) -> DorisConfig:
config = DorisConfig()
config.security.token_file_path = str(tmp_path / "tokens.json")
return config
def _clear_static_token_environment(monkeypatch: pytest.MonkeyPatch) -> None:
for name in list(os.environ):
if is_static_token_environment_variable(name):
monkeypatch.delenv(name, raising=False)
def test_repository_token_template_contains_no_credentials():
token_file = Path(__file__).parents[2] / "tokens.json"
token_data = json.loads(token_file.read_text(encoding="utf-8"))
assert token_data["tokens"] == []
assert "No usable credential is shipped" in token_data["notes"][0]
def test_legacy_token_secret_has_no_default():
assert DorisConfig().security.token_secret == ""
def test_static_auth_rejects_missing_active_credentials(tmp_path, monkeypatch):
_clear_static_token_environment(monkeypatch)
config = _config(tmp_path)
config.security.enable_token_auth = True
_mark_source(config, "enable_token_auth", "env")
with pytest.raises(AuthConfigError, match="at least one active high-entropy"):
normalize_effective_auth_config(config)
def test_static_auth_rejects_weak_environment_token(tmp_path, monkeypatch):
_clear_static_token_environment(monkeypatch)
config = _config(tmp_path)
config.security.enable_token_auth = True
_mark_source(config, "enable_token_auth", "env")
monkeypatch.setenv("TOKEN_ADMIN", "short-token")
with pytest.raises(AuthConfigError, match="at least 32 characters"):
normalize_effective_auth_config(config)
def test_management_settings_are_not_loaded_as_static_tokens(tmp_path, monkeypatch):
_clear_static_token_environment(monkeypatch)
config = _config(tmp_path)
config.security.enable_token_auth = True
_mark_source(config, "enable_token_auth", "env")
monkeypatch.setenv("TOKEN_MANAGEMENT_ADMIN_TOKEN", MANAGEMENT_TOKEN)
monkeypatch.setenv("TOKEN_MANAGEMENT_ALLOWED_IPS", "127.0.0.1")
with pytest.raises(AuthConfigError, match="at least one active high-entropy"):
normalize_effective_auth_config(config)
@pytest.mark.asyncio
async def test_strong_environment_token_bootstraps_static_auth(tmp_path, monkeypatch):
_clear_static_token_environment(monkeypatch)
config = _config(tmp_path)
config.security.enable_token_auth = True
_mark_source(config, "enable_token_auth", "env")
monkeypatch.setenv("TOKEN_ADMIN", STATIC_TOKEN)
normalize_effective_auth_config(config)
manager = TokenManager(config)
try:
result = await manager.validate_token(STATIC_TOKEN)
assert result.is_valid is True
assert result.token_info.token_id == "admin"
assert len(manager._tokens) == 1
finally:
manager.stop_hot_reload()
@pytest.mark.asyncio
async def test_manager_does_not_create_credentials_when_auth_is_disabled(
tmp_path,
monkeypatch,
):
_clear_static_token_environment(monkeypatch)
manager = TokenManager(_config(tmp_path))
try:
assert manager._tokens == {}
assert manager._token_ids == {}
finally:
manager.stop_hot_reload()
@pytest.mark.asyncio
async def test_custom_static_token_must_follow_secret_policy(tmp_path, monkeypatch):
_clear_static_token_environment(monkeypatch)
manager = TokenManager(_config(tmp_path))
try:
with pytest.raises(ValueError, match="at least 32 characters"):
await manager.create_token("weak", custom_token="short-token")
generated = await manager.create_token("generated")
assert len(generated) >= 32
assert (await manager.validate_token(generated)).is_valid is True
finally:
manager.stop_hot_reload()
@pytest.mark.parametrize(
("admin_token", "error"),
[
("", "is required"),
("short-token", "at least 32 characters"),
("a" * 40, "distinct characters"),
],
)
def test_http_token_management_rejects_weak_admin_secret(admin_token, error):
config = DorisConfig()
config.security.enable_http_token_management = True
config.security.require_admin_auth = True
config.security.token_management_admin_token = admin_token
with pytest.raises(AuthConfigError, match=error):
normalize_effective_auth_config(config)
def test_http_token_management_accepts_high_entropy_admin_secret():
config = DorisConfig()
config.security.enable_http_token_management = True
config.security.require_admin_auth = True
config.security.token_management_admin_token = MANAGEMENT_TOKEN
effective = normalize_effective_auth_config(config)
assert effective.auth_methods == ()