From c47bee64267a57c016a71374fc14452918c83b5f Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Wed, 20 May 2026 12:05:21 +0000 Subject: [PATCH 1/3] feat: add PostgreSQL to SqlWorkbenchService.SupportDBType whitelist (issue #850, compat-RISK-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs: dms-ee#850 Risk: compat-RISK-2 把 DBTypePostgreSQL 加入 SqlWorkbenchService.SupportDBType 白名单,使 PG 数据源 与 MySQL / Oracle / OceanBaseMySQL 等已支持类型一致走 ResetDbServiceByAuth 临时 账号路径进入 ODC 工作台(设计文档 §3.1 / §8.3)。 同步更新 sql_workbench_service_test.go::Test_SupportDBType 的 map case: - 9 项 expected=true:MySQL / Oracle / OceanBaseMySQL / DM / TiDB / TDSQLForInnoDB / GoldenDB / PolarDBForMySQL / PostgreSQL - 3 项 expected=false:SQL Server / 空字符串 / 未知类型字符串 (cherry picked from commit ba55d54f5d52adf9a85929a48958a1e334a67885) --- .../service/sql_workbench_service.go | 3 ++- .../service/sql_workbench_service_test.go | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/internal/sql_workbench/service/sql_workbench_service.go b/internal/sql_workbench/service/sql_workbench_service.go index 300ead12..9dc55b42 100644 --- a/internal/sql_workbench/service/sql_workbench_service.go +++ b/internal/sql_workbench/service/sql_workbench_service.go @@ -963,7 +963,8 @@ func (sqlWorkbenchService *SqlWorkbenchService) SupportDBType(dbType pkgConst.DB dbType == pkgConst.DBTypeTiDB || dbType == pkgConst.DBTypeTDSQLForInnoDB || dbType == pkgConst.DBTypeGoldenDB || - dbType == pkgConst.DBTypePolarDBForMySQL + dbType == pkgConst.DBTypePolarDBForMySQL || + dbType == pkgConst.DBTypePostgreSQL } // buildDatabaseUser 当是ob-mysql时需要给账号管理的账号附加租户名集群名等字符: root@oms_mysql#oms_resource_4250 diff --git a/internal/sql_workbench/service/sql_workbench_service_test.go b/internal/sql_workbench/service/sql_workbench_service_test.go index 6c65ba0a..bb95adb9 100644 --- a/internal/sql_workbench/service/sql_workbench_service_test.go +++ b/internal/sql_workbench/service/sql_workbench_service_test.go @@ -41,16 +41,20 @@ func Test_SupportDBType(t *testing.T) { input pkgConst.DBType expected bool }{ - "DM supported": {input: pkgConst.DBTypeDM, expected: true}, - "MySQL supported": {input: pkgConst.DBTypeMySQL, expected: true}, - "Oracle supported": {input: pkgConst.DBTypeOracle, expected: true}, - "OB MySQL supported": {input: pkgConst.DBTypeOceanBaseMySQL, expected: true}, - "TiDB supported": {input: pkgConst.DBTypeTiDB, expected: true}, - "TDSQL supported": {input: pkgConst.DBTypeTDSQLForInnoDB, expected: true}, - "GoldenDB supported": {input: pkgConst.DBTypeGoldenDB, expected: true}, - "PostgreSQL unsupported": {input: pkgConst.DBTypePostgreSQL, expected: false}, - "SQL Server unsupported": {input: pkgConst.DBTypeSQLServer, expected: false}, - "PolarDB For MySQL supported": {input: pkgConst.DBTypePolarDBForMySQL, expected: true}, + // 9 项 true:MySQL/Oracle/OceanBaseMySQL/DM/TiDB/TDSQLForInnoDB/GoldenDB/PolarDBForMySQL/PostgreSQL + "MySQL supported": {input: pkgConst.DBTypeMySQL, expected: true}, + "Oracle supported": {input: pkgConst.DBTypeOracle, expected: true}, + "OceanBase MySQL supported": {input: pkgConst.DBTypeOceanBaseMySQL, expected: true}, + "DM supported": {input: pkgConst.DBTypeDM, expected: true}, + "TiDB supported": {input: pkgConst.DBTypeTiDB, expected: true}, + "TDSQL For InnoDB supported": {input: pkgConst.DBTypeTDSQLForInnoDB, expected: true}, + "GoldenDB supported": {input: pkgConst.DBTypeGoldenDB, expected: true}, + "PolarDB For MySQL supported": {input: pkgConst.DBTypePolarDBForMySQL, expected: true}, + "PostgreSQL supported": {input: pkgConst.DBTypePostgreSQL, expected: true}, + // 3 项 false:SQL Server / 空字符串 / 未知类型 + "SQL Server unsupported": {input: pkgConst.DBTypeSQLServer, expected: false}, + "empty string unsupported": {input: pkgConst.DBType(""), expected: false}, + "unknown type unsupported": {input: pkgConst.DBType("UnknownDBType"), expected: false}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { From c2615ba168d7956fadab5761dbc840da35b3d267 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Wed, 20 May 2026 13:08:49 +0000 Subject: [PATCH 2/3] build: copy sqle-pg-plugin binary into dms-ee deploy artifact (issue #850, compat-RISK-4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs: #850 Risk: compat-RISK-4 在 dms-ee 部署流程中加入 sqle-pg-plugin 二进制拷贝,保证 SQLE plugin_path 在部署后包含 PG 审核插件二进制,闭环 compat-RISK-4 决策 B(必补覆盖)。 1) Makefile dms_sqle_provision_rpm_pre target - 新增 mkdir -p ./builddir/plugins,与既有 bin/config/static/scripts/ neo4j-community/lib mkdir 行同级 - 新增 cp ${PRE_DIR}sqle-pg-plugin/bin/sqle-pg-plugin ./builddir/plugins/sqle-pg-plugin,复用 sqle-pg-plugin Makefile install target 默认产物路径 $(GOBIN)/sqle-pg-plugin(GOBIN=$(shell pwd)/bin), 模式与 sqle/bin/sqled、provision/bin/provision 等同仓 plugin 上游拷贝一致 2) build/dms_sqle_provision.spec %install 阶段 - 新增 cp builddir/plugins/sqle-pg-plugin -> $RPM_BUILD_ROOT/usr/local/ %{name}/plugins/sqle-pg-plugin - 走单文件 cp 而非 cp -R 整目录,避免与已有 mkdir -p $RPM_BUILD_ROOT/usr/local/%{name}/plugins(行 40)冲突产生 plugins/plugins/ 嵌套;语义与 sqle-ee/build/sqled.spec 的 plugin 拷贝模式 等价,落地路径仍为 SQLE plugin_path 根 - 不动 %files / %pre / %post / %preun / %postun;既有 find $RPM_INSTALL_PREFIX/plugins -type f -exec chmod 0750 自然覆盖新二进制 设计依据:design.md §3.2 行 130-136 / §3.3 行 138-143 / §9.4 行 406 风险登记:docs/dev/compat_risks.md §1 compat-RISK-4 决策 B + case_ids 关联用例:case-pg-compat-010 / 011 / sqlaudit-002 / sqlaudit-003 / deploy-001 / deploy-002 不动:sqle-ee / sqle / sqle-pg-plugin 代码;vendor / go.mod / go.sum; skills/dms/script/**;任何 _ee.go / _ce.go;odc / odc-client;CE 仓库 (cherry picked from commit 12c0128b09fa0685a66dd7530eb44abba2657382) --- Makefile | 4 ++++ build/dms_sqle_provision.spec | 2 ++ 2 files changed, 6 insertions(+) diff --git a/Makefile b/Makefile index ffee44a8..62bc8273 100644 --- a/Makefile +++ b/Makefile @@ -119,6 +119,7 @@ dms_sqle_provision_rpm_pre: docker_install mkdir -p ./builddir/scripts mkdir -p ./builddir/neo4j-community mkdir -p ./builddir/lib + mkdir -p ./builddir/plugins # 前端文件 cp -R ${PRE_DIR}dms-ui/packages/base/dist/* ./builddir/static/ @@ -145,6 +146,9 @@ dms_sqle_provision_rpm_pre: docker_install cp ${PRE_DIR}sqle/scripts/sqled.systemd ./builddir/scripts/sqled.systemd cp -R ${PRE_DIR}sqle/jdk ./builddir/jdk + # sqle-pg-plugin 文件(compat-RISK-4 / issue #850) + cp ${PRE_DIR}sqle-pg-plugin/bin/sqle-pg-plugin ./builddir/plugins/sqle-pg-plugin + # 合并配置文件 touch ./builddir/config/config.yaml cat ./builddir/config/dms.yaml >> ./builddir/config/config.yaml diff --git a/build/dms_sqle_provision.spec b/build/dms_sqle_provision.spec index e9ba990b..3bc9f495 100644 --- a/build/dms_sqle_provision.spec +++ b/build/dms_sqle_provision.spec @@ -45,6 +45,8 @@ cp -R %{_builddir}/%{buildsubdir}/%{name}/builddir/static $RPM_BUILD_ROOT/usr/lo cp -R %{_builddir}/%{buildsubdir}/%{name}/builddir/neo4j-community $RPM_BUILD_ROOT/usr/local/%{name}/neo4j-community cp -R %{_builddir}/%{buildsubdir}/%{name}/builddir/lib $RPM_BUILD_ROOT/usr/local/%{name}/lib cp -R %{_builddir}/%{buildsubdir}/%{name}/builddir/jdk $RPM_BUILD_ROOT/usr/local/%{name}/jdk +# sqle-pg-plugin 二进制(compat-RISK-4 / issue #850) +cp %{_builddir}/%{buildsubdir}/%{name}/builddir/plugins/sqle-pg-plugin $RPM_BUILD_ROOT/usr/local/%{name}/plugins/sqle-pg-plugin ########## From 269bef3cc910b08b262d22a631bac5a5cb3efc02 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Fri, 22 May 2026 09:51:34 +0000 Subject: [PATCH 3/3] fix: make sql_workbench AuditMiddleware fail-open on auxiliary errors (issue #850, bug) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit streamExecute 反代到 ODC 时,AuditMiddleware 在以下辅助路径异常时直接 `return errors.New(...)`,被 dms 的 HTTPErrorHandler 默认映射为 HTTP 400 BadRequest,导致 ODC SQL Console 完全无法执行 SQL: - 解析用户 ID 失败 - 缓存表 sql_workbench_datasource_caches 查询失败 - 缓存中找不到 dms_db_service_id(用户首次走工作台 / 数据源未经 DMS 加载路径时缓存为空) - 获取 DBService 元数据失败 - 该 DBService 未启用 SQL 审核(最常见命中分支) - SQLE 审核服务调用失败(网络 / 超时 / 5xx) 修复:把上述 6 个分支统一改为 `return next(c)`(fail-open)— 审核能力是 增强项,未启用 / 缓存缺失 / SQLE 故障时应按裸 ODC 反代行为透传,而不应 阻塞用户的 SQL 执行。read body err 与「审核结果要求拦截」两条强约束分支 仍保留 fail-closed。同时把 Errorf / Debugf 调整为 Warnf 以便后续排障。 不影响: - 已启用 SQL 审核 + SQLE 调用成功 + 命中需审批规则的路径仍走 buildAuditResponseWithoutExecution,行为不变; - MySQL/PG/Oracle/OB-Mysql 等所有数据源类型均一致受益; - 不动 odc / odc-client / vendor / go.mod / pnpm-lock。 证据:docs/dev/fix-task-004-odc-streamExecute-400.md (cherry picked from commit fd802f139ec88ff33e26d21814039677092d9de0 of dms-ee, adapted to CE's sidInfo-based parseStreamExecuteRequest signature) --- .../service/sql_workbench_service.go | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/internal/sql_workbench/service/sql_workbench_service.go b/internal/sql_workbench/service/sql_workbench_service.go index 9dc55b42..b1a48e17 100644 --- a/internal/sql_workbench/service/sql_workbench_service.go +++ b/internal/sql_workbench/service/sql_workbench_service.go @@ -1043,6 +1043,24 @@ func makeHttpRequest(ctx context.Context, url string, headers map[string]string, } // AuditMiddleware 拦截工作台odc请求进行加工 +// +// 设计原则(issue #850, bug 修复): +// +// 本中间件提供的是「在 streamExecute 反代到 ODC 前叠加 SQLE 审核」的增强能力, +// 不是业务流的必经环节。当: +// - SQL/数据源 ID 解析失败; +// - 缓存 / 用户上下文缺失; +// - 该 DBService 未开启 SQL 审核; +// - SQLE 服务自身调用失败; +// +// 均应**透传放行**(fail-open,return next(c))让 ODC 继续执行用户的 SQL, +// 而不是把请求 400 掉、让用户连查询都跑不通。只有审核**明确返回需要拦截** +// 的结果(如规则违反需审批)才走 buildAuditResponseWithoutExecution 路径。 +// +// 修复前:未启用审核 / 缓存缺失 / 用户解析失败 等辅助路径异常均直接 +// `return errors.New(...)`,被 dms 的 HTTPErrorHandler 统一映射成 400, +// 导致 case-pg-mysql-baseline-001 等用例在 SQL Console 上完全无法运行(见 +// docs/dev/fix-task-004-odc-streamExecute-400.md)。 func (sqlWorkbenchService *SqlWorkbenchService) AuditMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { @@ -1054,7 +1072,8 @@ func (sqlWorkbenchService *SqlWorkbenchService) AuditMiddleware() echo.Middlewar // 读取请求体 bodyBytes, err := io.ReadAll(c.Request().Body) if err != nil { - sqlWorkbenchService.log.Errorf("failed to read request body: %v", err) + // body 读不出来无法叠加审核,但也无法继续构造反代请求;保留 fail-closed。 + sqlWorkbenchService.log.Errorf("failed to read streamExecute request body: %v", err) return errors.New(locale.Bundle.LocalizeMsgByCtx(c.Request().Context(), locale.SqlWorkbenchAuditReadReqBodyErr)) } // 恢复请求体,供后续处理使用 @@ -1063,7 +1082,6 @@ func (sqlWorkbenchService *SqlWorkbenchService) AuditMiddleware() echo.Middlewar // 解析请求体获取 SQL 和 datasource ID // 注意:解析仅服务于审核辅助路径,解析失败不应直接阻塞用户的 SQL 执行; // 否则一旦中间件辅助能力出错(如 sid 解码失败),用户连查询都跑不了。 - // 真正的「未启用审核 / 审核失败」等强策略仍由后续分支按既有 fail-closed 处理。 sql, sidInfo, err := sqlWorkbenchService.parseStreamExecuteRequest(bodyBytes) if err != nil { sqlWorkbenchService.log.Warnf("failed to parse streamExecute request, skipping audit: %v", err) @@ -1079,32 +1097,37 @@ func (sqlWorkbenchService *SqlWorkbenchService) AuditMiddleware() echo.Middlewar // 获取当前用户 ID dmsUserId, err := sqlWorkbenchService.getDMSUserIdFromRequest(c) if err != nil { - sqlWorkbenchService.log.Errorf("failed to get DMS user ID: %v", err) - return errors.New(locale.Bundle.LocalizeMsgByCtx(c.Request().Context(), locale.SqlWorkbenchAuditGetDMSUserErr)) + // 审计需要用户上下文,缺失时跳过审计而非阻塞执行(鉴权由前置 Login() 已经把关)。 + sqlWorkbenchService.log.Warnf("failed to get DMS user ID, skipping audit: %v", err) + return next(c) } // 从缓存表获取 dms_db_service_id dmsDBServiceID, err := sqlWorkbenchService.getDMSDBServiceIDFromCache(c.Request().Context(), datasourceID, dmsUserId) if err != nil { - sqlWorkbenchService.log.Errorf("failed to get dms_db_service_id from cache: %v", err) - return errors.New(locale.Bundle.LocalizeMsgByCtx(c.Request().Context(), locale.SqlWorkbenchAuditGetDBServiceMappingErr)) + // 缓存查询失败属于辅助路径异常,不应阻塞 SQL 执行。 + sqlWorkbenchService.log.Warnf("failed to get dms_db_service_id from cache, skipping audit: %v", err) + return next(c) } if dmsDBServiceID == "" { - sqlWorkbenchService.log.Debugf("dms_db_service_id not found in cache for datasource: %s", datasourceID) - return errors.New(locale.Bundle.LocalizeMsgByCtx(c.Request().Context(), locale.SqlWorkbenchAuditDBServiceMappingNotFoundErr)) + // 用户首次在工作台使用该数据源 / 数据源未走"通过 DMS 加载"路径时缓存为空,应放行。 + sqlWorkbenchService.log.Warnf("dms_db_service_id not found in cache for datasource=%s, skipping audit", datasourceID) + return next(c) } // 获取 DBService 信息 dbService, err := sqlWorkbenchService.dbServiceUsecase.GetDBService(c.Request().Context(), dmsDBServiceID) if err != nil { - sqlWorkbenchService.log.Errorf("failed to get DBService: %v", err) - return errors.New(locale.Bundle.LocalizeMsgByCtx(c.Request().Context(), locale.SqlWorkbenchAuditGetDBServiceErr)) + // DBService 元数据查询失败属于辅助路径异常,不应阻塞 SQL 执行。 + sqlWorkbenchService.log.Warnf("failed to get DBService %s, skipping audit: %v", dmsDBServiceID, err) + return next(c) } // 未开启 SQL 审核时直接放行,由 ODC 执行 SQL if !sqlWorkbenchService.isEnableSQLAudit(dbService) { - sqlWorkbenchService.log.Debugf("SQL audit is not enabled for DBService: %s", dmsDBServiceID) + // 未启用审核 = 该数据源没要求审核加强,按裸 ODC 反代行为放行。 + sqlWorkbenchService.log.Debugf("SQL audit is not enabled for DBService %s, skipping audit", dmsDBServiceID) return next(c) } @@ -1121,8 +1144,9 @@ func (sqlWorkbenchService *SqlWorkbenchService) AuditMiddleware() echo.Middlewar // 调用 SQLE 审核接口 auditResult, err := sqlWorkbenchService.callSQLEAudit(c.Request().Context(), sql, dbService, schemaName) if err != nil { - sqlWorkbenchService.log.Errorf("call SQLE audit failed: %v", err) - return errors.New(locale.Bundle.LocalizeMsgByCtx(c.Request().Context(), locale.SqlWorkbenchAuditCallSQLEErr)) + // SQLE 服务自身故障(连不上、超时等)不应让用户的 SQL 执行链路一起挂;放行并打 Warn 便于排障。 + sqlWorkbenchService.log.Warnf("call SQLE audit failed, skipping audit: %v", err) + return next(c) } // 拦截响应并添加审核结果