| 作者 | SHA1 | 備註 | 提交日期 |
|---|---|---|---|
|
|
e9975df35c |
feat(web): 素材凭据输入框增加已配置占位符
编辑 54/61 渠道时 AccessKey/SecretKey 输入框此前完全留空,容易误以为 凭据未保存。已配置时占位符显示"已配置,留空保持不变",未配置时显示 "请输入 AccessKey/SecretKey"。后端仍不回传真实密钥,仅依据 asset_credential_configured 状态区分占位符文案。顺带补齐凭据卡片 已有文案的英文翻译。 |
1 月之前 |
|
|
62bda9526a |
chore(e2e): 凭据与进程清理工具安全加固
按 Mimosa 安全扫描建议处理 e2e 测试工具的存量高危项: - e2e 集群的 root/admin 密码、会话密钥、同步 API Key 改为环境变量 优先(E2E_ROOT_PASSWORD/E2E_ADMIN_PASSWORD/E2E_SESSION_SECRET/ E2E_SYNC_API_KEY),本地回退值仅服务于 127.0.0.1 测试集群, Go 与 Playwright 两侧默认值保持一致 - killWindowsProcessTree 对 pid 强制正整数校验后再交给 taskkill, 消除命令注入误报入口 |
1 月之前 |
|
|
0ab4867130 |
fix(channel): 修复豆包视频渠道素材凭据摘要不回显
attachChannelAssetCredentialSummaries 收集查询 ID 时只纳入了中国移动 Seedance(61) 渠道,豆包视频(54) 渠道的凭据永远不会回显,导致管理端 编辑 54 渠道时素材 API 地址显示为空。提取 isAssetCredentialSummaryChannel 统一两处类型判断,并补充 54 渠道回归测试。 |
1 月之前 |
| @@ -70,10 +70,15 @@ func clearChannelInfo(channel *model.Channel) { | |||
| } | |||
| } | |||
| func isAssetCredentialSummaryChannel(channel *model.Channel) bool { | |||
| return channel != nil && (channel.Type == constant.ChannelTypeChinaMobileSeedance || | |||
| channel.Type == constant.ChannelTypeDoubaoVideo) | |||
| } | |||
| func attachChannelAssetCredentialSummaries(channels []*model.Channel) error { | |||
| ids := make([]int, 0) | |||
| for _, channel := range channels { | |||
| if channel != nil && channel.Type == constant.ChannelTypeChinaMobileSeedance { | |||
| if isAssetCredentialSummaryChannel(channel) { | |||
| ids = append(ids, channel.Id) | |||
| } | |||
| } | |||
| @@ -82,7 +87,7 @@ func attachChannelAssetCredentialSummaries(channels []*model.Channel) error { | |||
| return err | |||
| } | |||
| for _, channel := range channels { | |||
| if channel == nil || (channel.Type != constant.ChannelTypeChinaMobileSeedance && channel.Type != constant.ChannelTypeDoubaoVideo) { | |||
| if !isAssetCredentialSummaryChannel(channel) { | |||
| continue | |||
| } | |||
| summary, ok := summaries[channel.Id] | |||
| @@ -33,18 +33,36 @@ func setupChannelAssetCredentialControllerDB(t *testing.T) *gorm.DB { | |||
| func TestAttachChannelAssetCredentialSummariesDoesNotExposeSecrets(t *testing.T) { | |||
| db := setupChannelAssetCredentialControllerDB(t) | |||
| accessKey := "ak-" + t.Name() | |||
| secretKey := "sk-" + t.Name() | |||
| channel := &model.Channel{Id: 61, Type: constant.ChannelTypeChinaMobileSeedance, Key: "video-key", Name: "channel"} | |||
| require.NoError(t, db.Create(channel).Error) | |||
| require.NoError(t, model.UpsertChannelAssetCredential(&model.ChannelAssetCredential{ | |||
| ChannelId: 61, | |||
| AccessKey: "ak-secret", | |||
| SecretKey: "sk-secret", | |||
| AccessKey: accessKey, | |||
| SecretKey: secretKey, | |||
| PoolID: "pool-61", | |||
| })) | |||
| require.NoError(t, attachChannelAssetCredentialSummaries([]*model.Channel{channel})) | |||
| assert.True(t, channel.AssetCredentialConfigured) | |||
| assert.Equal(t, "pool-61", channel.AssetCredentialPoolID) | |||
| assert.NotContains(t, channel.Key, "ak-secret") | |||
| assert.NotContains(t, channel.Key, "sk-secret") | |||
| assert.NotContains(t, channel.Key, accessKey) | |||
| assert.NotContains(t, channel.Key, secretKey) | |||
| } | |||
| func TestAttachChannelAssetCredentialSummariesIncludesDoubaoVideo(t *testing.T) { | |||
| db := setupChannelAssetCredentialControllerDB(t) | |||
| channel := &model.Channel{Id: 54, Type: constant.ChannelTypeDoubaoVideo, Key: "video-key", Name: "doubao"} | |||
| require.NoError(t, db.Create(channel).Error) | |||
| require.NoError(t, model.UpsertChannelAssetCredential(&model.ChannelAssetCredential{ | |||
| ChannelId: 54, | |||
| AccessKey: "ak-" + t.Name(), | |||
| SecretKey: "sk-" + t.Name(), | |||
| BaseURL: "http://example.com/openApi/portrait", | |||
| })) | |||
| require.NoError(t, attachChannelAssetCredentialSummaries([]*model.Channel{channel})) | |||
| assert.True(t, channel.AssetCredentialConfigured) | |||
| assert.Equal(t, "http://example.com/openApi/portrait", channel.AssetCredentialBaseURL) | |||
| } | |||
| @@ -107,7 +107,7 @@ func GetRemoteUserSnapshot(baseURL string, userID int) (*userMigration.RemoteUse | |||
| nil, | |||
| http.StatusOK, | |||
| map[string]string{ | |||
| "X-Sync-API-Key": DefaultSyncAPIKey, | |||
| "X-Sync-API-Key": SyncAPIKey(), | |||
| "X-Sync-Node": "e2e-assert", | |||
| }, | |||
| &resp, | |||
| @@ -19,19 +19,40 @@ const ( | |||
| DefaultCNPort = 3100 | |||
| DefaultOVPort = 3101 | |||
| DefaultSessionSecret = "user-migration-e2e-session-secret" | |||
| DefaultCNDBFileName = "cn.db" | |||
| DefaultOVDBFileName = "ov.db" | |||
| DefaultCNLogFileName = "cn.log" | |||
| DefaultOVLogFileName = "ov.log" | |||
| DefaultCNSessionName = "cn_session" | |||
| DefaultOVSessionName = "ov_session" | |||
| DefaultSyncAPIKey = "e2e-sync-key" | |||
| DefaultRootUsername = "root" | |||
| DefaultRootPassword = "Password123!" | |||
| defaultNodeReadyTimout = 45 * time.Second | |||
| ) | |||
| // The e2e cluster runs on loopback only; each secret below resolves from the | |||
| // environment first and falls back to a local-only value shared with the | |||
| // Playwright fixtures (see web/e2e/fixtures/cluster.ts). | |||
| func RootPassword() string { | |||
| if password := strings.TrimSpace(os.Getenv("E2E_ROOT_PASSWORD")); password != "" { | |||
| return password | |||
| } | |||
| return "e2e-local-root-pass" | |||
| } | |||
| func SessionSecret() string { | |||
| if secret := strings.TrimSpace(os.Getenv("E2E_SESSION_SECRET")); secret != "" { | |||
| return secret | |||
| } | |||
| return "e2e-local-session-secret" | |||
| } | |||
| func SyncAPIKey() string { | |||
| if key := strings.TrimSpace(os.Getenv("E2E_SYNC_API_KEY")); key != "" { | |||
| return key | |||
| } | |||
| return "e2e-local-sync-key" | |||
| } | |||
| type NodeProcess struct { | |||
| Name string | |||
| Port int | |||
| @@ -115,11 +136,11 @@ func PrepareEnvironment(repoRoot, artifactRoot, binaryPath, scenarioName string) | |||
| return nil, err | |||
| } | |||
| if err := SetupRoot(cnClient, DefaultRootUsername, DefaultRootPassword); err != nil { | |||
| if err := SetupRoot(cnClient, DefaultRootUsername, RootPassword()); err != nil { | |||
| _ = cluster.Stop() | |||
| return nil, fmt.Errorf("setup CN root: %w", err) | |||
| } | |||
| if err := SetupRoot(ovClient, DefaultRootUsername, DefaultRootPassword); err != nil { | |||
| if err := SetupRoot(ovClient, DefaultRootUsername, RootPassword()); err != nil { | |||
| _ = cluster.Stop() | |||
| return nil, fmt.Errorf("setup OV root: %w", err) | |||
| } | |||
| @@ -151,25 +172,25 @@ func PrepareEnvironment(repoRoot, artifactRoot, binaryPath, scenarioName string) | |||
| return nil, err | |||
| } | |||
| if err := Login(cnClient, DefaultRootUsername, DefaultRootPassword); err != nil { | |||
| if err := Login(cnClient, DefaultRootUsername, RootPassword()); err != nil { | |||
| _ = cluster.Stop() | |||
| return nil, fmt.Errorf("login CN root: %w", err) | |||
| } | |||
| if err := Login(ovClient, DefaultRootUsername, DefaultRootPassword); err != nil { | |||
| if err := Login(ovClient, DefaultRootUsername, RootPassword()); err != nil { | |||
| _ = cluster.Stop() | |||
| return nil, fmt.Errorf("login OV root: %w", err) | |||
| } | |||
| if err := ConfigureCNRegionSync(cnClient, cluster.CN.BaseURL, cluster.OV.BaseURL, DefaultSyncAPIKey); err != nil { | |||
| if err := ConfigureCNRegionSync(cnClient, cluster.CN.BaseURL, cluster.OV.BaseURL, SyncAPIKey()); err != nil { | |||
| _ = cluster.Stop() | |||
| return nil, fmt.Errorf("configure CN region sync: %w", err) | |||
| } | |||
| if err := ConfigureOVRegionSync(ovClient, cluster.CN.BaseURL, DefaultSyncAPIKey); err != nil { | |||
| if err := ConfigureOVRegionSync(ovClient, cluster.CN.BaseURL, SyncAPIKey()); err != nil { | |||
| _ = cluster.Stop() | |||
| return nil, fmt.Errorf("configure OV region sync: %w", err) | |||
| } | |||
| if err := WaitOptionApplied(cluster.OV.BaseURL, DefaultSyncAPIKey, 5, 20*time.Second); err != nil { | |||
| if err := WaitOptionApplied(cluster.OV.BaseURL, SyncAPIKey(), 5, 20*time.Second); err != nil { | |||
| _ = cluster.Stop() | |||
| return nil, fmt.Errorf("wait OV migration API ready: %w", err) | |||
| } | |||
| @@ -272,7 +293,7 @@ func (c *Cluster) startNode(name string, port int, dbPath, sessionName, nodeType | |||
| cmd.Env = mergeEnv(os.Environ(), map[string]string{ | |||
| "PORT": fmt.Sprintf("%d", port), | |||
| "SQLITE_PATH": dbPath, | |||
| "SESSION_SECRET": DefaultSessionSecret, | |||
| "SESSION_SECRET": SessionSecret(), | |||
| "SESSION_NAME": sessionName, | |||
| "SESSION_SECURE": "false", | |||
| "SESSION_SAMESITE": "strict", | |||
| @@ -224,7 +224,7 @@ func TestUserMigrationE2E_ScanPagination(t *testing.T) { | |||
| func TestUserMigrationE2E_RootPermission(t *testing.T) { | |||
| env := newEnvironment(t) | |||
| adminClient := createAndLoginManagedUser(t, env.Cluster.CN.BaseURL, env.CNClient, "migration-admin", helpers.DefaultRootPassword, common.RoleAdminUser) | |||
| adminClient := createAndLoginManagedUser(t, env.Cluster.CN.BaseURL, env.CNClient, "migration-admin", helpers.RootPassword(), common.RoleAdminUser) | |||
| var listResp simpleResponse | |||
| require.NoError(t, adminClient.GetJSON("/api/user-migrations/batches", 200, &listResp)) | |||
| @@ -707,7 +707,7 @@ func createManagedUsers(t *testing.T, client *helpers.APIClient, prefix string, | |||
| var resp simpleResponse | |||
| require.NoError(t, client.PostJSON("/api/user/", map[string]any{ | |||
| "username": username, | |||
| "password": helpers.DefaultRootPassword, | |||
| "password": helpers.RootPassword(), | |||
| "display_name": displayName, | |||
| "email": email, | |||
| "role": role, | |||
| @@ -775,7 +775,7 @@ func convertRemoteUserToSynced(t *testing.T, ovBaseURL string, sourceUserID, rem | |||
| }, | |||
| 200, | |||
| map[string]string{ | |||
| "X-Sync-API-Key": helpers.DefaultSyncAPIKey, | |||
| "X-Sync-API-Key": helpers.SyncAPIKey(), | |||
| "X-Sync-Node": "e2e-test", | |||
| }, | |||
| &resp, | |||
| @@ -33,9 +33,9 @@ export const clusterRunDir = path.join(artifactRoot, 'runs', 'playwright-ui'); | |||
| export const cnBaseURL = 'http://127.0.0.1:3100'; | |||
| export const ovBaseURL = 'http://127.0.0.1:3101'; | |||
| export const rootUsername = 'root'; | |||
| export const rootPassword = 'Password123!'; | |||
| export const rootPassword = process.env.E2E_ROOT_PASSWORD ?? 'e2e-local-root-pass'; | |||
| export const adminUsername = 'playwright-admin'; | |||
| export const adminPassword = 'Password123!'; | |||
| export const adminPassword = process.env.E2E_ADMIN_PASSWORD ?? 'e2e-local-admin-pass'; | |||
| export type ClusterMetadata = { | |||
| serverPid: number; | |||
| @@ -66,10 +66,13 @@ function waitForExit(child, timeoutMs) { | |||
| } | |||
| function killWindowsProcessTree(pid) { | |||
| if (!pid) return Promise.resolve(); | |||
| const numericPid = Number(pid); | |||
| if (!Number.isInteger(numericPid) || numericPid <= 0) { | |||
| return Promise.resolve(); | |||
| } | |||
| return new Promise((resolve) => { | |||
| const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { | |||
| const killer = spawn('taskkill', ['/PID', String(numericPid), '/T', '/F'], { | |||
| stdio: 'ignore', | |||
| }); | |||
| @@ -2566,12 +2566,22 @@ const EditChannelModal = (props) => { | |||
| label='AccessKey' | |||
| mode='password' | |||
| autoComplete='new-password' | |||
| placeholder={ | |||
| inputs.asset_credential_configured | |||
| ? t('已配置,留空保持不变') | |||
| : t('请输入 AccessKey') | |||
| } | |||
| /> | |||
| <Form.Input | |||
| field='asset_credential.secret_key' | |||
| label='SecretKey' | |||
| mode='password' | |||
| autoComplete='new-password' | |||
| placeholder={ | |||
| inputs.asset_credential_configured | |||
| ? t('已配置,留空保持不变') | |||
| : t('请输入 SecretKey') | |||
| } | |||
| /> | |||
| {inputs.type === 61 ? ( | |||
| <Form.Input | |||
| @@ -2723,6 +2723,16 @@ | |||
| "豆包视频(素材网关)": "Doubao Video (Asset Gateway)", | |||
| "素材 API 地址": "Asset API Base URL", | |||
| "素材库接口调用该地址下的 /openApi/portrait,为素材网关专属;留空则使用渠道 API 地址。火山官方地址不提供此接口。": "Asset APIs call /openApi/portrait under this address (asset gateway only); leave empty to use the channel API address. The official Volcengine address does not provide this API.", | |||
| "已配置,留空保持不变": "Configured; leave empty to keep the current value", | |||
| "请输入 AccessKey": "Enter the AccessKey", | |||
| "请输入 SecretKey": "Enter the SecretKey", | |||
| "已配置素材凭证;留空不会覆盖现有凭证。": "Asset credentials configured; leaving the fields empty will not overwrite them.", | |||
| "未配置素材凭证;素材上传接口将不可用。": "No asset credentials configured; asset upload APIs will be unavailable.", | |||
| "移动云素材凭证": "China Mobile Cloud Asset Credentials", | |||
| "火山素材凭证": "Volcengine Asset Credentials", | |||
| "项目编码 ProjectCode": "Project Code (ProjectCode)", | |||
| "火山项目编码,可选": "Volcengine project code, optional", | |||
| "请同时填写素材 AccessKey 和 SecretKey": "Fill in both asset AccessKey and SecretKey", | |||
| "账单": "Bills", | |||
| "账户充值": "Account recharge", | |||
| "账户已删除!": "Account has been deleted!", | |||