fix(security): 屏蔽 admin 账号接口返回的敏感凭证字段

Account.Credentials 是 JSONB map,混合存放可编辑的非敏感配置(base_url、
model_mapping、project_id 等)与敏感秘钥(OAuth access/refresh/id token、
API key、AWS secret、Vertex private key 等)。当前所有 admin 账号接口直接
透传该 map,token 经由浏览器 DevTools、抓包、日志等途径泄漏。

- service 包新增 SensitiveCredentialKeys 清单与 MergePreservingSensitiveCreds
  作为单一权威定义。
- dto 层 RedactCredentials 在响应里剥离敏感子键,输出 credentials_status
  (has_<key> 布尔标识)告知前端存在性,不暴露原值。
- AccountFromServiceShallow 接入脱敏,覆盖 list、get、create、update、
  refresh、batch、bulk-update、OAuth 创建等 9 个 handler。
- service.UpdateAccount 改为合并语义:incoming 没传敏感键则保留 existing,
  让前端"全对象 PUT"流程在脱敏后无感工作;显式提供新 token 仍会覆盖。
- 前端 EditAccountModal 修复脱敏后会崩的两处兜底:apikey 必填检查和
  Vertex SA JSON 存在性校验改读 credentials_status.has_*。
- 导出端点 /admin/accounts/data 走独立的 DataAccount 结构,按设计保留
  完整 credentials 作为管理员备份路径。

测试:RedactCredentials 单元测试、mapper 端到端 JSON 断言(确认序列化
后无 token 子串)、UpdateAccount 合并语义三种场景(保留 / 覆盖 / 空 map 跳过)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
harukaandClaude Opus 4.7 committed 2026-05-08 03:44:04 +08:00
1 parent a466e80ed6
commit 0f8e2d0934
12 files changed
+510 -27

No files matched your search

@@ -3343,13 +3343,12 @@ const handleSubmit = async () => {
}
// Handle API key
// 后端响应已脱敏:currentCredentials 不会再包含 api_key 原文。
// 用户填入新值则覆盖;留空且后端已存在旧 key(看 credentials_status)则不带 api_key 字段,
// 由后端合并保留;两者都无才报错。
if (editApiKey.value.trim()) {
// User provided a new API key
newCredentials.api_key = editApiKey.value.trim()
} else if (currentCredentials.api_key) {
// Preserve existing api_key
newCredentials.api_key = currentCredentials.api_key
} else {
} else if (!props.account.credentials_status?.has_api_key) {
appStore.showError(t('admin.accounts.apiKeyIsRequired'))
return
}
@@ -3434,7 +3433,11 @@ const handleSubmit = async () => {
return
}
if (!currentCredentials.service_account_json && !currentCredentials.service_account) {
// SA JSON 已脱敏不再随 credentials 返回,存在性改读 credentials_status。
const hasExistingServiceAccountJson =
props.account.credentials_status?.has_service_account_json ||
props.account.credentials_status?.has_service_account
if (!hasExistingServiceAccountJson) {
appStore.showError(t('admin.accounts.vertexSaJsonRequired'))
return
}