sub2api/backend/internal/service/account_credentials_redact_test.go
haruka 0f8e2d0934 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>
2026-05-08 03:44:04 +08:00

91 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build unit
package service
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestMergePreservingSensitiveCreds_PreservesSensitiveWhenIncomingMissing(t *testing.T) {
existing := map[string]any{
"refresh_token": "rt-old",
"access_token": "at-old",
"api_key": "sk-old",
"base_url": "https://old.example.com",
}
incoming := map[string]any{
"base_url": "https://new.example.com",
"model_mapping": map[string]any{"foo": "bar"},
}
out := MergePreservingSensitiveCreds(existing, incoming)
require.Equal(t, "rt-old", out["refresh_token"], "incoming 没传 refresh_token应保留 existing")
require.Equal(t, "at-old", out["access_token"])
require.Equal(t, "sk-old", out["api_key"])
require.Equal(t, "https://new.example.com", out["base_url"], "非敏感键由 incoming 决定")
require.Equal(t, map[string]any{"foo": "bar"}, out["model_mapping"])
}
func TestMergePreservingSensitiveCreds_OverwritesWhenIncomingProvidesSensitive(t *testing.T) {
existing := map[string]any{
"refresh_token": "rt-old",
"api_key": "sk-old",
}
incoming := map[string]any{
"refresh_token": "rt-new",
// 显式没传 api_key —— 应保留
}
out := MergePreservingSensitiveCreds(existing, incoming)
require.Equal(t, "rt-new", out["refresh_token"], "incoming 显式传入应覆盖")
require.Equal(t, "sk-old", out["api_key"], "incoming 没传应保留")
}
func TestMergePreservingSensitiveCreds_DoesNotMutateInputs(t *testing.T) {
existing := map[string]any{"refresh_token": "rt"}
incoming := map[string]any{"base_url": "x"}
_ = MergePreservingSensitiveCreds(existing, incoming)
require.Equal(t, "rt", existing["refresh_token"])
require.NotContains(t, existing, "base_url")
require.Equal(t, "x", incoming["base_url"])
require.NotContains(t, incoming, "refresh_token")
}
func TestMergePreservingSensitiveCreds_NilInputs(t *testing.T) {
out := MergePreservingSensitiveCreds(nil, map[string]any{"base_url": "x"})
require.Equal(t, "x", out["base_url"])
require.NotContains(t, out, "refresh_token")
out2 := MergePreservingSensitiveCreds(map[string]any{"refresh_token": "rt"}, nil)
require.Equal(t, "rt", out2["refresh_token"])
}
func TestMergePreservingSensitiveCreds_NonSensitiveDeletionAllowed(t *testing.T) {
existing := map[string]any{
"refresh_token": "rt",
"base_url": "https://old",
"project_id": "p1",
}
incoming := map[string]any{
"base_url": "https://new",
// 不带 project_id —— 等同删除(非敏感键由 incoming 决定)
}
out := MergePreservingSensitiveCreds(existing, incoming)
require.Equal(t, "rt", out["refresh_token"], "敏感键保留")
require.Equal(t, "https://new", out["base_url"])
require.NotContains(t, out, "project_id", "非敏感键 incoming 不传 = 删除")
}
func TestIsSensitiveCredentialKey(t *testing.T) {
require.True(t, IsSensitiveCredentialKey("refresh_token"))
require.True(t, IsSensitiveCredentialKey("api_key"))
require.True(t, IsSensitiveCredentialKey("private_key"))
require.False(t, IsSensitiveCredentialKey("base_url"))
require.False(t, IsSensitiveCredentialKey(""))
require.False(t, IsSensitiveCredentialKey("model_mapping"))
}