- config.set_proxy gains password_env (aligned with api_key_env convention): config file persists only the env var name; plaintext password lives in memory for the current session only - _save_config deep-copies and nulls proxy.socks5.password before json.dump, so every save path is sanitized (defense in depth) - get_proxy resolves password as in-memory plaintext first, then via password_env from os.environ; downstream consumers (llm_client, web tool curl args) unchanged - legacy migration on load: plaintext found on disk is kept in memory for the session, file is immediately rewritten sanitized - SetProxyTool exposes password_env and reports password source - registry debug logs mask password/api_key/token/secret/authorization kwargs (N11) Tests: new tests/test_config.py (3 cases: no plaintext on save, password_env resolution, legacy migration); full suite 267 passed. Audit report updated with Batch F progress.
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
||
"""F1(N1 高危)修复回归测试:代理密码不落盘。
|
||
|
||
覆盖三点:
|
||
1. set_proxy(password=..., save=True) 后配置文件无明文密码(_save_config 脱敏)
|
||
2. password_env 解析:get_proxy() 从环境变量取密码拼 socks5:// URL
|
||
3. 旧版明文配置迁移:加载后盘上明文被清除,内存中本会话仍可用
|
||
"""
|
||
|
||
import json
|
||
|
||
import pytest
|
||
|
||
from common.config import ConfigManager
|
||
|
||
|
||
@pytest.fixture
|
||
def fresh_config(tmp_path, monkeypatch):
|
||
"""隔离 ConfigManager 单例:AI_CONFIG_PATH 指向临时文件,前后重置类状态"""
|
||
cfg_file = tmp_path / "config.json"
|
||
monkeypatch.setenv("AI_CONFIG_PATH", str(cfg_file))
|
||
|
||
def _reset():
|
||
ConfigManager._instance = None
|
||
ConfigManager._config = None
|
||
ConfigManager._config_path = None
|
||
|
||
_reset()
|
||
yield ConfigManager
|
||
_reset()
|
||
|
||
|
||
def _read_cfg_file() -> dict:
|
||
path = ConfigManager()._config_path
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
return json.load(f)
|
||
|
||
|
||
def test_set_proxy_sanitizes_password_on_save(fresh_config, tmp_path):
|
||
"""save=True 落盘后,文件中不得出现明文密码(纵深防御脱敏)"""
|
||
cfg = fresh_config()
|
||
cfg.set_proxy("127.0.0.1", 1080, username="u1", password="secret123",
|
||
password_env="MY_SOCKS_PW", save=True)
|
||
|
||
on_disk = _read_cfg_file()
|
||
socks = on_disk["proxy"]["socks5"]
|
||
assert socks["password"] in (None, ""), "落盘文件不得含明文密码"
|
||
assert socks["password_env"] == "MY_SOCKS_PW", "password_env 应持久化"
|
||
|
||
raw = (tmp_path / "config.json").read_text(encoding="utf-8")
|
||
assert "secret123" not in raw, "整个配置文件不得出现明文密码字符串"
|
||
|
||
# 内存中本会话仍可用
|
||
proxies = cfg.get_proxy()
|
||
assert proxies is not None
|
||
assert "u1:secret123@" in proxies["https"]
|
||
|
||
|
||
def test_password_env_resolution(fresh_config, monkeypatch):
|
||
"""password 为空时按 password_env 从环境变量解析"""
|
||
monkeypatch.setenv("TEST_SOCKS_PW", "env-secret-42")
|
||
cfg = fresh_config()
|
||
cfg.set_proxy("10.0.0.8", 7890, username="u2", password=None,
|
||
password_env="TEST_SOCKS_PW", save=True)
|
||
|
||
proxies = cfg.get_proxy()
|
||
assert proxies is not None
|
||
assert "u2:env-secret-42@10.0.0.8:7890" in proxies["https"]
|
||
|
||
# get_proxy_info 不泄漏密码值,只给布尔 + 变量名
|
||
info = cfg.get_proxy_info()
|
||
assert info["password"] is True
|
||
assert info["password_env"] == "TEST_SOCKS_PW"
|
||
|
||
|
||
def test_legacy_plaintext_migration(fresh_config, tmp_path):
|
||
"""旧版明文配置:加载后盘上清除,内存保留供本会话使用"""
|
||
cfg_file = tmp_path / "config.json"
|
||
legacy = {
|
||
"proxy": {
|
||
"socks5": {
|
||
"enabled": True, "host": "1.2.3.4", "port": 1080,
|
||
"username": "old_u", "password": "legacy_plain_pw"
|
||
}
|
||
}
|
||
}
|
||
cfg_file.write_text(json.dumps(legacy, ensure_ascii=False), encoding="utf-8")
|
||
|
||
cfg = fresh_config() # 触发 _load_config → 迁移
|
||
|
||
# 盘上已脱敏
|
||
on_disk = _read_cfg_file()
|
||
assert on_disk["proxy"]["socks5"]["password"] in (None, "")
|
||
raw = cfg_file.read_text(encoding="utf-8")
|
||
assert "legacy_plain_pw" not in raw
|
||
|
||
# 内存中本会话仍可解析出旧密码(代理不断)
|
||
proxies = cfg.get_proxy()
|
||
assert proxies is not None
|
||
assert "old_u:legacy_plain_pw@1.2.3.4:1080" in proxies["https"]
|