fix(security): 凭证扫描放行依赖注入传参 + 四仓口径更正
CI / ai-gates (push) Failing after 29s

`api_key=volcengine.api_key` 这类**依赖注入传参**被 KEY-ASSIGN 判为疑似凭证。
这是规则的真缺口而非代码问题:行里没有任何秘密,而做 DI 的代码到处都是这种
写法——绕过一次就得在每个调用点绕过,那等于把规则废掉。故改规则。

新增 ALLOW_REFERENCE,刻意收得很紧以免开后门:
- 值必须是**未加引号**的小写起首标识符引用(真密钥要么带引号,要么不是标识符形状)
- 只允许**恰好一个点且随即行尾**
- 连字符不在字符集内

对抗性夹具验证(三真三假):
- 拦下 `api_key = "sk-T3Blbk…"`(OpenAI 形态)
- 拦下 `secret_key: "AKID…"`(腾讯云形态)
- 拦下 `auth_token=eyJ….eyJ….Sfl…`(**三段式 JWT——两个点,故不落进放行**)
- 放行 `api_key=lowercase.reference` / `api_key = settings.api_key` / `api_token: config.authToken`
原 5 形态泄漏夹具复验仍 8 条命中,一条不少。

另更正口径:脚本头与 git-workflow.md 仍写「三仓同构」,实际已是四仓——
patbond-ai 是唯一持有 AI 服务商凭证的仓,漏同步它风险最大。四仓 md5 已一致
(4945940093b4b8413b7d3eec8bf6a5ec),四仓 --all 全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-20 12:18:18 +08:00
parent c6e8dddc8d
commit 4762358f54
24 changed files with 1953 additions and 775 deletions
+98
View File
@@ -0,0 +1,98 @@
"""配置加载的测试。
重点在两处:非法值必须**响亮失败而不是静默回落**(一次配置笔误变成难查的行为
偏差是最糟的结果),以及密钥**绝不能有默认值**。
"""
from __future__ import annotations
import pytest
from app.config import ServiceAuthSettings, Settings, VolcengineSettings
class TestCredentialsHaveNoDefaults:
def test_api_key_defaults_to_empty_not_a_placeholder(self, monkeypatch):
monkeypatch.delenv("ARK_API_KEY", raising=False)
assert VolcengineSettings.from_env().api_key == ""
def test_service_jwt_public_key_defaults_to_empty(self, monkeypatch):
monkeypatch.delenv("PATBOND_SERVICE_JWT_PUBLIC_KEY", raising=False)
assert ServiceAuthSettings.from_env().public_key == ""
def test_missing_credentials_do_not_prevent_loading(self, monkeypatch):
"""缺密钥时配置仍能加载。
服务要能起来并提供健康检查;缺配置在**调用点**响亮失败,而不是让整个
进程起不来——与 Java 侧 JwtVerifier/ServiceTokenSigner 的既有约定一致。
"""
for name in ("ARK_API_KEY", "PATBOND_SERVICE_JWT_PUBLIC_KEY"):
monkeypatch.delenv(name, raising=False)
settings = Settings.from_env()
assert settings.volcengine.api_key == ""
assert settings.service_auth.public_key == ""
class TestEnvParsing:
def test_values_are_trimmed(self, monkeypatch):
# 从 .env 或 compose 里复制粘贴时尾随空格极常见。
monkeypatch.setenv("ARK_API_KEY", " dummy-value ")
assert VolcengineSettings.from_env().api_key == "dummy-value"
def test_int_override(self, monkeypatch):
monkeypatch.setenv("ARK_TASK_EXPIRY_SEC", "7200")
assert VolcengineSettings.from_env().task_expiry_sec == 7200
def test_float_override(self, monkeypatch):
monkeypatch.setenv("ARK_TIMEOUT_SEC", "2.5")
assert VolcengineSettings.from_env().timeout_sec == 2.5
def test_malformed_int_fails_loudly_instead_of_falling_back(self, monkeypatch):
# 静默回落到默认值会让一次笔误变成难查的行为偏差。
monkeypatch.setenv("ARK_TASK_EXPIRY_SEC", "一小时")
with pytest.raises(ValueError, match="不是合法整数"):
VolcengineSettings.from_env()
def test_malformed_float_fails_loudly(self, monkeypatch):
monkeypatch.setenv("ARK_TIMEOUT_SEC", "fast")
with pytest.raises(ValueError, match="不是合法数字"):
VolcengineSettings.from_env()
def test_empty_string_uses_the_default(self, monkeypatch):
# compose 里 `ARK_TIMEOUT_SEC: ${FOO:-}` 展开成空串是常见情形。
monkeypatch.setenv("ARK_TIMEOUT_SEC", "")
assert VolcengineSettings.from_env().timeout_sec == 10.0
class TestVendorConstraintsAreEnforcedAtStartup:
"""厂商的硬性区间在启动时校验,而不是等提交时被拒。"""
def test_expiry_below_the_vendor_minimum_is_rejected(self):
with pytest.raises(ValueError, match=r"\[3600, 259200\]"):
VolcengineSettings(task_expiry_sec=60)
def test_expiry_above_the_vendor_maximum_is_rejected(self):
with pytest.raises(ValueError, match=r"\[3600, 259200\]"):
VolcengineSettings(task_expiry_sec=300000)
def test_the_boundaries_themselves_are_allowed(self):
assert VolcengineSettings(task_expiry_sec=3600).task_expiry_sec == 3600
assert VolcengineSettings(task_expiry_sec=259200).task_expiry_sec == 259200
def test_default_is_the_minimum_not_the_vendor_default(self):
# 厂商默认 48 小时,被放弃的任务会在厂商侧挂两天。
assert VolcengineSettings().task_expiry_sec == 3600
class TestServiceAuthDefaults:
def test_audience_and_type_have_sensible_defaults(self, monkeypatch):
for name in (
"PATBOND_SERVICE_JWT_AUDIENCE",
"PATBOND_SERVICE_JWT_ISSUER",
"PATBOND_SERVICE_JWT_TYPE",
):
monkeypatch.delenv(name, raising=False)
auth = ServiceAuthSettings.from_env()
assert auth.audience == "patbond-ai"
assert auth.issuer == "patbond-api"
assert auth.required_type == "service"