mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 07:33:06 +08:00
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>
118 lines
3.2 KiB
Go
118 lines
3.2 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type updateAccountCredsRepoStub struct {
|
|
mockAccountRepoForGemini
|
|
account *Account
|
|
updateCalls int
|
|
}
|
|
|
|
func (r *updateAccountCredsRepoStub) GetByID(ctx context.Context, id int64) (*Account, error) {
|
|
return r.account, nil
|
|
}
|
|
|
|
func (r *updateAccountCredsRepoStub) Update(ctx context.Context, account *Account) error {
|
|
r.updateCalls++
|
|
r.account = account
|
|
return nil
|
|
}
|
|
|
|
func TestUpdateAccount_PreservesSensitiveCredsWhenIncomingOmits(t *testing.T) {
|
|
accountID := int64(202)
|
|
repo := &updateAccountCredsRepoStub{
|
|
account: &Account{
|
|
ID: accountID,
|
|
Platform: PlatformAnthropic,
|
|
Type: AccountTypeOAuth,
|
|
Status: StatusActive,
|
|
Credentials: map[string]any{
|
|
"refresh_token": "rt-existing",
|
|
"access_token": "at-existing",
|
|
"id_token": "id-existing",
|
|
"base_url": "https://old.example.com",
|
|
},
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{accountRepo: repo}
|
|
|
|
// 模拟前端编辑:仅修改 base_url,没有传 token(脱敏后前端 spread 拿不到敏感键)
|
|
updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
|
|
Credentials: map[string]any{
|
|
"base_url": "https://new.example.com",
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, updated)
|
|
require.Equal(t, 1, repo.updateCalls)
|
|
|
|
// 敏感键应保留
|
|
require.Equal(t, "rt-existing", repo.account.Credentials["refresh_token"])
|
|
require.Equal(t, "at-existing", repo.account.Credentials["access_token"])
|
|
require.Equal(t, "id-existing", repo.account.Credentials["id_token"])
|
|
// 非敏感键被替换
|
|
require.Equal(t, "https://new.example.com", repo.account.Credentials["base_url"])
|
|
}
|
|
|
|
func TestUpdateAccount_ExplicitNewTokenOverwrites(t *testing.T) {
|
|
accountID := int64(203)
|
|
repo := &updateAccountCredsRepoStub{
|
|
account: &Account{
|
|
ID: accountID,
|
|
Platform: PlatformAnthropic,
|
|
Type: AccountTypeOAuth,
|
|
Status: StatusActive,
|
|
Credentials: map[string]any{
|
|
"refresh_token": "rt-old",
|
|
"api_key": "sk-old",
|
|
},
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{accountRepo: repo}
|
|
|
|
updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
|
|
Credentials: map[string]any{
|
|
"refresh_token": "rt-new",
|
|
// api_key 没传 → 应保留旧值
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, updated)
|
|
|
|
require.Equal(t, "rt-new", repo.account.Credentials["refresh_token"])
|
|
require.Equal(t, "sk-old", repo.account.Credentials["api_key"])
|
|
}
|
|
|
|
func TestUpdateAccount_EmptyCredentialsSkipsUpdate(t *testing.T) {
|
|
accountID := int64(204)
|
|
repo := &updateAccountCredsRepoStub{
|
|
account: &Account{
|
|
ID: accountID,
|
|
Platform: PlatformAnthropic,
|
|
Type: AccountTypeOAuth,
|
|
Status: StatusActive,
|
|
Credentials: map[string]any{
|
|
"refresh_token": "rt-existing",
|
|
},
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{accountRepo: repo}
|
|
|
|
_, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
|
|
Credentials: map[string]any{}, // len == 0 → 闸门跳过
|
|
Name: "renamed",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "rt-existing", repo.account.Credentials["refresh_token"], "空 credentials 不应触碰已有 token")
|
|
require.Equal(t, "renamed", repo.account.Name)
|
|
}
|