From a3def8f2d57d90058c769e2f00b5b80035ca6b6f Mon Sep 17 00:00:00 2001 From: WinfredLIN Date: Wed, 22 Apr 2026 08:53:08 +0000 Subject: [PATCH 01/16] ce feat: implement unmasking workflow use cases and data structures Added new files for unmasking workflow functionality, including use cases for creating, retrieving, and managing unmasking workflows. Introduced data structures for unmasking workflows, SQL content, and related operations, ensuring compatibility with both DMS and non-DMS builds. This implementation lays the groundwork for enhanced data masking capabilities. --- .../data_masking/biz/unmasking_workflow.go | 271 ++++++++++++++++++ .../data_masking/biz/unmasking_workflow_ce.go | 84 ++++++ 2 files changed, 355 insertions(+) create mode 100644 internal/data_masking/biz/unmasking_workflow.go create mode 100644 internal/data_masking/biz/unmasking_workflow_ce.go diff --git a/internal/data_masking/biz/unmasking_workflow.go b/internal/data_masking/biz/unmasking_workflow.go new file mode 100644 index 000000000..27b5f0ad4 --- /dev/null +++ b/internal/data_masking/biz/unmasking_workflow.go @@ -0,0 +1,271 @@ +package biz + +import ( + "context" + "time" + + maskingCore "github.com/actiontech/dms/internal/data_masking/core" +) + +// MaskingConfigStatus 脱敏配置状态 +// swagger:enum MaskingConfigStatus +type MaskingConfigStatus string + +const ( + MaskingConfigStatusPendingConfirm MaskingConfigStatus = "PENDING_CONFIRM" // 系统发现,待人工确认 + MaskingConfigStatusConfigured MaskingConfigStatus = "CONFIGURED" // 用户已确认/手动配置 + MaskingConfigStatusSystemConfirmed MaskingConfigStatus = "SYSTEM_CONFIRMED" // 系统已确认 +) + +// ColumnMaskingConfig 列级别脱敏配置领域模型 +// swagger:model ColumnMaskingConfig +type ColumnMaskingConfig struct { + // 配置记录 ID + ID uint `json:"id"` + // 数据源 UID + DBServiceUID string `json:"db_service_uid"` + // 列 ID(db_columns.id) + ColumnID uint `json:"column_id"` + // Schema 名称 + SchemaName string `json:"schema_name"` + // 表名 + TableName string `json:"table_name"` + // 列名 + ColumnName string `json:"column_name"` + // 是否启用脱敏 + IsMaskingEnabled bool `json:"is_masking_enabled"` + // 脱敏规则 ID + MaskingRuleID int `json:"masking_rule_id"` + // 脱敏规则名称(中文) + MaskingRuleName string `json:"masking_rule_name"` + // 置信度 + Confidence maskingCore.Confidence `json:"confidence,omitempty"` + // 配置状态 + Status MaskingConfigStatus `json:"status"` + // 创建时间 + CreatedAt time.Time `json:"created_at"` + // 更新时间 + UpdatedAt time.Time `json:"updated_at"` +} + +// swagger:model TableRef +type TableRef struct { + // Schema 名称 + Schema string `json:"schema"` + // 表名 + Table string `json:"table"` + // 表别名 + Alias string `json:"alias"` +} + +// swagger:model ColumnRef +type ColumnRef struct { + // Schema 名称 + Schema string `json:"schema"` + // 表名 + Table string `json:"table"` + // 列名 + Column string `json:"column"` +} + +// swagger:model ResultColumn +type ResultColumn struct { + // 结果列名 + Name string `json:"name"` + // 结果列表达式(SQL 片段) + Expression string `json:"expression"` + // 来源列列表 + Sources []ColumnRef `json:"sources"` +} + +// swagger:model LineageNode +type LineageNode struct { + // 节点 ID(图内唯一) + ID string `json:"id"` + // 节点类型 + Type NodeType `json:"type"` + // 节点展示名 + Name string `json:"name"` + // Schema 名称(列节点可能存在) + Schema string `json:"schema"` + // 表名(列节点可能存在) + Table string `json:"table"` + // 列名(列节点可能存在) + Column string `json:"column"` + // 表达式内容(表达式节点可能存在) + Expr string `json:"expr"` +} + +// swagger:model LineageEdge +type LineageEdge struct { + // 起点节点 ID + FromID string `json:"from_id"` + // 终点节点 ID + ToID string `json:"to_id"` + // 边类型 + Type EdgeType `json:"type"` +} + +// swagger:model AnalyzeResult +type AnalyzeResult struct { + // 分析标题/摘要 + Title string `json:"title"` + // 原始 SQL + OriginalSQL string `json:"original_sql"` + // 解析到的表引用列表 + Tables []TableRef `json:"tables"` + // 解析到的源列列表 + SourceColumns []ColumnRef `json:"source_columns"` + // 解析到的结果列列表 + ResultColumns []ResultColumn `json:"result_columns"` + // 血缘图节点 + Nodes []LineageNode `json:"nodes"` + // 血缘图边 + Edges []LineageEdge `json:"edges"` + // 警告信息(解析不完整等) + Warnings []string `json:"warnings,omitempty"` +} + +// NodeType 血缘节点类型 +// swagger:enum NodeType +type NodeType string + +const ( + // 源列节点 + NodeTypeSource NodeType = "source_column" + // 表达式节点 + NodeTypeExpression NodeType = "expression" + // 结果列节点 + NodeTypeResult NodeType = "result_column" + // 表节点 + NodeTypeTable NodeType = "table" +) + +// EdgeType 血缘边类型 +// swagger:enum EdgeType +type EdgeType string + +const ( + // 直接依赖 + EdgeTypeDirect EdgeType = "direct" + // 转换/计算依赖 + EdgeTypeTransform EdgeType = "transform" + // 聚合依赖 + EdgeTypeAggregate EdgeType = "aggregate" +) + +// UnmaskingWorkflowApprovalStatus 审批状态 +// swagger:enum UnmaskingWorkflowApprovalStatus +type UnmaskingWorkflowApprovalStatus string + +const ( + // 待审批 + UnmaskingWorkflowApprovalStatusPending UnmaskingWorkflowApprovalStatus = "pending" + // 已批准 + UnmaskingWorkflowApprovalStatusApproved UnmaskingWorkflowApprovalStatus = "approved" + // 已驳回 + UnmaskingWorkflowApprovalStatusRejected UnmaskingWorkflowApprovalStatus = "rejected" + // 已取消 + UnmaskingWorkflowApprovalStatusCancelled UnmaskingWorkflowApprovalStatus = "cancelled" +) + +func (s UnmaskingWorkflowApprovalStatus) String() string { + return string(s) +} + +// UnmaskingWorkflowUsageStatus 使用情况 +// swagger:enum UnmaskingWorkflowUsageStatus +type UnmaskingWorkflowUsageStatus string + +const ( + // 未查看 + UnmaskingWorkflowUsageStatusUnviewed UnmaskingWorkflowUsageStatus = "unviewed" + // 已查看 + UnmaskingWorkflowUsageStatusViewed UnmaskingWorkflowUsageStatus = "viewed" +) + +func (s UnmaskingWorkflowUsageStatus) String() string { + return string(s) +} + +// UnmaskingWorkflowSourceType 来源类型 +// swagger:enum UnmaskingWorkflowSourceType +type UnmaskingWorkflowSourceType string + +const ( + // 数据导出工单 + UnmaskingWorkflowSourceTypeDataExport UnmaskingWorkflowSourceType = "data_export" + // SQL工作台 + UnmaskingWorkflowSourceTypeSQLWorkbench UnmaskingWorkflowSourceType = "sql_workbench" +) + +func (s UnmaskingWorkflowSourceType) String() string { + return string(s) +} + +// UnmaskingOriginalDataCredentialArgs 工单凭证路径参数,供 CheckOriginalDataAccess 使用;nil 表示仅做权限直通(不入库解析工单)。enterprise 与 dms 构建共用。 +type UnmaskingOriginalDataCredentialArgs struct { + SourceType UnmaskingWorkflowSourceType + SourceUID string + Credential string +} + +// UnmaskingAction 操作动作类型 +// swagger:enum UnmaskingAction +type UnmaskingAction string + +const ( + // 提交申请 + UnmaskingActionSubmit UnmaskingAction = "submit" + // 批准申请 + UnmaskingActionApprove UnmaskingAction = "approve" + // 驳回申请 + UnmaskingActionReject UnmaskingAction = "reject" + // 查看工单详情 + UnmaskingActionViewOriginalDataWorkflowDetail UnmaskingAction = "view_unmasking_workflow_detail" + // 查看原文 + UnmaskingActionViewOriginalData UnmaskingAction = "view_full_original_data" + // 下载原文 + UnmaskingActionDownloadOriginalData UnmaskingAction = "download_full_original_data" + // 取消申请 + UnmaskingActionCancel UnmaskingAction = "cancel" +) + +func (a UnmaskingAction) String() string { + return string(a) +} + +// 与 DMS OpRangeType 字符串取值一致,用于 UnmaskingOpPermissionRange 判定。 +const ( + UnmaskingOpRangeProject = "project" + UnmaskingOpRangeDBService = "db_service" +) + +// UnmaskingOpPermissionRange 查看原文工单权限判定用的「操作权限 + 范围」快照。 +type UnmaskingOpPermissionRange struct { + OpPermissionUID string + OpRangeType string + RangeUIDs []string +} + +// UnmaskingWorkflowOpPermissionVerifier 操作权限校验能力。 +type UnmaskingWorkflowOpPermissionVerifier interface { + IsUserDMSAdmin(ctx context.Context, userUID string) (bool, error) + CanOpGlobal(ctx context.Context, userUID string) (bool, error) + CanViewGlobal(ctx context.Context, userUID string) (bool, error) + IsUserProjectAdmin(ctx context.Context, userUID, projectUID string) (bool, error) + GetUserOpPermissionInProject(ctx context.Context, userUID, projectUID string) ([]UnmaskingOpPermissionRange, error) + GetCanOpDBUsers(ctx context.Context, projectUID, dbServiceUID string, needOpPermissionTypes []string) ([]string, error) +} + +// UnmaskingWorkflowUserDirectory 用户领域:工单列表/详情等场景将用户 UID 解析为展示名。 +// 由 DMS service 层适配 UserUsecase 实现。 +type UnmaskingWorkflowUserDirectory interface { + GetUserNamesByUIDs(ctx context.Context, uids []string) (map[string]string, error) +} + +// UnmaskingWorkflowDBServiceDirectory 数据源领域:将 DBService UID 解析为实例名称。 +// 由 DMS service 层适配 DBServiceUsecase 实现。 +type UnmaskingWorkflowDBServiceDirectory interface { + GetDBServiceNamesByUIDs(ctx context.Context, uids []string) (map[string]string, error) +} diff --git a/internal/data_masking/biz/unmasking_workflow_ce.go b/internal/data_masking/biz/unmasking_workflow_ce.go new file mode 100644 index 000000000..2ccb3e44d --- /dev/null +++ b/internal/data_masking/biz/unmasking_workflow_ce.go @@ -0,0 +1,84 @@ +//go:build !dms + +package biz + +import ( + "context" +) + +type UnmaskingWorkflowUsecase struct { +} + +// UnmaskingSQL 非 dms 构建占位;与 enterprise 中间件读取的字段对齐。 +type UnmaskingSQL struct { + UID string + SQLContent string +} + +// UnmaskingWorkflow 非 dms 构建下的占位类型;enterprise 会读 UID / ProjectUID 等(如 data_export_workflow_ee)。 +type UnmaskingWorkflow struct { + UID string + ProjectUID string + ApplicantUID string + UnmaskingSQLs []*UnmaskingSQL +} + +type CreateUnmaskingWorkflowArgs struct{} + +type UnmaskingDBConfig struct{} + +type ListUnmaskingWorkflowsOption struct{} + +func NewUnmaskingWorkflowUsecase(ctx context.Context) *UnmaskingWorkflowUsecase { + return &UnmaskingWorkflowUsecase{} +} + +func (u *UnmaskingWorkflowUsecase) CreateUnmaskingWorkflow(ctx context.Context, args *CreateUnmaskingWorkflowArgs) (string, error) { + return "", nil +} + +func (u *UnmaskingWorkflowUsecase) GetUnmaskingWorkflow(ctx context.Context, workflowUID string) (*UnmaskingWorkflow, error) { + return nil, nil +} + +func (u *UnmaskingWorkflowUsecase) ListUnmaskingWorkflows(ctx context.Context, opt *ListUnmaskingWorkflowsOption) ([]*UnmaskingWorkflow, error) { + return nil, nil +} + +func (u *UnmaskingWorkflowUsecase) ApproveUnmaskingWorkflow(ctx context.Context, workflowUID string) error { + return nil +} + +func (u *UnmaskingWorkflowUsecase) RejectUnmaskingWorkflow(ctx context.Context, workflowUID string) error { + return nil +} + +func (u *UnmaskingWorkflowUsecase) CancelUnmaskingWorkflow(ctx context.Context, workflowUID string) error { + return nil +} + +func (u *UnmaskingWorkflowUsecase) GetUnmaskingWorkflowAssignees(ctx context.Context, projectUID, datasourceUID, applicantUID string) ([]string, error) { + return nil, nil +} + +func (u *UnmaskingWorkflowUsecase) CheckOriginalDataAccess(ctx context.Context, projectUID, datasourceUID, userUID string, credential *UnmaskingOriginalDataCredentialArgs) (bool, *UnmaskingWorkflow, error) { + return false, nil, nil +} + +// MarkWorkflowUsage 完整实现仅在 dms 构建中提供;非 dms 占位。 +func (u *UnmaskingWorkflowUsecase) MarkWorkflowUsage(_ context.Context, _, _, _ string, _ UnmaskingAction) error { + return nil +} + +// GetUnmaskingWorkflowDetail 完整实现仅在 dms 构建中提供;非 dms 占位(运行时 enterprise 且未启用 dms 时 usecase 一般为 nil)。 +func (u *UnmaskingWorkflowUsecase) GetUnmaskingWorkflowDetail(_ context.Context, _, _, _ string) (*UnmaskingWorkflow, error) { + return &UnmaskingWorkflow{}, nil +} + +// AnalyzeLineageAndBuildMaskingSnapshot 完整实现仅在 dms 构建中提供;非 dms 构建占位以满足跨标签编译。 +func (u *UnmaskingWorkflowUsecase) AnalyzeLineageAndBuildMaskingSnapshot( + _ context.Context, + _, _, _, _ string, +) (*AnalyzeResult, []*ColumnMaskingConfig) { + return nil, nil +} From 447043adf8ab4954589c1e6b6e77eb679466f3a0 Mon Sep 17 00:00:00 2001 From: WinfredLIN Date: Wed, 22 Apr 2026 08:58:11 +0000 Subject: [PATCH 02/16] ce refactor: move Confidence type and constants to a new file for better organization Refactored the Confidence type and its associated constants by moving them from types_ee.go to a newly created types.go file. This change improves code organization and clarity, making it easier to manage and understand the confidence levels used in sensitive data detection. --- internal/data_masking/core/types.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 internal/data_masking/core/types.go diff --git a/internal/data_masking/core/types.go b/internal/data_masking/core/types.go new file mode 100644 index 000000000..8df3416dd --- /dev/null +++ b/internal/data_masking/core/types.go @@ -0,0 +1,14 @@ +package core + +// Confidence 置信度 (例如: High, Medium, Low) +// swagger:enum Confidence +type Confidence string + +const ( + // 高:高度确信为敏感数据 + ConfidenceHigh Confidence = "High" + // 中:中等确信为敏感数据 + ConfidenceMedium Confidence = "Medium" + // 低:低确信为敏感数据 + ConfidenceLow Confidence = "Low" +) From a22b3e9c4d192dd0acd88ee40b17bc973e6bd885 Mon Sep 17 00:00:00 2001 From: WinfredLIN Date: Wed, 22 Apr 2026 09:05:38 +0000 Subject: [PATCH 03/16] ce feat: implement DownloadOriginalDataExportWorkflow functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据导出集成查看原文工单功能 Added the DownloadOriginalDataExportWorkflow method to the DataExportWorkflowUsecase, enabling the download of unmasked data export workflows as a zip file. This includes validation checks for project UID, workflow status, and user permissions. Updated related service files to support this functionality, ensuring compatibility with both enterprise and community editions. Enhanced error handling and logging for better traceability. --- .../apiserver/service/data_mask_controller.go | 344 ++++++++++++++++-- internal/dms/biz/data_export_workflow.go | 6 +- internal/dms/biz/data_export_workflow_ce.go | 4 + internal/dms/service/data_export_workflow.go | 13 + .../dms/service/data_export_workflow_ce.go | 16 + internal/dms/service/service.go | 4 +- 6 files changed, 350 insertions(+), 37 deletions(-) create mode 100644 internal/dms/service/data_export_workflow_ce.go diff --git a/internal/apiserver/service/data_mask_controller.go b/internal/apiserver/service/data_mask_controller.go index 560b9ab8b..65b0d167e 100644 --- a/internal/apiserver/service/data_mask_controller.go +++ b/internal/apiserver/service/data_mask_controller.go @@ -1,8 +1,11 @@ package service import ( + "errors" + aV1 "github.com/actiontech/dms/api/dms/service/v1" apiError "github.com/actiontech/dms/internal/apiserver/pkg/error" + pkgConst "github.com/actiontech/dms/internal/dms/pkg/constant" "github.com/actiontech/dms/pkg/dms-common/api/jwt" "github.com/labstack/echo/v4" ) @@ -48,6 +51,35 @@ func (ctl *DMSController) ListMaskingTemplates(c echo.Context) error { return NewOkRespWithReply(c, reply) } +// swagger:route GET /v1/dms/projects/{project_uid}/db_services/{db_service_uid}/schemas/{schema_name}/tables/{table_name}/columns DBStructure ListTableColumns +// +// List table columns (internal API for lineage analysis). +// +// responses: +// 200: body:ListTableColumnsReply +// default: body:GenericResp +func (ctl *DMSController) ListTableColumns(c echo.Context) error { + // 内部接口,仅允许sys/admin用户访问 + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + if currentUserUid != pkgConst.UIDOfUserSys && currentUserUid != pkgConst.UIDOfUserAdmin { + return NewErrResp(c, errors.New("insufficient permission"), apiError.UnauthorizedErr) + } + + req := new(aV1.ListTableColumnsReq) + if err := bindAndValidateReq(c, req); err != nil { + return NewErrResp(c, err, apiError.BadRequestErr) + } + + reply, err := ctl.DMS.ListTableColumns(c.Request().Context(), req) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, reply) +} + // swagger:operation POST /v1/dms/projects/{project_uid}/masking/templates Masking AddMaskingTemplate // // 新增脱敏模板。 @@ -468,75 +500,317 @@ func (ctl *DMSController) GetTableColumnMaskingDetails(c echo.Context) error { return NewOkRespWithReply(c, reply) } -// swagger:route GET /v1/dms/projects/{project_uid}/masking/approval-requests/pending Masking ListPendingApprovalRequests +// swagger:operation POST /v1/dms/projects/{project_uid}/masking/unmasking-workflows Masking CreateUnmaskingWorkflow // -// 查询待审批申请列表。 +// Create unmasking workflow. // -// responses: -// 200: body:ListPendingApprovalRequestsReply -// default: body:GenericResp -func (ctl *DMSController) ListPendingApprovalRequests(c echo.Context) error { - req := &aV1.ListPendingApprovalRequestsReq{} +// --- +// parameters: +// - name: project_uid +// description: project id +// in: path +// required: true +// type: string +// - name: unmasking_workflow +// description: unmasking workflow info +// in: body +// required: true +// schema: +// "$ref": "#/definitions/CreateUnmaskingWorkflowReq" +// +// responses: +// +// '200': +// description: Create unmasking workflow successfully +// schema: +// "$ref": "#/definitions/CreateUnmaskingWorkflowReply" +// default: +// description: Generic error response +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) CreateUnmaskingWorkflow(c echo.Context) error { + req := &aV1.CreateUnmaskingWorkflowReq{} if err := bindAndValidateReq(c, req); err != nil { return NewErrResp(c, err, apiError.BadRequestErr) } - return NewOkRespWithReply(c, &aV1.ListPendingApprovalRequestsReply{}) + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.UnauthorizedErr) + } + + reply, err := ctl.DMS.CreateUnmaskingWorkflow(c.Request().Context(), req, currentUserUid) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + return NewOkRespWithReply(c, reply) } -// swagger:route GET /v1/dms/projects/{project_uid}/masking/approval-requests/{request_id} Masking GetPlaintextAccessRequestDetail +// swagger:operation GET /v1/dms/projects/{project_uid}/masking/unmasking-workflows Masking ListUnmaskingWorkflows // -// 获取明文访问申请详情。 +// List unmasking workflows. // -// responses: -// 200: body:GetPlaintextAccessRequestDetailReply -// default: body:GenericResp -func (ctl *DMSController) GetPlaintextAccessRequestDetail(c echo.Context) error { - req := &aV1.GetPlaintextAccessRequestDetailReq{} +// --- +// parameters: +// - name: project_uid +// description: project id +// in: path +// required: true +// type: string +// - name: page_size +// description: the maximum count of workflows to be returned +// in: query +// required: true +// type: integer +// format: uint32 +// - name: page_index +// description: the offset of workflows to be returned, default is 0 +// in: query +// required: false +// type: integer +// format: uint32 +// - name: filter_by_approval_status +// description: filter the approval status +// in: query +// required: false +// type: string +// enum: [pending, approved, rejected, cancelled] +// - name: filter_by_usage_status +// description: filter the usage status +// in: query +// required: false +// type: string +// enum: [unviewed, viewed] +// - name: filter_by_db_service_uid +// description: filter db_service id +// in: query +// required: false +// type: string +// +// responses: +// +// '200': +// description: List unmasking workflows successfully +// schema: +// "$ref": "#/definitions/ListUnmaskingWorkflowsReply" +// default: +// description: Generic error response +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) ListUnmaskingWorkflows(c echo.Context) error { + req := &aV1.ListUnmaskingWorkflowsReq{} if err := bindAndValidateReq(c, req); err != nil { return NewErrResp(c, err, apiError.BadRequestErr) } - return NewOkRespWithReply(c, &aV1.GetPlaintextAccessRequestDetailReply{}) + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.UnauthorizedErr) + } + + reply, err := ctl.DMS.ListUnmaskingWorkflows(c.Request().Context(), req, currentUserUid) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + return NewOkRespWithReply(c, reply) } -// swagger:operation POST /v1/dms/projects/{project_uid}/masking/approval-requests/{request_id}/decisions Masking ProcessApprovalRequest +// swagger:operation GET /v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id} Masking GetUnmaskingWorkflow // -// 处理审批申请。 +// Get unmasking workflow detail. // // --- // parameters: // - name: project_uid -// description: 项目 UID +// description: project id // in: path // required: true // type: string -// - name: request_id -// description: 审批申请 ID +// - name: workflow_id +// description: workflow id // in: path // required: true -// type: integer -// - name: action -// description: 处理动作信息 +// type: string +// +// responses: +// +// '200': +// description: Get unmasking workflow detail successfully +// schema: +// "$ref": "#/definitions/GetUnmaskingWorkflowReply" +// default: +// description: Generic error response +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) GetUnmaskingWorkflow(c echo.Context) error { + req := &aV1.GetUnmaskingWorkflowReq{} + if err := bindAndValidateReq(c, req); err != nil { + return NewErrResp(c, err, apiError.BadRequestErr) + } + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.UnauthorizedErr) + } + + reply, err := ctl.DMS.GetUnmaskingWorkflow(c.Request().Context(), req, currentUserUid) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + return NewOkRespWithReply(c, reply) +} + +// swagger:operation POST /v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/approve Masking ApproveUnmaskingWorkflow +// +// Approve unmasking workflow. +// +// --- +// parameters: +// - name: project_uid +// description: project id +// in: path +// required: true +// type: string +// - name: workflow_id +// description: workflow id +// in: path +// required: true +// type: string +// - name: approve_unmasking_workflow +// description: approve unmasking workflow info // in: body // required: true // schema: -// "$ref": "#/definitions/ProcessApprovalRequestReq" +// "$ref": "#/definitions/ApproveUnmaskingWorkflow" // // responses: // -// '200': -// description: 成功处理审批申请 -// schema: -// "$ref": "#/definitions/ProcessApprovalRequestReply" -// default: -// description: 通用错误响应 +// '200': +// description: Approve unmasking workflow successfully +// schema: +// "$ref": "#/definitions/ApproveUnmaskingWorkflowReply" +// default: +// description: Generic error response +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) ApproveUnmaskingWorkflow(c echo.Context) error { + req := &aV1.ApproveUnmaskingWorkflowReq{} + if err := bindAndValidateReq(c, req); err != nil { + return NewErrResp(c, err, apiError.BadRequestErr) + } + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.UnauthorizedErr) + } + + err = ctl.DMS.ApproveUnmaskingWorkflow(c.Request().Context(), req, currentUserUid) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + return NewOkRespWithReply(c, &aV1.ApproveUnmaskingWorkflowReply{}) +} + +// swagger:operation POST /v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/reject Masking RejectUnmaskingWorkflow +// +// Reject unmasking workflow. +// +// --- +// parameters: +// - name: project_uid +// description: project id +// in: path +// required: true +// type: string +// - name: workflow_id +// description: workflow id +// in: path +// required: true +// type: string +// - name: reject_unmasking_workflow +// description: reject unmasking workflow info +// in: body +// required: true // schema: -// "$ref": "#/definitions/GenericResp" -func (ctl *DMSController) ProcessApprovalRequest(c echo.Context) error { - req := &aV1.ProcessApprovalRequestReq{} +// "$ref": "#/definitions/RejectUnmaskingWorkflow" +// +// responses: +// +// '200': +// description: Reject unmasking workflow successfully +// schema: +// "$ref": "#/definitions/RejectUnmaskingWorkflowReply" +// default: +// description: Generic error response +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) RejectUnmaskingWorkflow(c echo.Context) error { + req := &aV1.RejectUnmaskingWorkflowReq{} if err := bindAndValidateReq(c, req); err != nil { return NewErrResp(c, err, apiError.BadRequestErr) } - return NewOkRespWithReply(c, &aV1.ProcessApprovalRequestReply{}) + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.UnauthorizedErr) + } + + err = ctl.DMS.RejectUnmaskingWorkflow(c.Request().Context(), req, currentUserUid) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + return NewOkRespWithReply(c, &aV1.RejectUnmaskingWorkflowReply{}) +} + +// swagger:operation POST /v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/cancel Masking CancelUnmaskingWorkflow +// +// Cancel unmasking workflow. +// +// --- +// parameters: +// - name: project_uid +// description: project id +// in: path +// required: true +// type: string +// - name: workflow_id +// description: workflow id +// in: path +// required: true +// type: string +// +// responses: +// +// '200': +// description: Cancel unmasking workflow successfully +// schema: +// "$ref": "#/definitions/CancelUnmaskingWorkflowReply" +// default: +// description: Generic error response +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) CancelUnmaskingWorkflow(c echo.Context) error { + req := &aV1.CancelUnmaskingWorkflowReq{} + if err := bindAndValidateReq(c, req); err != nil { + return NewErrResp(c, err, apiError.BadRequestErr) + } + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.UnauthorizedErr) + } + + err = ctl.DMS.CancelUnmaskingWorkflow(c.Request().Context(), req, currentUserUid) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + return NewOkRespWithReply(c, &aV1.CancelUnmaskingWorkflowReply{}) } // swagger:route GET /v1/dms/projects/{project_uid}/masking/rules/{rule_id} Masking GetMaskingRuleDetail diff --git a/internal/dms/biz/data_export_workflow.go b/internal/dms/biz/data_export_workflow.go index 148acaf2c..a574bedb2 100644 --- a/internal/dms/biz/data_export_workflow.go +++ b/internal/dms/biz/data_export_workflow.go @@ -8,6 +8,8 @@ import ( pkgConst "github.com/actiontech/dms/internal/dms/pkg/constant" dmsCommonV1 "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" + + dataMaskingBiz "github.com/actiontech/dms/internal/data_masking/biz" ) var ErrDataExportWorkflowNameDuplicate = errors.New("data export workflow name duplicate") @@ -135,11 +137,12 @@ type DataExportWorkflowUsecase struct { userUsecase *UserUsecase systemVariableUsecase *SystemVariableUsecase maskingTaskRepo MaskingTaskRepo + unmaskingWorkflowUsecase *dataMaskingBiz.UnmaskingWorkflowUsecase log *utilLog.Helper reportHost string } -func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator, repo WorkflowRepo, dataExportTaskRepo DataExportTaskRepo, dbServiceRepo DBServiceRepo, maskingConfigRepo DataExportMaskingConfigRepo, maskingRuleRepo DataExportMaskingRuleRepo, opPermissionVerifyUsecase *OpPermissionVerifyUsecase, projectUsecase *ProjectUsecase, proxyTargetRepo ProxyTargetRepo, clusterUseCase *ClusterUsecase, webhookUsecase *WebHookConfigurationUsecase, userUsecase *UserUsecase, systemVariableUsecase *SystemVariableUsecase, maskingTaskRepo MaskingTaskRepo, reportHost string) *DataExportWorkflowUsecase { +func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator, repo WorkflowRepo, dataExportTaskRepo DataExportTaskRepo, dbServiceRepo DBServiceRepo, maskingConfigRepo DataExportMaskingConfigRepo, maskingRuleRepo DataExportMaskingRuleRepo, opPermissionVerifyUsecase *OpPermissionVerifyUsecase, projectUsecase *ProjectUsecase, proxyTargetRepo ProxyTargetRepo, clusterUseCase *ClusterUsecase, webhookUsecase *WebHookConfigurationUsecase, userUsecase *UserUsecase, systemVariableUsecase *SystemVariableUsecase, maskingTaskRepo MaskingTaskRepo, unmaskingWorkflowUsecase *dataMaskingBiz.UnmaskingWorkflowUsecase, reportHost string) *DataExportWorkflowUsecase { return &DataExportWorkflowUsecase{ tx: tx, repo: repo, @@ -155,6 +158,7 @@ func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator userUsecase: userUsecase, systemVariableUsecase: systemVariableUsecase, maskingTaskRepo: maskingTaskRepo, + unmaskingWorkflowUsecase: unmaskingWorkflowUsecase, log: utilLog.NewHelper(logger, utilLog.WithMessageKey("biz.dataExportWorkflow")), reportHost: reportHost, } diff --git a/internal/dms/biz/data_export_workflow_ce.go b/internal/dms/biz/data_export_workflow_ce.go index 181b39540..f3116ab1e 100644 --- a/internal/dms/biz/data_export_workflow_ce.go +++ b/internal/dms/biz/data_export_workflow_ce.go @@ -39,6 +39,10 @@ func (d *DataExportWorkflowUsecase) ExportDataExportWorkflow(ctx context.Context return errNotDataExportWorkflow } +func (d *DataExportWorkflowUsecase) DownloadOriginalDataExportWorkflow(ctx context.Context, projectUid, workflowUid, currentUserUid, unmaskingWorkflowUid string) (string, []byte, error) { + return "", nil, errNotDataExportWorkflow +} + func (d *DataExportWorkflowUsecase) DownloadDataExportTask(ctx context.Context, userId string, req *dmsV1.DownloadDataExportTaskReq) (bool, string, error) { return false, "", errNotDataExportTask } diff --git a/internal/dms/service/data_export_workflow.go b/internal/dms/service/data_export_workflow.go index e02b6a1e5..f1a2e3eb8 100644 --- a/internal/dms/service/data_export_workflow.go +++ b/internal/dms/service/data_export_workflow.go @@ -248,6 +248,8 @@ func (d *DMSService) GetDataExportWorkflow(ctx context.Context, req *dmsV1.GetDa data.WorkflowRecord.Steps = append(data.WorkflowRecord.Steps, step) } + d.fillGetDataExportUnmaskingWorkflowSummary(ctx, req.DataExportWorkflowUid, data) + return &dmsV1.GetDataExportWorkflowReply{ Data: data, }, nil @@ -337,6 +339,12 @@ func (d *DMSService) ListDataExportTaskSQLs(ctx context.Context, req *dmsV1.List return nil, err } + tasks, err := d.DataExportWorkflowUsecase.BatchGetDataExportTask(ctx, []string{req.DataExportTaskUid}) + if err != nil || len(tasks) == 0 { + return nil, fmt.Errorf("failed to get data export task: %w", err) + } + task := tasks[0] + ret := make([]*dmsV1.ListDataExportTaskSQL, len(taskRecords)) for i, w := range taskRecords { ret[i] = &dmsV1.ListDataExportTaskSQL{ @@ -346,6 +354,11 @@ func (d *DMSService) ListDataExportTaskSQLs(ctx context.Context, req *dmsV1.List ExportResult: w.ExportResult, ExportSQLType: w.ExportSQLType, } + if d.UnmaskingWorkflowUsecase != nil { + lineage, snapshot := d.UnmaskingWorkflowUsecase.AnalyzeLineageAndBuildMaskingSnapshot(ctx, req.ProjectUid, task.DBServiceUid, task.DatabaseName, w.ExportSQL) + ret[i].LineageAnalysisSnapshot = lineage + ret[i].MaskingConfigSnapshot = snapshot + } if w.AuditSQLResults != nil { for _, result := range w.AuditSQLResults { ret[i].AuditSQLResult = append(ret[i].AuditSQLResult, dmsV1.AuditSQLResult{ diff --git a/internal/dms/service/data_export_workflow_ce.go b/internal/dms/service/data_export_workflow_ce.go new file mode 100644 index 000000000..67de1d381 --- /dev/null +++ b/internal/dms/service/data_export_workflow_ce.go @@ -0,0 +1,16 @@ +//go:build !dms + +package service + +import ( + "context" + "errors" + + dmsV1 "github.com/actiontech/dms/api/dms/service/v1" +) + +func (d *DMSService) DownloadOriginalDataExportWorkflow(ctx context.Context, req *dmsV1.DownloadOriginalDataExportWorkflowReq, currentUserUid string) (string, []byte, error) { + return "", nil, errors.New("export original data is an enterprise version function") +} + +func (d *DMSService) fillGetDataExportUnmaskingWorkflowSummary(_ context.Context, _ string, _ *dmsV1.GetDataExportWorkflow) {} diff --git a/internal/dms/service/service.go b/internal/dms/service/service.go index b0f23dfb5..4f9156954 100644 --- a/internal/dms/service/service.go +++ b/internal/dms/service/service.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/actiontech/dms/internal/apiserver/conf" + maskingBiz "github.com/actiontech/dms/internal/data_masking/biz" "github.com/actiontech/dms/internal/dms/biz" "github.com/actiontech/dms/internal/dms/storage" @@ -43,6 +44,7 @@ type DMSService struct { DataExportWorkflowUsecase *biz.DataExportWorkflowUsecase CbOperationLogUsecase *biz.CbOperationLogUsecase DataMaskingUsecase *dataMaskingUsecase + UnmaskingWorkflowUsecase *maskingBiz.UnmaskingWorkflowUsecase FunctionSupportRegistry *biz.FunctionSupportRegistry AuthAccessTokenUseCase *biz.AuthAccessTokenUsecase SwaggerUseCase *biz.SwaggerUseCase @@ -153,7 +155,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer workflowRepo := storage.NewWorkflowRepo(logger, st) dataExportMaskingConfigRepo := initDataExportMaskingConfigRepo(logger, st) dataExportMaskingRuleRepo := initDataExportMaskingRuleRepo(logger, st) - DataExportWorkflowUsecase := biz.NewDataExportWorkflowUsecase(logger, tx, workflowRepo, dataExportTaskRepo, dbServiceRepo, dataExportMaskingConfigRepo, dataExportMaskingRuleRepo, opPermissionVerifyUsecase, projectUsecase, dmsProxyTargetRepo, clusterUsecase, webhookConfigurationUsecase, userUsecase, systemVariableUsecase, discoveryTaskRepo, fmt.Sprintf("%s:%d", opts.ReportHost, opts.APIServiceOpts.Port)) + DataExportWorkflowUsecase := biz.NewDataExportWorkflowUsecase(logger, tx, workflowRepo, dataExportTaskRepo, dbServiceRepo, dataExportMaskingConfigRepo, dataExportMaskingRuleRepo, opPermissionVerifyUsecase, projectUsecase, dmsProxyTargetRepo, clusterUsecase, webhookConfigurationUsecase, userUsecase, systemVariableUsecase, discoveryTaskRepo, nil, fmt.Sprintf("%s:%d", opts.ReportHost, opts.APIServiceOpts.Port)) dataMaskingUsecase, stopDataMaskingScheduler, err := initDataMaskingUsecase(logger, st, dbServiceUseCase, clusterUsecase, dmsProxyTargetRepo) if err != nil { return nil, fmt.Errorf("failed to initialize data masking usecase: %v", err) From 6f86d9a900d5282c700bb9153f899eefdde05b4d Mon Sep 17 00:00:00 2001 From: yangzhongjiao Date: Mon, 11 May 2026 12:08:44 +0000 Subject: [PATCH 04/16] ce feat: DMSService: add UnmaskingWorkflowUsecase field for export original (CE) Owns the service struct wiring previously marked ee; pairs with biz/data_export EE implementation. --- internal/dms/service/service.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/dms/service/service.go b/internal/dms/service/service.go index 4f9156954..f6a027883 100644 --- a/internal/dms/service/service.go +++ b/internal/dms/service/service.go @@ -42,9 +42,9 @@ type DMSService struct { LicenseUsecase *biz.LicenseUsecase ClusterUsecase *biz.ClusterUsecase DataExportWorkflowUsecase *biz.DataExportWorkflowUsecase + UnmaskingWorkflowUsecase *maskingBiz.UnmaskingWorkflowUsecase CbOperationLogUsecase *biz.CbOperationLogUsecase DataMaskingUsecase *dataMaskingUsecase - UnmaskingWorkflowUsecase *maskingBiz.UnmaskingWorkflowUsecase FunctionSupportRegistry *biz.FunctionSupportRegistry AuthAccessTokenUseCase *biz.AuthAccessTokenUsecase SwaggerUseCase *biz.SwaggerUseCase @@ -204,6 +204,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer LicenseUsecase: LicenseUsecase, ClusterUsecase: clusterUsecase, DataExportWorkflowUsecase: DataExportWorkflowUsecase, + UnmaskingWorkflowUsecase: nil, CbOperationLogUsecase: CbOperationLogUsecase, DataMaskingUsecase: dataMaskingUsecase, FunctionSupportRegistry: functionSupportRegistry, From 020b41503c8b0b8ca8bb6c82a739972e4c15f631 Mon Sep 17 00:00:00 2001 From: WinfredLIN Date: Wed, 22 Apr 2026 09:07:54 +0000 Subject: [PATCH 05/16] ce feat: enhance SQL result masking with project UID support Updated the SQLResultMasker interface to include projectUID as a parameter for masking SQL results. Modified the CloudbeaverUsecase to pass projectUID during data masking operations. Introduced a new SQLResultMasker implementation for SQL workbench results, allowing for improved context and handling of masking operations. Refactored response writer to ensure consistent handling of response statuses and headers. --- internal/dms/biz/cloudbeaver.go | 9 ++- .../service/sql_workbench_service.go | 62 +++++++++++-------- .../sql_workbench/sqlresultmasker/masker.go | 18 ++++++ 3 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 internal/sql_workbench/sqlresultmasker/masker.go diff --git a/internal/dms/biz/cloudbeaver.go b/internal/dms/biz/cloudbeaver.go index 24766f0f7..1ab04ff61 100644 --- a/internal/dms/biz/cloudbeaver.go +++ b/internal/dms/biz/cloudbeaver.go @@ -65,7 +65,7 @@ func (c CloudbeaverConnection) PrimaryKey() string { } type SQLResultMasker interface { - MaskSQLResults(ctx context.Context, result *model.SQLExecuteInfo, dbServiceUID, schemaName string) error + MaskSQLResults(ctx context.Context, result *model.SQLExecuteInfo, dbServiceUID, schemaName, projectUID string) error } type CloudbeaverRepo interface { @@ -339,6 +339,7 @@ type taskMaskingContext struct { Enabled bool DBServiceUID string SchemaName string + ProjectUID string } var ( @@ -471,6 +472,7 @@ func (cu *CloudbeaverUsecase) GraphQLDistributor() echo.MiddlewareFunc { return cu.buildTaskIdAssocDataMasking(cloudbeaverResBuf.Bytes(), taskMaskingContext{ Enabled: isMaskingEnabled, DBServiceUID: dbService.UID, + ProjectUID: dbService.ProjectUID, }) } @@ -499,6 +501,7 @@ func (cu *CloudbeaverUsecase) GraphQLDistributor() echo.MiddlewareFunc { maskCtx := taskMaskingContext{ Enabled: isMaskingEnabled, DBServiceUID: dbService.UID, + ProjectUID: dbService.ProjectUID, } if ep, epErr := cu.getWorkflowExecParams(c, params); epErr == nil { maskCtx.SchemaName = ep.instanceSchema @@ -569,6 +572,7 @@ func (cu *CloudbeaverUsecase) GraphQLDistributor() echo.MiddlewareFunc { return cu.buildTaskIdAssocDataMasking(cloudbeaverResBuf.Bytes(), taskMaskingContext{ Enabled: isMaskingEnabled, DBServiceUID: dbService.UID, + ProjectUID: dbService.ProjectUID, }) } @@ -703,6 +707,7 @@ func (cu *CloudbeaverUsecase) GraphQLDistributor() echo.MiddlewareFunc { Enabled: isMaskingEnabled, DBServiceUID: dbService.UID, SchemaName: maskingSchemaName, + ProjectUID: dbService.ProjectUID, }); err != nil { return nil, err } @@ -754,7 +759,7 @@ func (cu *CloudbeaverUsecase) GraphQLDistributor() echo.MiddlewareFunc { if cu.sqlResultMasker == nil { return nil } - return cu.sqlResultMasker.MaskSQLResults(ctx, result, maskingCtx.DBServiceUID, maskingCtx.SchemaName) + return cu.sqlResultMasker.MaskSQLResults(ctx, result, maskingCtx.DBServiceUID, maskingCtx.SchemaName, maskingCtx.ProjectUID) } // 创建GraphQL可执行schema diff --git a/internal/sql_workbench/service/sql_workbench_service.go b/internal/sql_workbench/service/sql_workbench_service.go index c86563ede..4862b1749 100644 --- a/internal/sql_workbench/service/sql_workbench_service.go +++ b/internal/sql_workbench/service/sql_workbench_service.go @@ -23,10 +23,11 @@ import ( "github.com/actiontech/dms/internal/dms/biz" pkgConst "github.com/actiontech/dms/internal/dms/pkg/constant" "github.com/actiontech/dms/internal/dms/storage" - "github.com/actiontech/dms/internal/pkg/locale" dbmodel "github.com/actiontech/dms/internal/dms/storage/model" + "github.com/actiontech/dms/internal/pkg/locale" "github.com/actiontech/dms/internal/sql_workbench/client" config "github.com/actiontech/dms/internal/sql_workbench/config" + "github.com/actiontech/dms/internal/sql_workbench/sqlresultmasker" "github.com/actiontech/dms/pkg/dms-common/api/jwt" "github.com/actiontech/dms/pkg/dms-common/i18nPkg" _const "github.com/actiontech/dms/pkg/dms-common/pkg/const" @@ -102,6 +103,15 @@ type SqlWorkbenchService struct { sqlWorkbenchDatasourceRepo biz.SqlWorkbenchDatasourceRepo proxyTargetRepo biz.ProxyTargetRepo cbOperationLogUsecase *biz.CbOperationLogUsecase + sqlResultMasker sqlresultmasker.SQLResultMasker +} + +func (sqlWorkbenchService *SqlWorkbenchService) SetSqlResultMasker(masker sqlresultmasker.SQLResultMasker) { + sqlWorkbenchService.sqlResultMasker = masker +} + +func (sqlWorkbenchService *SqlWorkbenchService) GetSqlResultMasker() sqlresultmasker.SQLResultMasker { + return sqlWorkbenchService.sqlResultMasker } func NewAndInitSqlWorkbenchService(logger utilLog.Logger, opts *conf.DMSOptions) (*SqlWorkbenchService, error) { @@ -1427,14 +1437,14 @@ func extractSessionID(path string) string { // executeAndAddAuditResult 执行真实请求并添加审核结果 func (sqlWorkbenchService *SqlWorkbenchService) executeAndAddAuditResult(c echo.Context, next echo.HandlerFunc, auditResult *cloudbeaver.AuditSQLReply, dbService *biz.DBService) error { // 创建响应拦截器 - srw := newStreamExecuteResponseWriter(c) + srw := NewStreamExecuteResponseWriter(c) cloudbeaverResBuf := srw.Buffer c.Response().Writer = srw defer func() { // 在 defer 中处理响应 - if srw.status != 0 { - srw.original.WriteHeader(srw.status) + if srw.Status != 0 { + srw.Original.WriteHeader(srw.Status) } // 读取响应内容 @@ -1447,13 +1457,13 @@ func (sqlWorkbenchService *SqlWorkbenchService) executeAndAddAuditResult(c echo. responseBytes, wasGzip, err := sqlWorkbenchService.decodeResponseBody(cloudbeaverResBuf.Bytes(), srw.Header().Get("Content-Encoding")) if err != nil { sqlWorkbenchService.log.Debugf("Failed to decode response body, returning original response: %v", err) - srw.original.Write(cloudbeaverResBuf.Bytes()) + srw.Original.Write(cloudbeaverResBuf.Bytes()) return } // 如果解压过,先移除 Content-Encoding,后续根据需要重新设置 if wasGzip { - srw.original.Header().Del("Content-Encoding") + srw.Original.Header().Del("Content-Encoding") } // 解析响应 JSON @@ -1461,7 +1471,7 @@ func (sqlWorkbenchService *SqlWorkbenchService) executeAndAddAuditResult(c echo. if err := json.Unmarshal(responseBytes, &responseBody); err != nil { sqlWorkbenchService.log.Debugf("Failed to unmarshal response, returning original response: %v", err) // 如果解析失败,直接返回原始响应 - srw.original.Write(cloudbeaverResBuf.Bytes()) + srw.Original.Write(cloudbeaverResBuf.Bytes()) return } @@ -1475,7 +1485,7 @@ func (sqlWorkbenchService *SqlWorkbenchService) executeAndAddAuditResult(c echo. modifiedResponse, err := json.Marshal(responseBody) if err != nil { sqlWorkbenchService.log.Errorf("Failed to marshal modified response: %v", err) - srw.original.Write(cloudbeaverResBuf.Bytes()) + srw.Original.Write(cloudbeaverResBuf.Bytes()) return } @@ -1484,26 +1494,26 @@ func (sqlWorkbenchService *SqlWorkbenchService) executeAndAddAuditResult(c echo. encoded, err := sqlWorkbenchService.encodeResponseBody(modifiedResponse) if err != nil { sqlWorkbenchService.log.Errorf("Failed to re-encode gzip response: %v", err) - srw.original.Write(cloudbeaverResBuf.Bytes()) + srw.Original.Write(cloudbeaverResBuf.Bytes()) return } finalResponse = encoded - srw.original.Header().Set("Content-Encoding", "gzip") + srw.Original.Header().Set("Content-Encoding", "gzip") } // 更新 Content-Length - header := srw.original.Header() + header := srw.Original.Header() header.Set("Content-Length", fmt.Sprintf("%d", len(finalResponse))) // 如果拦截过程中未显式写入状态码,默认使用 200 - statusCode := srw.status + statusCode := srw.Status if statusCode == 0 { statusCode = http.StatusOK } - srw.original.WriteHeader(statusCode) + srw.Original.WriteHeader(statusCode) // 写入修改后的响应 - if _, err := srw.original.Write(finalResponse); err != nil { + if _, err := srw.Original.Write(finalResponse); err != nil { sqlWorkbenchService.log.Errorf("Failed to write modified response: %v", err) } }() @@ -1736,34 +1746,32 @@ type SQLEAuditResultSummary struct { PassRate float64 `json:"pass_rate"` } -// streamExecuteResponseWriter 响应拦截器,用于捕获响应内容 -type streamExecuteResponseWriter struct { +// StreamExecuteResponseWriter 响应拦截器,用于捕获响应内容 +type StreamExecuteResponseWriter struct { echo.Response Buffer *bytes.Buffer - original http.ResponseWriter - status int + Original http.ResponseWriter + Status int } -func newStreamExecuteResponseWriter(c echo.Context) *streamExecuteResponseWriter { +func NewStreamExecuteResponseWriter(c echo.Context) *StreamExecuteResponseWriter { buf := new(bytes.Buffer) - return &streamExecuteResponseWriter{ + return &StreamExecuteResponseWriter{ Response: *c.Response(), Buffer: buf, - original: c.Response().Writer, + Original: c.Response().Writer, } } -func (w *streamExecuteResponseWriter) Write(b []byte) (int, error) { +func (w *StreamExecuteResponseWriter) Write(b []byte) (int, error) { // 如果未设置状态码,则补默认值 - if w.status == 0 { + if w.Status == 0 { w.WriteHeader(http.StatusOK) } // 写入 buffer,不立即写给客户端 return w.Buffer.Write(b) } -func (w *streamExecuteResponseWriter) WriteHeader(code int) { - w.status = code +func (w *StreamExecuteResponseWriter) WriteHeader(code int) { + w.Status = code } - - diff --git a/internal/sql_workbench/sqlresultmasker/masker.go b/internal/sql_workbench/sqlresultmasker/masker.go new file mode 100644 index 000000000..780c02f30 --- /dev/null +++ b/internal/sql_workbench/sqlresultmasker/masker.go @@ -0,0 +1,18 @@ +package sqlresultmasker + +import "context" + +// MaskWorkbenchResultsArgs carries tabular SQL workbench (ODC) result data and masking scope. Rows are masked in place. +type MaskWorkbenchResultsArgs struct { + Rows [][]any `json:"rows"` + ColumnNames []string `json:"column_names"` + SQL string `json:"sql"` + DBServiceUID string `json:"db_service_uid"` + SchemaName string `json:"schema_name"` + ProjectUID string `json:"project_uid"` +} + +// SQLResultMasker masks SQL workbench (ODC) tabular result rows in place and reports which columns were masked. +type SQLResultMasker interface { + MaskSQLWorkbenchResults(ctx context.Context, args *MaskWorkbenchResultsArgs) (map[string]bool, error) +} From 32faa8834b6d81c233ac89b95f769ffe871c358a Mon Sep 17 00:00:00 2001 From: WinfredLIN Date: Wed, 22 Apr 2026 09:09:36 +0000 Subject: [PATCH 06/16] ce feat: implement unmasking workflow endpoints and middleware Added new endpoints for managing unmasking workflows, including creation, retrieval, approval, rejection, and cancellation. Introduced middleware for handling data masking and unmasking operations in SQL Workbench, ensuring proper permission checks and response handling. Enhanced the API structure to support these functionalities, improving overall data management capabilities. --- .../service/data_masking_middleware.go | 15 ++++++++++++ .../service/data_masking_middleware_ce.go | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 internal/sql_workbench/service/data_masking_middleware.go create mode 100644 internal/sql_workbench/service/data_masking_middleware_ce.go diff --git a/internal/sql_workbench/service/data_masking_middleware.go b/internal/sql_workbench/service/data_masking_middleware.go new file mode 100644 index 000000000..46e0dc766 --- /dev/null +++ b/internal/sql_workbench/service/data_masking_middleware.go @@ -0,0 +1,15 @@ +package sql_workbench + +import ( + "github.com/actiontech/dms/internal/dms/biz" + dataMaskingBiz "github.com/actiontech/dms/internal/data_masking/biz" + "github.com/actiontech/dms/internal/sql_workbench/sqlresultmasker" +) + +// DataMaskingMiddlewareConfig 配置脱敏中间件 +type DataMaskingMiddlewareConfig struct { + SqlResultMasker sqlresultmasker.SQLResultMasker + DBServiceUsecase *biz.DBServiceUsecase + SqlWorkbenchService *SqlWorkbenchService + UnmaskingWorkflowUsecase *dataMaskingBiz.UnmaskingWorkflowUsecase +} diff --git a/internal/sql_workbench/service/data_masking_middleware_ce.go b/internal/sql_workbench/service/data_masking_middleware_ce.go new file mode 100644 index 000000000..5f10b9047 --- /dev/null +++ b/internal/sql_workbench/service/data_masking_middleware_ce.go @@ -0,0 +1,24 @@ +//go:build !enterprise +// +build !enterprise + +package sql_workbench + +import "github.com/labstack/echo/v4" + +// GetDataMaskingMiddleware 返回用于脱敏的中间件 +func GetDataMaskingMiddleware(config DataMaskingMiddlewareConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + return next(c) + } + } +} + +// GetUnmaskingWorkflowMiddleware 处理获批后查看原文,执行 SQL 之前进行替换 +func GetUnmaskingWorkflowMiddleware(config DataMaskingMiddlewareConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + return next(c) + } + } +} From d522ad0c1f434a83d1a10283b7a733883fc8a2c3 Mon Sep 17 00:00:00 2001 From: yangzhongjiao Date: Mon, 11 May 2026 12:08:44 +0000 Subject: [PATCH 07/16] ce feat: enhance DataExportWorkflowUsecase with projectUID and unmasking workflow functionality Updated the newExportMaskingTransfer method to include projectUID as a parameter, improving the masking transfer capabilities. Added markUnmaskingWorkflowUsage method to track unmasking workflow usage. Initialized unmaskingWorkflowUsecase in DMSService, enabling unmasking workflow management in the enterprise edition. Introduced new SQL workbench result masker implementations for both community and enterprise editions, ensuring proper handling based on build tags. --- internal/dms/service/service.go | 8 +++- .../service/sql_workbench_result_masker_ce.go | 14 ++++++ internal/dms/service/unmasking_workflow_ce.go | 46 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 internal/dms/service/sql_workbench_result_masker_ce.go create mode 100644 internal/dms/service/unmasking_workflow_ce.go diff --git a/internal/dms/service/service.go b/internal/dms/service/service.go index f6a027883..a8fb00933 100644 --- a/internal/dms/service/service.go +++ b/internal/dms/service/service.go @@ -155,7 +155,11 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer workflowRepo := storage.NewWorkflowRepo(logger, st) dataExportMaskingConfigRepo := initDataExportMaskingConfigRepo(logger, st) dataExportMaskingRuleRepo := initDataExportMaskingRuleRepo(logger, st) - DataExportWorkflowUsecase := biz.NewDataExportWorkflowUsecase(logger, tx, workflowRepo, dataExportTaskRepo, dbServiceRepo, dataExportMaskingConfigRepo, dataExportMaskingRuleRepo, opPermissionVerifyUsecase, projectUsecase, dmsProxyTargetRepo, clusterUsecase, webhookConfigurationUsecase, userUsecase, systemVariableUsecase, discoveryTaskRepo, nil, fmt.Sprintf("%s:%d", opts.ReportHost, opts.APIServiceOpts.Port)) + unmaskingWorkflowUsecase, err := initUnmaskingWorkflowUsecase(logger, st, dmsProxyTargetRepo, opPermissionVerifyUsecase, userUsecase, dbServiceUseCase) + if err != nil { + return nil, fmt.Errorf("failed to initialize unmasking workflow usecase: %v", err) + } + DataExportWorkflowUsecase := biz.NewDataExportWorkflowUsecase(logger, tx, workflowRepo, dataExportTaskRepo, dbServiceRepo, dataExportMaskingConfigRepo, dataExportMaskingRuleRepo, opPermissionVerifyUsecase, projectUsecase, dmsProxyTargetRepo, clusterUsecase, webhookConfigurationUsecase, userUsecase, systemVariableUsecase, discoveryTaskRepo, unmaskingWorkflowUsecase, fmt.Sprintf("%s:%d", opts.ReportHost, opts.APIServiceOpts.Port)) dataMaskingUsecase, stopDataMaskingScheduler, err := initDataMaskingUsecase(logger, st, dbServiceUseCase, clusterUsecase, dmsProxyTargetRepo) if err != nil { return nil, fmt.Errorf("failed to initialize data masking usecase: %v", err) @@ -204,7 +208,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer LicenseUsecase: LicenseUsecase, ClusterUsecase: clusterUsecase, DataExportWorkflowUsecase: DataExportWorkflowUsecase, - UnmaskingWorkflowUsecase: nil, + UnmaskingWorkflowUsecase: unmaskingWorkflowUsecase, CbOperationLogUsecase: CbOperationLogUsecase, DataMaskingUsecase: dataMaskingUsecase, FunctionSupportRegistry: functionSupportRegistry, diff --git a/internal/dms/service/sql_workbench_result_masker_ce.go b/internal/dms/service/sql_workbench_result_masker_ce.go new file mode 100644 index 000000000..de7219276 --- /dev/null +++ b/internal/dms/service/sql_workbench_result_masker_ce.go @@ -0,0 +1,14 @@ +//go:build !dms + +package service + +import ( + "github.com/actiontech/dms/internal/dms/storage" + sqlresultmasker "github.com/actiontech/dms/internal/sql_workbench/sqlresultmasker" + utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" +) + +// NewSQLWorkbenchSQLResultMasker is a no-op in builds without the dms data-masking stack. +func NewSQLWorkbenchSQLResultMasker(_ utilLog.Logger, _ *storage.Storage) (sqlresultmasker.SQLResultMasker, error) { + return nil, nil +} diff --git a/internal/dms/service/unmasking_workflow_ce.go b/internal/dms/service/unmasking_workflow_ce.go new file mode 100644 index 000000000..4cf7bd295 --- /dev/null +++ b/internal/dms/service/unmasking_workflow_ce.go @@ -0,0 +1,46 @@ +//go:build !dms + +package service + +import ( + "context" + "errors" + + v1 "github.com/actiontech/dms/api/dms/service/v1" + "github.com/actiontech/dms/internal/data_masking/biz" + dmsBiz "github.com/actiontech/dms/internal/dms/biz" + "github.com/actiontech/dms/internal/dms/storage" + utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" +) + +var errNotSupportUnmaskingWorkflow = errors.New("unmasking workflow related functions are enterprise version functions") + +type unmaskingWorkflowUsecase = biz.UnmaskingWorkflowUsecase + +func initUnmaskingWorkflowUsecase(_ utilLog.Logger, _ *storage.Storage, _ dmsBiz.ProxyTargetRepo, _ *dmsBiz.OpPermissionVerifyUsecase, _ *dmsBiz.UserUsecase, _ *dmsBiz.DBServiceUsecase) (*unmaskingWorkflowUsecase, error) { + return nil, nil +} + +func (d *DMSService) CreateUnmaskingWorkflow(ctx context.Context, req *v1.CreateUnmaskingWorkflowReq, currentUserUid string) (*v1.CreateUnmaskingWorkflowReply, error) { + return nil, errNotSupportUnmaskingWorkflow +} + +func (d *DMSService) GetUnmaskingWorkflow(ctx context.Context, req *v1.GetUnmaskingWorkflowReq, currentUserUid string) (*v1.GetUnmaskingWorkflowReply, error) { + return nil, errNotSupportUnmaskingWorkflow +} + +func (d *DMSService) ListUnmaskingWorkflows(ctx context.Context, req *v1.ListUnmaskingWorkflowsReq, currentUserUid string) (*v1.ListUnmaskingWorkflowsReply, error) { + return nil, errNotSupportUnmaskingWorkflow +} + +func (d *DMSService) ApproveUnmaskingWorkflow(ctx context.Context, req *v1.ApproveUnmaskingWorkflowReq, currentUserUid string) error { + return errNotSupportUnmaskingWorkflow +} + +func (d *DMSService) RejectUnmaskingWorkflow(ctx context.Context, req *v1.RejectUnmaskingWorkflowReq, currentUserUid string) error { + return errNotSupportUnmaskingWorkflow +} + +func (d *DMSService) CancelUnmaskingWorkflow(ctx context.Context, req *v1.CancelUnmaskingWorkflowReq, currentUserUid string) error { + return errNotSupportUnmaskingWorkflow +} From 1acb95e3ad4ccc83235c458b8615dd41b445f75d Mon Sep 17 00:00:00 2001 From: yangzhongjiao Date: Mon, 11 May 2026 12:08:45 +0000 Subject: [PATCH 08/16] ce feat: DMSService: align UnmaskingWorkflowUsecase field type (CE) Owns the service struct type alignment previously marked ee; pairs with storage/model EE implementation. --- internal/dms/service/service.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/dms/service/service.go b/internal/dms/service/service.go index a8fb00933..69897ec6b 100644 --- a/internal/dms/service/service.go +++ b/internal/dms/service/service.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/actiontech/dms/internal/apiserver/conf" - maskingBiz "github.com/actiontech/dms/internal/data_masking/biz" "github.com/actiontech/dms/internal/dms/biz" "github.com/actiontech/dms/internal/dms/storage" @@ -42,7 +41,7 @@ type DMSService struct { LicenseUsecase *biz.LicenseUsecase ClusterUsecase *biz.ClusterUsecase DataExportWorkflowUsecase *biz.DataExportWorkflowUsecase - UnmaskingWorkflowUsecase *maskingBiz.UnmaskingWorkflowUsecase + UnmaskingWorkflowUsecase *unmaskingWorkflowUsecase CbOperationLogUsecase *biz.CbOperationLogUsecase DataMaskingUsecase *dataMaskingUsecase FunctionSupportRegistry *biz.FunctionSupportRegistry From 8d984e2d0f33e33e37f4d50c8c3feb951aa0f1a1 Mon Sep 17 00:00:00 2001 From: WinfredLIN Date: Wed, 22 Apr 2026 09:14:57 +0000 Subject: [PATCH 09/16] ce feat: add ListTableColumns method for DMSService Introduced the ListTableColumns method in the DMSService to handle requests for listing table columns. This method currently returns an error indicating unsupported data masking, laying the groundwork for future enhancements in data management capabilities. --- internal/dms/service/db_structures_ce.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 internal/dms/service/db_structures_ce.go diff --git a/internal/dms/service/db_structures_ce.go b/internal/dms/service/db_structures_ce.go new file mode 100644 index 000000000..de768ef09 --- /dev/null +++ b/internal/dms/service/db_structures_ce.go @@ -0,0 +1,14 @@ +//go:build !dms + +package service + +import ( + "context" + + dmsV1 "github.com/actiontech/dms/api/dms/service/v1" +) + +func (d *DMSService) ListTableColumns(ctx context.Context, req *dmsV1.ListTableColumnsReq) (*dmsV1.ListTableColumnsReply, error) { + return nil, errNotSupportDataMasking +} + From 399d69cfbe5814dc65e0340f1a83e942972434fb Mon Sep 17 00:00:00 2001 From: WinfredLIN Date: Wed, 22 Apr 2026 09:15:29 +0000 Subject: [PATCH 10/16] ce feat: add unmasking workflow messages for English and Chinese locales Updated the locale files to include new messages related to unmasking workflows, enhancing user feedback for operations such as submission, approval, cancellation, and viewing details. This addition supports the recently implemented unmasking workflow features in the DMS service. --- internal/pkg/locale/active.en.toml | 9 +++ internal/pkg/locale/active.zh.toml | 9 +++ internal/pkg/locale/message_zh.go | 105 ++++++++++++++++------------- 3 files changed, 78 insertions(+), 45 deletions(-) diff --git a/internal/pkg/locale/active.en.toml b/internal/pkg/locale/active.en.toml index c828dfc8f..bdef06600 100644 --- a/internal/pkg/locale/active.en.toml +++ b/internal/pkg/locale/active.en.toml @@ -179,6 +179,15 @@ OpRecordDataExportCreate = "Create data export workflow" OpRecordDataExportCreateWithName = "Create data export workflow %s" OpRecordDataExportExportWithName = "Execute data export %s" OpRecordDataExportRejectWithName = "Reject data export workflow %s" +OpRecordUnmaskingAPICreate = "Submit unmasking workflow application" +OpRecordUnmaskingApproveWithWorkflowUID = "Approve unmasking workflow %s" +OpRecordUnmaskingCancelWithWorkflowUID = "Cancel unmasking workflow %s" +OpRecordUnmaskingDownloadOriginalWithWorkflowUID = "Download original data %s" +OpRecordUnmaskingRejectWithWorkflowUID = "Reject unmasking workflow %s" +OpRecordUnmaskingSubmitWithWorkflowUID = "Submit unmasking workflow %s" +OpRecordUnmaskingUnknownWithWorkflowUID = "Unmasking workflow action %s (%s)" +OpRecordUnmaskingViewDetailWithWorkflowUID = "View unmasking workflow detail %s" +OpRecordUnmaskingViewOriginalWithWorkflowUID = "View full original data (SQL) %s" OpRecordMemberCreate = "Add member" OpRecordMemberCreateWithName = "Add member %s" OpRecordMemberDelete = "Delete member %s" diff --git a/internal/pkg/locale/active.zh.toml b/internal/pkg/locale/active.zh.toml index 23f55bc90..2c441bada 100644 --- a/internal/pkg/locale/active.zh.toml +++ b/internal/pkg/locale/active.zh.toml @@ -179,6 +179,15 @@ OpRecordDataExportCreate = "创建数据导出工单" OpRecordDataExportCreateWithName = "创建数据导出工单 %s" OpRecordDataExportExportWithName = "执行数据导出 %s" OpRecordDataExportRejectWithName = "驳回数据导出工单 %s" +OpRecordUnmaskingAPICreate = "提交查看原文工单申请" +OpRecordUnmaskingApproveWithWorkflowUID = "审批通过查看原文工单 %s" +OpRecordUnmaskingCancelWithWorkflowUID = "撤回查看原文工单 %s" +OpRecordUnmaskingDownloadOriginalWithWorkflowUID = "下载原文数据 %s" +OpRecordUnmaskingRejectWithWorkflowUID = "驳回查看原文工单 %s" +OpRecordUnmaskingSubmitWithWorkflowUID = "提交查看原文工单 %s" +OpRecordUnmaskingUnknownWithWorkflowUID = "查看原文工单操作 %s (%s)" +OpRecordUnmaskingViewDetailWithWorkflowUID = "查看查看原文工单详情 %s" +OpRecordUnmaskingViewOriginalWithWorkflowUID = "查看原文(SQL)%s" OpRecordMemberCreate = "添加成员" OpRecordMemberCreateWithName = "添加成员 %s" OpRecordMemberDelete = "删除成员 %s" diff --git a/internal/pkg/locale/message_zh.go b/internal/pkg/locale/message_zh.go index 19ed3f3fa..849017dc8 100644 --- a/internal/pkg/locale/message_zh.go +++ b/internal/pkg/locale/message_zh.go @@ -267,49 +267,64 @@ var ( // Operation Record var ( - OpRecordUserCreate = &i18n.Message{ID: "OpRecordUserCreate", Other: "创建用户"} - OpRecordUserCreateWithName = &i18n.Message{ID: "OpRecordUserCreateWithName", Other: "创建用户 %s"} - OpRecordCurrentUserUpdate = &i18n.Message{ID: "OpRecordCurrentUserUpdate", Other: "更新个人中心账号基本信息"} - OpRecordUserUpdate = &i18n.Message{ID: "OpRecordUserUpdate", Other: "更新用户 %s"} - OpRecordUserDelete = &i18n.Message{ID: "OpRecordUserDelete", Other: "删除用户 %s"} - OpRecordMemberCreate = &i18n.Message{ID: "OpRecordMemberCreate", Other: "添加成员"} - OpRecordMemberCreateWithName = &i18n.Message{ID: "OpRecordMemberCreateWithName", Other: "添加成员 %s"} - OpRecordMemberUpdate = &i18n.Message{ID: "OpRecordMemberUpdate", Other: "更新成员 %s"} - OpRecordMemberDelete = &i18n.Message{ID: "OpRecordMemberDelete", Other: "删除成员 %s"} - OpRecordMemberGroupCreate = &i18n.Message{ID: "OpRecordMemberGroupCreate", Other: "添加成员组"} - OpRecordMemberGroupCreateWithName = &i18n.Message{ID: "OpRecordMemberGroupCreateWithName", Other: "添加成员组 %s"} - OpRecordMemberGroupUpdate = &i18n.Message{ID: "OpRecordMemberGroupUpdate", Other: "更新成员组 %s"} - OpRecordMemberGroupDelete = &i18n.Message{ID: "OpRecordMemberGroupDelete", Other: "删除成员组 %s"} - OpRecordRoleCreate = &i18n.Message{ID: "OpRecordRoleCreate", Other: "创建角色"} - OpRecordRoleCreateWithName = &i18n.Message{ID: "OpRecordRoleCreateWithName", Other: "创建角色 %s"} - OpRecordRoleUpdate = &i18n.Message{ID: "OpRecordRoleUpdate", Other: "更新角色 %s"} - OpRecordRoleDelete = &i18n.Message{ID: "OpRecordRoleDelete", Other: "删除角色 %s"} - OpRecordProjectCreate = &i18n.Message{ID: "OpRecordProjectCreate", Other: "创建项目"} - OpRecordProjectCreateWithName = &i18n.Message{ID: "OpRecordProjectCreateWithName", Other: "创建项目 %s"} - OpRecordProjectUpdate = &i18n.Message{ID: "OpRecordProjectUpdate", Other: "更新项目 %s"} - OpRecordProjectDelete = &i18n.Message{ID: "OpRecordProjectDelete", Other: "删除项目 %s"} - OpRecordProjectArchive = &i18n.Message{ID: "OpRecordProjectArchive", Other: "归档项目 %s"} - OpRecordProjectUnarchive = &i18n.Message{ID: "OpRecordProjectUnarchive", Other: "取消归档项目 %s"} - OpRecordDBServiceCreate = &i18n.Message{ID: "OpRecordDBServiceCreate", Other: "创建数据源"} - OpRecordDBServiceCreateWithName = &i18n.Message{ID: "OpRecordDBServiceCreateWithName", Other: "创建数据源 %s"} - OpRecordDBServiceUpdate = &i18n.Message{ID: "OpRecordDBServiceUpdate", Other: "更新数据源 %s"} - OpRecordDBServiceDelete = &i18n.Message{ID: "OpRecordDBServiceDelete", Other: "删除数据源 %s"} - OpRecordDBServiceImport = &i18n.Message{ID: "OpRecordDBServiceImport", Other: "导入数据源"} - OpRecordConfigLogin = &i18n.Message{ID: "OpRecordConfigLogin", Other: "更新登录配置"} - OpRecordConfigOAuth2 = &i18n.Message{ID: "OpRecordConfigOAuth2", Other: "更新OAuth2配置"} - OpRecordConfigLDAP = &i18n.Message{ID: "OpRecordConfigLDAP", Other: "更新LDAP配置"} - OpRecordConfigSMTP = &i18n.Message{ID: "OpRecordConfigSMTP", Other: "更新SMTP配置"} - OpRecordConfigWechat = &i18n.Message{ID: "OpRecordConfigWechat", Other: "更新企业微信配置"} - OpRecordConfigFeishu = &i18n.Message{ID: "OpRecordConfigFeishu", Other: "更新飞书配置"} - OpRecordConfigWebhook = &i18n.Message{ID: "OpRecordConfigWebhook", Other: "更新Webhook配置"} - OpRecordConfigSms = &i18n.Message{ID: "OpRecordConfigSms", Other: "更新短信配置"} - OpRecordConfigSystemVariables = &i18n.Message{ID: "OpRecordConfigSystemVariables", Other: "更新系统变量配置"} - OpRecordConfigCompanyNotice = &i18n.Message{ID: "OpRecordConfigCompanyNotice", Other: "更新系统公告"} - OpRecordDataExportCreate = &i18n.Message{ID: "OpRecordDataExportCreate", Other: "创建数据导出工单"} - OpRecordDataExportCreateWithName = &i18n.Message{ID: "OpRecordDataExportCreateWithName", Other: "创建数据导出工单 %s"} - OpRecordDataExportApproveWithName = &i18n.Message{ID: "OpRecordDataExportApproveWithName", Other: "审批通过数据导出工单 %s"} - OpRecordDataExportRejectWithName = &i18n.Message{ID: "OpRecordDataExportRejectWithName", Other: "驳回数据导出工单 %s"} - OpRecordDataExportExportWithName = &i18n.Message{ID: "OpRecordDataExportExportWithName", Other: "执行数据导出 %s"} - OpRecordDataExportCancel = &i18n.Message{ID: "OpRecordDataExportCancel", Other: "取消数据导出工单"} - OpRecordDataExportCancelWithName = &i18n.Message{ID: "OpRecordDataExportCancelWithName", Other: "取消数据导出工单 %s"} + OpRecordUserCreate = &i18n.Message{ID: "OpRecordUserCreate", Other: "创建用户"} + OpRecordUserCreateWithName = &i18n.Message{ID: "OpRecordUserCreateWithName", Other: "创建用户 %s"} + OpRecordCurrentUserUpdate = &i18n.Message{ID: "OpRecordCurrentUserUpdate", Other: "更新个人中心账号基本信息"} + OpRecordUserUpdate = &i18n.Message{ID: "OpRecordUserUpdate", Other: "更新用户 %s"} + OpRecordUserDelete = &i18n.Message{ID: "OpRecordUserDelete", Other: "删除用户 %s"} + OpRecordMemberCreate = &i18n.Message{ID: "OpRecordMemberCreate", Other: "添加成员"} + OpRecordMemberCreateWithName = &i18n.Message{ID: "OpRecordMemberCreateWithName", Other: "添加成员 %s"} + OpRecordMemberUpdate = &i18n.Message{ID: "OpRecordMemberUpdate", Other: "更新成员 %s"} + OpRecordMemberDelete = &i18n.Message{ID: "OpRecordMemberDelete", Other: "删除成员 %s"} + OpRecordMemberGroupCreate = &i18n.Message{ID: "OpRecordMemberGroupCreate", Other: "添加成员组"} + OpRecordMemberGroupCreateWithName = &i18n.Message{ID: "OpRecordMemberGroupCreateWithName", Other: "添加成员组 %s"} + OpRecordMemberGroupUpdate = &i18n.Message{ID: "OpRecordMemberGroupUpdate", Other: "更新成员组 %s"} + OpRecordMemberGroupDelete = &i18n.Message{ID: "OpRecordMemberGroupDelete", Other: "删除成员组 %s"} + OpRecordRoleCreate = &i18n.Message{ID: "OpRecordRoleCreate", Other: "创建角色"} + OpRecordRoleCreateWithName = &i18n.Message{ID: "OpRecordRoleCreateWithName", Other: "创建角色 %s"} + OpRecordRoleUpdate = &i18n.Message{ID: "OpRecordRoleUpdate", Other: "更新角色 %s"} + OpRecordRoleDelete = &i18n.Message{ID: "OpRecordRoleDelete", Other: "删除角色 %s"} + OpRecordProjectCreate = &i18n.Message{ID: "OpRecordProjectCreate", Other: "创建项目"} + OpRecordProjectCreateWithName = &i18n.Message{ID: "OpRecordProjectCreateWithName", Other: "创建项目 %s"} + OpRecordProjectUpdate = &i18n.Message{ID: "OpRecordProjectUpdate", Other: "更新项目 %s"} + OpRecordProjectDelete = &i18n.Message{ID: "OpRecordProjectDelete", Other: "删除项目 %s"} + OpRecordProjectArchive = &i18n.Message{ID: "OpRecordProjectArchive", Other: "归档项目 %s"} + OpRecordProjectUnarchive = &i18n.Message{ID: "OpRecordProjectUnarchive", Other: "取消归档项目 %s"} + OpRecordDBServiceCreate = &i18n.Message{ID: "OpRecordDBServiceCreate", Other: "创建数据源"} + OpRecordDBServiceCreateWithName = &i18n.Message{ID: "OpRecordDBServiceCreateWithName", Other: "创建数据源 %s"} + OpRecordDBServiceUpdate = &i18n.Message{ID: "OpRecordDBServiceUpdate", Other: "更新数据源 %s"} + OpRecordDBServiceDelete = &i18n.Message{ID: "OpRecordDBServiceDelete", Other: "删除数据源 %s"} + OpRecordDBServiceImport = &i18n.Message{ID: "OpRecordDBServiceImport", Other: "导入数据源"} + OpRecordConfigLogin = &i18n.Message{ID: "OpRecordConfigLogin", Other: "更新登录配置"} + OpRecordConfigOAuth2 = &i18n.Message{ID: "OpRecordConfigOAuth2", Other: "更新OAuth2配置"} + OpRecordConfigLDAP = &i18n.Message{ID: "OpRecordConfigLDAP", Other: "更新LDAP配置"} + OpRecordConfigSMTP = &i18n.Message{ID: "OpRecordConfigSMTP", Other: "更新SMTP配置"} + OpRecordConfigWechat = &i18n.Message{ID: "OpRecordConfigWechat", Other: "更新企业微信配置"} + OpRecordConfigFeishu = &i18n.Message{ID: "OpRecordConfigFeishu", Other: "更新飞书配置"} + OpRecordConfigWebhook = &i18n.Message{ID: "OpRecordConfigWebhook", Other: "更新Webhook配置"} + OpRecordConfigSms = &i18n.Message{ID: "OpRecordConfigSms", Other: "更新短信配置"} + OpRecordConfigSystemVariables = &i18n.Message{ID: "OpRecordConfigSystemVariables", Other: "更新系统变量配置"} + OpRecordConfigCompanyNotice = &i18n.Message{ID: "OpRecordConfigCompanyNotice", Other: "更新系统公告"} + OpRecordDataExportCreate = &i18n.Message{ID: "OpRecordDataExportCreate", Other: "创建数据导出工单"} + OpRecordDataExportCreateWithName = &i18n.Message{ID: "OpRecordDataExportCreateWithName", Other: "创建数据导出工单 %s"} + OpRecordDataExportApproveWithName = &i18n.Message{ID: "OpRecordDataExportApproveWithName", Other: "审批通过数据导出工单 %s"} + OpRecordDataExportRejectWithName = &i18n.Message{ID: "OpRecordDataExportRejectWithName", Other: "驳回数据导出工单 %s"} + OpRecordDataExportExportWithName = &i18n.Message{ID: "OpRecordDataExportExportWithName", Other: "执行数据导出 %s"} + OpRecordDataExportCancel = &i18n.Message{ID: "OpRecordDataExportCancel", Other: "取消数据导出工单"} + OpRecordDataExportCancelWithName = &i18n.Message{ID: "OpRecordDataExportCancelWithName", Other: "取消数据导出工单 %s"} + OpRecordUnmaskingAPICreate = &i18n.Message{ID: "OpRecordUnmaskingAPICreate", Other: "提交查看原文工单申请"} + OpRecordUnmaskingSubmitWithWorkflowUID = &i18n.Message{ID: "OpRecordUnmaskingSubmitWithWorkflowUID", Other: "提交查看原文工单 %s"} + OpRecordUnmaskingApproveWithWorkflowUID = &i18n.Message{ID: "OpRecordUnmaskingApproveWithWorkflowUID", Other: "审批通过查看原文工单 %s"} + OpRecordUnmaskingRejectWithWorkflowUID = &i18n.Message{ID: "OpRecordUnmaskingRejectWithWorkflowUID", Other: "驳回查看原文工单 %s"} + OpRecordUnmaskingViewDetailWithWorkflowUID = &i18n.Message{ID: "OpRecordUnmaskingViewDetailWithWorkflowUID", Other: "查看查看原文工单详情 %s"} + OpRecordUnmaskingViewOriginalWithWorkflowUID = &i18n.Message{ID: "OpRecordUnmaskingViewOriginalWithWorkflowUID", Other: "查看原文(SQL)%s"} + OpRecordUnmaskingDownloadOriginalWithWorkflowUID = &i18n.Message{ID: "OpRecordUnmaskingDownloadOriginalWithWorkflowUID", Other: "下载原文数据 %s"} + OpRecordUnmaskingCancelWithWorkflowUID = &i18n.Message{ID: "OpRecordUnmaskingCancelWithWorkflowUID", Other: "撤回查看原文工单 %s"} + OpRecordUnmaskingUnknownWithWorkflowUID = &i18n.Message{ID: "OpRecordUnmaskingUnknownWithWorkflowUID", Other: "查看原文工单操作 %s (%s)"} +) + +// Unmasking Workflow +var ( + SqlWorkbenchUnmaskingNoPermissionErr = &i18n.Message{ID: "SqlWorkbenchUnmaskingNoPermissionErr", Other: "您没有查看原文的权限,请提交查看原文工单"} + SqlWorkbenchUnmaskingWorkflowNotFoundErr = &i18n.Message{ID: "SqlWorkbenchUnmaskingWorkflowNotFoundErr", Other: "未找到对应的查看原文工单"} ) From 4dda14a0b5258d2bc5bd096a4f3d2c4c7969578c Mon Sep 17 00:00:00 2001 From: WinfredLIN Date: Wed, 22 Apr 2026 09:15:52 +0000 Subject: [PATCH 11/16] ce feat: add build verification script for multiple edition combinations Introduced a new script to verify that the application can be built successfully under four different GO_BUILD_TAGS combinations: Community, Trial, Enterprise, and DMS Enterprise. This addition enhances the build process by ensuring compatibility across editions before submission. --- Makefile | 4 ++ scripts/verify_build_editions.sh | 102 +++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100755 scripts/verify_build_editions.sh diff --git a/Makefile b/Makefile index 824691b8a..ffee44a8c 100644 --- a/Makefile +++ b/Makefile @@ -185,6 +185,10 @@ dms_unit_test_clean: dms_test_dms: go test -v -p 1 ./internal/dms/... +# 提交前校验:社区版 / 试用版 / 企业版 / DMS 企业版 四种 GO_BUILD_TAGS 组合下均能 make install +verify_edition_builds: + bash ./scripts/verify_build_editions.sh + ############################### generate ################################## gen_repo_fields: go run ./internal/dms/cmd/gencli/gencli.go -d generate-node-repo-fields ./internal/dms/storage/model/ ./internal/dms/biz/ diff --git a/scripts/verify_build_editions.sh b/scripts/verify_build_editions.sh new file mode 100755 index 000000000..4e238610b --- /dev/null +++ b/scripts/verify_build_editions.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# 提交前校验:社区版 / 试用版 / 企业版 / DMS 企业版 四种构建标签组合均能成功编译(与 Makefile install 一致)。 +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +readonly RED=$'\033[0;31m' +readonly GREEN=$'\033[0;32m' +readonly YELLOW=$'\033[1;33m' +readonly BLUE=$'\033[0;34m' +readonly CYAN=$'\033[0;36m' +readonly BOLD=$'\033[1m' +readonly DIM=$'\033[2m' +readonly NC=$'\033[0m' + +bar() { + printf '%b\n' "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +sub() { + printf '%b %s\n' "${DIM}" "$1${NC}" +} + +err() { + printf '%b %s\n' "${RED}" "$1${NC}" +} + +step_begin() { + local idx="$1" + local total="$2" + local name="$3" + local tags="$4" + printf '\n' + bar + printf '%b Step %s/%s %s\n' "${BOLD}" "$idx" "$total" "$name${NC}" + printf '%b 构建标签 (GO_BUILD_TAGS): %s%s\n' "${CYAN}" "$tags" "${NC}" + bar + printf '%b ⏳ 正在执行 make install …%s\n' "${YELLOW}" "${NC}" +} + +step_ok() { + local secs="$1" + printf '%b ✅ 本步编译成功%s' "${GREEN}" "${NC}" + printf '%b (耗时 %ss)%s\n' "${DIM}" "$secs" "${NC}" +} + +TOTAL_STEPS=4 +FAILED=0 + +run_one() { + local idx="$1" name="$2" tags="$3" + shift 3 + local start=$SECONDS + step_begin "$idx" "$TOTAL_STEPS" "$name" "$tags" + sub "命令: make install $*" + if make install "$@"; then + step_ok "$((SECONDS - start))" + else + err "❌ 本步失败: ${name}" + FAILED=1 + return 1 + fi +} + +printf '\n%b\n' "${BOLD}${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}" +printf '%b\n' "${BOLD}${BLUE}║ 🔍 DMS 多版本构建校验 ║${NC}" +printf '%b\n' "${BOLD}${BLUE}║ 依次验证 Makefile install 在四种标签组合下均可通过 ║${NC}" +printf '%b\n' "${BOLD}${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}" +sub "工作目录: ${ROOT}" + +# 1 社区版: dummyhead(EDITION 默认 ce) +run_one 1 "社区版 (CE)" "dummyhead" || true + +# 2 试用版: dummyhead + trial +if [[ "$FAILED" -eq 0 ]]; then + run_one 2 "试用版 (Trial)" "dummyhead,trial" EDITION=trial || true +fi + +# 3 企业版: dummyhead + enterprise +if [[ "$FAILED" -eq 0 ]]; then + run_one 3 "企业版 (EE)" "dummyhead,enterprise" EDITION=ee || true +fi + +# 4 DMS 企业版: dummyhead + enterprise + dms +if [[ "$FAILED" -eq 0 ]]; then + run_one 4 "DMS 企业版 (EE + DMS)" "dummyhead,enterprise,dms" EDITION=ee PRODUCT_CATEGORY=dms || true +fi + +printf '\n' +bar +if [[ "$FAILED" -eq 0 ]]; then + printf '%b🎉 全部 %s 个版本构建校验通过。%s\n' "${GREEN}${BOLD}" "$TOTAL_STEPS" "${NC}" + bar + printf '\n' + exit 0 +else + printf '%b💥 构建校验未全部通过,请根据上方日志修复后重试。%s\n' "${RED}${BOLD}" "${NC}" + bar + printf '\n' + exit 1 +fi From 607c58c363c9bdc9d4cc8e95a79f8cd673372b39 Mon Sep 17 00:00:00 2001 From: yangzhongjiao Date: Sat, 9 May 2026 07:10:59 +0000 Subject: [PATCH 12/16] fix: update ListMaskingRulesReq to clarify projectUid parameter usage Modified the ListMaskingRulesReq struct in masking.go to enhance documentation for the projectUid parameter. The comment now specifies that projectUid is injected via path for specific requests and is optional for global requests, improving clarity for API users. --- api/dms/service/v1/masking.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/dms/service/v1/masking.go b/api/dms/service/v1/masking.go index 94f60bb84..d991780e6 100644 --- a/api/dms/service/v1/masking.go +++ b/api/dms/service/v1/masking.go @@ -8,11 +8,10 @@ import ( // swagger:parameters ListMaskingRules type ListMaskingRulesReq struct { - // project uid + // project uid(项目路径下由 path 注入;全局 GET /v1/dms/masking/rules 可不传,仅返回内置规则) // in: path - // Required: true // Example: "project_uid" - ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + ProjectUid string `param:"project_uid" query:"project_uid" json:"project_uid"` // 规则来源筛选: builtin 或 custom,为空时返回全部 // in: query // Example: "custom" From 4d0a7ed7e9a3ba37a16d89b0d72f03815ae06d94 Mon Sep 17 00:00:00 2001 From: yangzhongjiao Date: Sat, 9 May 2026 10:00:54 +0000 Subject: [PATCH 13/16] refactor: update ApproveUnmaskingWorkflowReq to make approve_unmasking_workflow optional Modified the ApproveUnmaskingWorkflowReq struct to change the approve_unmasking_workflow field from required to optional. Updated related documentation to reflect this change, clarifying that the field now carries an optional approve_reason. This adjustment improves API flexibility and aligns with user needs. --- api/dms/service/v1/unmasking_workflow.go | 261 ++++++++++++++++++ .../apiserver/service/data_mask_controller.go | 4 +- 2 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 api/dms/service/v1/unmasking_workflow.go diff --git a/api/dms/service/v1/unmasking_workflow.go b/api/dms/service/v1/unmasking_workflow.go new file mode 100644 index 000000000..baff76855 --- /dev/null +++ b/api/dms/service/v1/unmasking_workflow.go @@ -0,0 +1,261 @@ +package v1 + +import ( + "github.com/actiontech/dms/internal/data_masking/biz" + base "github.com/actiontech/dms/pkg/dms-common/api/base/v1" +) + +// swagger:parameters ListUnmaskingWorkflows +type ListUnmaskingWorkflowsReq struct { + // project id + // Required: true + // in: path + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // the maximum count of workflows to be returned + // in: query + // Required: true + PageSize uint32 `query:"page_size" json:"page_size" validate:"required"` + // the offset of workflows to be returned, default is 0 + // in: query + PageIndex uint32 `query:"page_index" json:"page_index"` + // filter the approval status + // in: query + FilterByApprovalStatus biz.UnmaskingWorkflowApprovalStatus `query:"filter_by_approval_status" json:"filter_by_approval_status"` + // filter the usage status + // in: query + FilterByUsageStatus biz.UnmaskingWorkflowUsageStatus `query:"filter_by_usage_status" json:"filter_by_usage_status"` + // filter db_service id + // in: query + FilterByDBServiceUid string `query:"filter_by_db_service_uid" json:"filter_by_db_service_uid"` +} + +// swagger:model ListUnmaskingWorkflowsReply +type ListUnmaskingWorkflowsReply struct { + Data []*UnmaskingWorkflowListItem `json:"data"` + Total int64 `json:"total_nums"` + // Generic reply + base.GenericResp +} + +// swagger:model UnmaskingWorkflowListItem +type UnmaskingWorkflowListItem struct { + // 申请编号 + WorkflowID string `json:"workflow_id"` + // 申请人用户名 + ApplicantName string `json:"applicant_name"` + // 申请时间 (RFC3339) + CreatedAt string `json:"created_at" example:"2024-01-15T10:30:00Z"` + // 数据源实例名称 + DatasourceName string `json:"datasource_name"` + // 数据源实例ID + DatasourceUid string `json:"datasource_uid"` + // 来源类型 + SourceType biz.UnmaskingWorkflowSourceType `json:"source_type" validate:"oneof=data_export sql_workbench"` + // 来源对象UID + SourceUID string `json:"source_uid"` + // 审批状态 + ApprovalStatus biz.UnmaskingWorkflowApprovalStatus `json:"approval_status" validate:"oneof=pending approved rejected cancelled"` + // 使用情况 + UsageStatus biz.UnmaskingWorkflowUsageStatus `json:"usage_status" validate:"oneof=unviewed viewed"` + // 过期时间 (RFC3339) + ExpireTime string `json:"expire_time" example:"2024-01-16T10:30:00Z"` + // 申请理由 + ApplyReason string `json:"apply_reason"` + // 当前待处理人 + CurrentAssignees []*UidWithName `json:"current_assignees"` +} + +// swagger:model CreateUnmaskingWorkflowReq +type CreateUnmaskingWorkflowReq struct { + // swagger:ignore + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // in: body + // Required: true + UnmaskingWorkflow *CreateUnmaskingWorkflow `json:"unmasking_workflow" validate:"required"` +} + +// swagger:model CreateUnmaskingWorkflow +type CreateUnmaskingWorkflow struct { + // 数据源 UID + DatasourceUID string `json:"datasource_uid" validate:"required"` + // SQL 默认 schema + DefaultSchema string `json:"default_schema" validate:"required"` + // 来源类型 + SourceType biz.UnmaskingWorkflowSourceType `json:"source_type" validate:"required,oneof=data_export sql_workbench"` + // 来源对象 UID (如数据导出任务 UID) + SourceUID string `json:"source_uid"` + // 申请理由 + ApplyReason string `json:"apply_reason" validate:"required"` + // 待脱敏 SQL 列表 + UnmaskingSQLs []CreateUnmaskingSQLItem `json:"unmasking_sqls" validate:"required,gt=0"` +} + +// swagger:model CreateUnmaskingSQLItem +type CreateUnmaskingSQLItem struct { + // 来源侧 SQL 索引 id(如数据导出记录中的 SQL 序号);与 SQL 工作台场景的索引约定可能不同,需结合 source_type、source_uid 解析 + SQLIndexID string `json:"sql_index_id" validate:"required"` + // 原始 SQL 内容 + SQLContent string `json:"sql_content" validate:"required"` +} + +// swagger:model CreateUnmaskingWorkflowReply +type CreateUnmaskingWorkflowReply struct { + Data *CreateUnmaskingWorkflowReplyData `json:"data"` + // Generic reply + base.GenericResp +} + +// swagger:model CreateUnmaskingWorkflowReplyData +type CreateUnmaskingWorkflowReplyData struct { + WorkflowID string `json:"workflow_id"` +} + +// swagger:parameters GetUnmaskingWorkflow +type GetUnmaskingWorkflowReq struct { + // project id + // Required: true + // in: path + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // in: path + // Required: true + WorkflowID string `param:"workflow_id" json:"workflow_id" validate:"required"` +} + +// swagger:model GetUnmaskingWorkflowReply +type GetUnmaskingWorkflowReply struct { + Data *UnmaskingWorkflowDetail `json:"data"` + // Generic reply + base.GenericResp +} + +// swagger:model UnmaskingWorkflowDetail +type UnmaskingWorkflowDetail struct { + UnmaskingWorkflowListItem + // 驳回理由 (整单驳回时) + RejectReason string `json:"reject_reason"` + // 当前待处理人 + CurrentAssignees []*UidWithName `json:"current_assignees"` + // SQL 详情列表 + UnmaskingSQLs []*UnmaskingSQLDetail `json:"unmasking_sqls"` + // 操作日志 + OperationLogs []*UnmaskingOperationLogItem `json:"operation_logs"` +} + +// swagger:model UnmaskingSQLDetail +type UnmaskingSQLDetail struct { + // SQL 详情 UID + UID string `json:"uid"` + // 来源侧 SQL 索引 id + SQLIndexID string `json:"sql_index_id"` + // 原始 SQL 内容 + SQLContent string `json:"sql_content"` + // 脱敏配置快照 + MaskingConfigSnapshot []*biz.ColumnMaskingConfig `json:"masking_config_snapshot,omitempty"` + // 血缘分析快照 + LineageAnalysisSnapshot *biz.AnalyzeResult `json:"lineage_analysis_snapshot,omitempty"` + + UnmaskingSQLPreviewData +} + +// swagger:model UnmaskingSQLPreviewData +type UnmaskingSQLPreviewData struct { + // 脱敏后的预览数据 (普通用户仅能看到此数据) + MaskedData *SQLQueryResult `json:"masked_data"` + // 原始采样数据 (仅有权限的审核人能看到) + OriginalData *SQLQueryResult `json:"original_data"` +} + +// swagger:model SQLQueryResultRow +// SQLQueryResultRow 一行数据;每个元素为单元格的字符串形式(与 columns 顺序一致)。 +type SQLQueryResultRow []string + +// swagger:model SQLQueryResult +type SQLQueryResult struct { + // 列名列表 + Columns []string `json:"columns"` + // 数据行列表 (每一行的数据顺序与 Columns 一致) + Rows []SQLQueryResultRow `json:"rows"` + // 总行数 + RowCount int `json:"row_count"` +} + +// swagger:model UnmaskingOperationLogItem +type UnmaskingOperationLogItem struct { + // 操作人 UID + OperatorUID string `json:"operator_uid"` + // 操作人姓名 + OperatorName string `json:"operator_name"` + // 操作动作 + Action biz.UnmaskingAction `json:"action"` + // 操作时间 (RFC3339) + ActionTime string `json:"action_time"` + // 额外信息 (如拦截原因) + ExtraMessage string `json:"extra_message"` +} + +// swagger:model ApproveUnmaskingWorkflowReq +type ApproveUnmaskingWorkflowReq struct { + // swagger:ignore + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // in: path + // Required: true + // swagger:ignore + WorkflowID string `param:"workflow_id" json:"workflow_id" validate:"required"` + // in: body + ApproveUnmaskingWorkflow *ApproveUnmaskingWorkflow `json:"approve_unmasking_workflow,omitempty"` +} + +// swagger:model ApproveUnmaskingWorkflow +type ApproveUnmaskingWorkflow struct { + // 审批理由 非必须 + ApproveReason string `json:"approve_reason"` +} + +// swagger:model ApproveUnmaskingWorkflowReply +type ApproveUnmaskingWorkflowReply struct { + // Generic reply + base.GenericResp +} + +// swagger:model RejectUnmaskingWorkflowReq +type RejectUnmaskingWorkflowReq struct { + // swagger:ignore + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // in: path + // Required: true + // swagger:ignore + WorkflowID string `param:"workflow_id" json:"workflow_id" validate:"required"` + // in: body + // Required: true + RejectUnmaskingWorkflow *RejectUnmaskingWorkflow `json:"reject_unmasking_workflow" validate:"required"` +} + +// swagger:model RejectUnmaskingWorkflow +type RejectUnmaskingWorkflow struct { + // 驳回理由 + // Required: true + RejectReason string `json:"reject_reason" validate:"required"` +} + +// swagger:model RejectUnmaskingWorkflowReply +type RejectUnmaskingWorkflowReply struct { + // Generic reply + base.GenericResp +} + +// swagger:parameters CancelUnmaskingWorkflow +type CancelUnmaskingWorkflowReq struct { + // project id + // Required: true + // in: path + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // in: path + // Required: true + WorkflowID string `param:"workflow_id" json:"workflow_id" validate:"required"` +} + +// swagger:model CancelUnmaskingWorkflowReply +type CancelUnmaskingWorkflowReply struct { + // Generic reply + base.GenericResp +} diff --git a/internal/apiserver/service/data_mask_controller.go b/internal/apiserver/service/data_mask_controller.go index 65b0d167e..45054c5e5 100644 --- a/internal/apiserver/service/data_mask_controller.go +++ b/internal/apiserver/service/data_mask_controller.go @@ -680,9 +680,9 @@ func (ctl *DMSController) GetUnmaskingWorkflow(c echo.Context) error { // required: true // type: string // - name: approve_unmasking_workflow -// description: approve unmasking workflow info +// description: approve unmasking workflow info (optional, only carries approve_reason) // in: body -// required: true +// required: false // schema: // "$ref": "#/definitions/ApproveUnmaskingWorkflow" // From afb696eedba4708eb9dc80a7b24e928c10000fcf Mon Sep 17 00:00:00 2001 From: yangzhongjiao Date: Sat, 9 May 2026 10:19:59 +0000 Subject: [PATCH 14/16] refactor: improve SQL audit middleware error handling and request parsing Updated the AuditMiddleware function to enhance error handling by logging warnings instead of errors when parsing SQL requests fails or when SQL and datasource ID are empty. This change ensures that audit processing does not block user SQL execution. Additionally, modified the base64 decoding to use URL-safe encoding, improving compatibility with session IDs generated by the ODC server. --- .../service/sql_workbench_service.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/sql_workbench/service/sql_workbench_service.go b/internal/sql_workbench/service/sql_workbench_service.go index 4862b1749..dcd3a658b 100644 --- a/internal/sql_workbench/service/sql_workbench_service.go +++ b/internal/sql_workbench/service/sql_workbench_service.go @@ -1045,15 +1045,18 @@ func (sqlWorkbenchService *SqlWorkbenchService) AuditMiddleware() echo.Middlewar c.Request().Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // 解析请求体获取 SQL 和 datasource ID + // 注意:解析仅服务于审核辅助路径,解析失败不应直接阻塞用户的 SQL 执行; + // 否则一旦中间件辅助能力出错(如 sid 解码失败),用户连查询都跑不了。 + // 真正的「未启用审核 / 审核失败」等强策略仍由后续分支按既有 fail-closed 处理。 sql, datasourceID, err := sqlWorkbenchService.parseStreamExecuteRequest(bodyBytes) if err != nil { - sqlWorkbenchService.log.Errorf("failed to parse streamExecute request, skipping audit: %v", err) - return errors.New(locale.Bundle.LocalizeMsgByCtx(c.Request().Context(), locale.SqlWorkbenchAuditParseReqErr)) + sqlWorkbenchService.log.Warnf("failed to parse streamExecute request, skipping audit: %v", err) + return next(c) } if sql == "" || datasourceID == "" { - sqlWorkbenchService.log.Debugf("SQL or datasource ID is empty, skipping audit") - return errors.New(locale.Bundle.LocalizeMsgByCtx(c.Request().Context(), locale.SqlWorkbenchAuditMissingSQLOrDatasourceErr)) + sqlWorkbenchService.log.Warnf("SQL or datasource ID is empty, skipping audit") + return next(c) } // 获取当前用户 ID @@ -1148,8 +1151,9 @@ func (sqlWorkbenchService *SqlWorkbenchService) parseSidToDatasourceID(sid strin sid = sid[:idx] } - // 解码 base64 - decodedBytes, err := base64.StdEncoding.DecodeString(sid) + // ODC 服务端使用 Base64.getUrlEncoder() 生成 sessionId(URL-safe,包含 '-'/'_'), + // 这里必须用 URLEncoding 解码,否则遇到 '-'/'_' 会报 illegal base64 data。 + decodedBytes, err := base64.URLEncoding.DecodeString(sid) if err != nil { return "", fmt.Errorf("failed to decode base64 sid: %v", err) } From d6d303f3e0e8f8caa5b37707ce38e9803b2728a2 Mon Sep 17 00:00:00 2001 From: yangzhongjiao Date: Mon, 11 May 2026 11:27:37 +0000 Subject: [PATCH 15/16] refactor: streamline Cloudbeaver and DataExportWorkflow usecases by removing maskingTaskRepo Removed the maskingTaskRepo from the CloudbeaverUsecase and DataExportWorkflowUsecase, replacing its functionality with the new HasSensitiveDataMaskingTask method in the DBServiceUsecase. This change simplifies the usecases and enhances the handling of sensitive data masking tasks, improving overall code clarity and maintainability. --- internal/dms/biz/cloudbeaver.go | 12 +++++------ internal/dms/biz/data_export_workflow.go | 6 +++--- internal/dms/biz/db_service.go | 20 +++++++++++++++++++ internal/dms/service/cloudbeaver.go | 2 +- internal/dms/service/service.go | 2 +- .../service/data_masking_middleware.go | 2 +- 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/internal/dms/biz/cloudbeaver.go b/internal/dms/biz/cloudbeaver.go index 1ab04ff61..166e30322 100644 --- a/internal/dms/biz/cloudbeaver.go +++ b/internal/dms/biz/cloudbeaver.go @@ -91,13 +91,12 @@ type CloudbeaverUsecase struct { sqlResultMasker SQLResultMasker cbOperationLogUsecase *CbOperationLogUsecase projectUsecase *ProjectUsecase - maskingTaskRepo MaskingTaskRepo repo CloudbeaverRepo proxyTargetRepo ProxyTargetRepo maintenanceTimeUsecase *MaintenanceTimeUsecase } -func NewCloudbeaverUsecase(log utilLog.Logger, cfg *CloudbeaverCfg, userUsecase *UserUsecase, dbServiceUsecase *DBServiceUsecase, opPermissionVerifyUsecase *OpPermissionVerifyUsecase, dmsConfigUseCase *DMSConfigUseCase, sqlResultMasker SQLResultMasker, cloudbeaverRepo CloudbeaverRepo, proxyTargetRepo ProxyTargetRepo, cbOperationUseCase *CbOperationLogUsecase, projectUsecase *ProjectUsecase, maskingTaskRepo MaskingTaskRepo, maintenanceTimeUsecase *MaintenanceTimeUsecase) (cu *CloudbeaverUsecase) { +func NewCloudbeaverUsecase(log utilLog.Logger, cfg *CloudbeaverCfg, userUsecase *UserUsecase, dbServiceUsecase *DBServiceUsecase, opPermissionVerifyUsecase *OpPermissionVerifyUsecase, dmsConfigUseCase *DMSConfigUseCase, sqlResultMasker SQLResultMasker, cloudbeaverRepo CloudbeaverRepo, proxyTargetRepo ProxyTargetRepo, cbOperationUseCase *CbOperationLogUsecase, projectUsecase *ProjectUsecase, maintenanceTimeUsecase *MaintenanceTimeUsecase) (cu *CloudbeaverUsecase) { cu = &CloudbeaverUsecase{ repo: cloudbeaverRepo, proxyTargetRepo: proxyTargetRepo, @@ -108,7 +107,6 @@ func NewCloudbeaverUsecase(log utilLog.Logger, cfg *CloudbeaverCfg, userUsecase sqlResultMasker: sqlResultMasker, cbOperationLogUsecase: cbOperationUseCase, projectUsecase: projectUsecase, - maskingTaskRepo: maskingTaskRepo, cloudbeaverCfg: cfg, log: utilLog.NewHelper(log, utilLog.WithMessageKey("biz.cloudbeaver")), maintenanceTimeUsecase: maintenanceTimeUsecase, @@ -467,7 +465,7 @@ func (cu *CloudbeaverUsecase) GraphQLDistributor() echo.MiddlewareFunc { return err } - isMaskingEnabled, _ := cu.maskingTaskRepo.CheckMaskingTaskExist(c.Request().Context(), dbService.UID) + isMaskingEnabled := cu.dbServiceUsecase != nil && cu.dbServiceUsecase.HasSensitiveDataMaskingTask(c.Request().Context(), dbService.UID) // 构建任务ID与数据脱敏的关联 return cu.buildTaskIdAssocDataMasking(cloudbeaverResBuf.Bytes(), taskMaskingContext{ Enabled: isMaskingEnabled, @@ -497,7 +495,7 @@ func (cu *CloudbeaverUsecase) GraphQLDistributor() echo.MiddlewareFunc { cu.log.Error(err) } - isMaskingEnabled, _ := cu.maskingTaskRepo.CheckMaskingTaskExist(c.Request().Context(), dbService.UID) + isMaskingEnabled := cu.dbServiceUsecase != nil && cu.dbServiceUsecase.HasSensitiveDataMaskingTask(c.Request().Context(), dbService.UID) maskCtx := taskMaskingContext{ Enabled: isMaskingEnabled, DBServiceUID: dbService.UID, @@ -567,7 +565,7 @@ func (cu *CloudbeaverUsecase) GraphQLDistributor() echo.MiddlewareFunc { return err } - isMaskingEnabled, _ := cu.maskingTaskRepo.CheckMaskingTaskExist(c.Request().Context(), dbService.UID) + isMaskingEnabled := cu.dbServiceUsecase != nil && cu.dbServiceUsecase.HasSensitiveDataMaskingTask(c.Request().Context(), dbService.UID) // 构建任务ID与数据脱敏的关联 return cu.buildTaskIdAssocDataMasking(cloudbeaverResBuf.Bytes(), taskMaskingContext{ Enabled: isMaskingEnabled, @@ -702,7 +700,7 @@ func (cu *CloudbeaverUsecase) GraphQLDistributor() echo.MiddlewareFunc { } if params.OperationName == "asyncSqlExecuteQuery" { - isMaskingEnabled, _ := cu.maskingTaskRepo.CheckMaskingTaskExist(c.Request().Context(), dbService.UID) + isMaskingEnabled := cu.dbServiceUsecase != nil && cu.dbServiceUsecase.HasSensitiveDataMaskingTask(c.Request().Context(), dbService.UID) if err := cu.buildTaskIdAssocDataMasking(cloudbeaverResBuf.Bytes(), taskMaskingContext{ Enabled: isMaskingEnabled, DBServiceUID: dbService.UID, diff --git a/internal/dms/biz/data_export_workflow.go b/internal/dms/biz/data_export_workflow.go index a574bedb2..be58e7063 100644 --- a/internal/dms/biz/data_export_workflow.go +++ b/internal/dms/biz/data_export_workflow.go @@ -136,13 +136,13 @@ type DataExportWorkflowUsecase struct { webhookUsecase *WebHookConfigurationUsecase userUsecase *UserUsecase systemVariableUsecase *SystemVariableUsecase - maskingTaskRepo MaskingTaskRepo + dbServiceUsecase *DBServiceUsecase unmaskingWorkflowUsecase *dataMaskingBiz.UnmaskingWorkflowUsecase log *utilLog.Helper reportHost string } -func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator, repo WorkflowRepo, dataExportTaskRepo DataExportTaskRepo, dbServiceRepo DBServiceRepo, maskingConfigRepo DataExportMaskingConfigRepo, maskingRuleRepo DataExportMaskingRuleRepo, opPermissionVerifyUsecase *OpPermissionVerifyUsecase, projectUsecase *ProjectUsecase, proxyTargetRepo ProxyTargetRepo, clusterUseCase *ClusterUsecase, webhookUsecase *WebHookConfigurationUsecase, userUsecase *UserUsecase, systemVariableUsecase *SystemVariableUsecase, maskingTaskRepo MaskingTaskRepo, unmaskingWorkflowUsecase *dataMaskingBiz.UnmaskingWorkflowUsecase, reportHost string) *DataExportWorkflowUsecase { +func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator, repo WorkflowRepo, dataExportTaskRepo DataExportTaskRepo, dbServiceRepo DBServiceRepo, maskingConfigRepo DataExportMaskingConfigRepo, maskingRuleRepo DataExportMaskingRuleRepo, opPermissionVerifyUsecase *OpPermissionVerifyUsecase, projectUsecase *ProjectUsecase, proxyTargetRepo ProxyTargetRepo, clusterUseCase *ClusterUsecase, webhookUsecase *WebHookConfigurationUsecase, userUsecase *UserUsecase, systemVariableUsecase *SystemVariableUsecase, dbServiceUsecase *DBServiceUsecase, unmaskingWorkflowUsecase *dataMaskingBiz.UnmaskingWorkflowUsecase, reportHost string) *DataExportWorkflowUsecase { return &DataExportWorkflowUsecase{ tx: tx, repo: repo, @@ -157,7 +157,7 @@ func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator webhookUsecase: webhookUsecase, userUsecase: userUsecase, systemVariableUsecase: systemVariableUsecase, - maskingTaskRepo: maskingTaskRepo, + dbServiceUsecase: dbServiceUsecase, unmaskingWorkflowUsecase: unmaskingWorkflowUsecase, log: utilLog.NewHelper(logger, utilLog.WithMessageKey("biz.dataExportWorkflow")), reportHost: reportHost, diff --git a/internal/dms/biz/db_service.go b/internal/dms/biz/db_service.go index 782a56d3e..bdc4d5d8c 100644 --- a/internal/dms/biz/db_service.go +++ b/internal/dms/biz/db_service.go @@ -201,6 +201,26 @@ func NewDBServiceUsecase(log utilLog.Logger, repo DBServiceRepo, maskingTaskRepo } } +// CheckSensitiveDataMaskingTask 查询 data_masking_discovery_tasks 是否存在该数据源的任务行;错误原样返回(用于导出等需失败路径)。 +func (d *DBServiceUsecase) CheckSensitiveDataMaskingTask(ctx context.Context, dbServiceUID string) (bool, error) { + if d == nil || d.maskingTaskRepo == nil || dbServiceUID == "" { + return false, nil + } + return d.maskingTaskRepo.CheckMaskingTaskExist(ctx, dbServiceUID) +} + +// HasSensitiveDataMaskingTask 该数据源是否已存在敏感数据发现任务;查询语义与 CheckSensitiveDataMaskingTask 一致,存储错误时视为未开启并打 debug 日志。 +func (d *DBServiceUsecase) HasSensitiveDataMaskingTask(ctx context.Context, dbServiceUID string) bool { + ok, err := d.CheckSensitiveDataMaskingTask(ctx, dbServiceUID) + if err != nil { + if d != nil && d.log != nil { + d.log.Debugf("CheckSensitiveDataMaskingTask db_service_uid=%s: %v", dbServiceUID, err) + } + return false + } + return ok +} + type BizDBServiceArgs struct { Name string Desc *string diff --git a/internal/dms/service/cloudbeaver.go b/internal/dms/service/cloudbeaver.go index ecd135ba3..4a721dc08 100644 --- a/internal/dms/service/cloudbeaver.go +++ b/internal/dms/service/cloudbeaver.go @@ -82,7 +82,7 @@ func NewAndInitCloudbeaverService(logger utilLog.Logger, opts *conf.DMSOptions) } } - cloudbeaverUsecase := biz.NewCloudbeaverUsecase(logger, cfg, userUsecase, dbServiceUseCase, opPermissionVerifyUsecase, dmsConfigUseCase, sqlResultMasker, cloudbeaverRepo, dmsProxyTargetRepo, cbOperationLogUsecase, projectUsecase, discoveryTaskRepo, maintenanceTimeUsecase) + cloudbeaverUsecase := biz.NewCloudbeaverUsecase(logger, cfg, userUsecase, dbServiceUseCase, opPermissionVerifyUsecase, dmsConfigUseCase, sqlResultMasker, cloudbeaverRepo, dmsProxyTargetRepo, cbOperationLogUsecase, projectUsecase, maintenanceTimeUsecase) proxyUsecase := biz.NewCloudbeaverProxyUsecase(logger, cloudbeaverUsecase) return &CloudbeaverService{ diff --git a/internal/dms/service/service.go b/internal/dms/service/service.go index 69897ec6b..10cd8c75e 100644 --- a/internal/dms/service/service.go +++ b/internal/dms/service/service.go @@ -158,7 +158,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer if err != nil { return nil, fmt.Errorf("failed to initialize unmasking workflow usecase: %v", err) } - DataExportWorkflowUsecase := biz.NewDataExportWorkflowUsecase(logger, tx, workflowRepo, dataExportTaskRepo, dbServiceRepo, dataExportMaskingConfigRepo, dataExportMaskingRuleRepo, opPermissionVerifyUsecase, projectUsecase, dmsProxyTargetRepo, clusterUsecase, webhookConfigurationUsecase, userUsecase, systemVariableUsecase, discoveryTaskRepo, unmaskingWorkflowUsecase, fmt.Sprintf("%s:%d", opts.ReportHost, opts.APIServiceOpts.Port)) + DataExportWorkflowUsecase := biz.NewDataExportWorkflowUsecase(logger, tx, workflowRepo, dataExportTaskRepo, dbServiceRepo, dataExportMaskingConfigRepo, dataExportMaskingRuleRepo, opPermissionVerifyUsecase, projectUsecase, dmsProxyTargetRepo, clusterUsecase, webhookConfigurationUsecase, userUsecase, systemVariableUsecase, dbServiceUseCase, unmaskingWorkflowUsecase, fmt.Sprintf("%s:%d", opts.ReportHost, opts.APIServiceOpts.Port)) dataMaskingUsecase, stopDataMaskingScheduler, err := initDataMaskingUsecase(logger, st, dbServiceUseCase, clusterUsecase, dmsProxyTargetRepo) if err != nil { return nil, fmt.Errorf("failed to initialize data masking usecase: %v", err) diff --git a/internal/sql_workbench/service/data_masking_middleware.go b/internal/sql_workbench/service/data_masking_middleware.go index 46e0dc766..621dca64c 100644 --- a/internal/sql_workbench/service/data_masking_middleware.go +++ b/internal/sql_workbench/service/data_masking_middleware.go @@ -1,8 +1,8 @@ package sql_workbench import ( - "github.com/actiontech/dms/internal/dms/biz" dataMaskingBiz "github.com/actiontech/dms/internal/data_masking/biz" + "github.com/actiontech/dms/internal/dms/biz" "github.com/actiontech/dms/internal/sql_workbench/sqlresultmasker" ) From 162dc223945cde45d090845241dbb1c5a54619e7 Mon Sep 17 00:00:00 2001 From: yangzhongjiao Date: Tue, 12 May 2026 07:38:48 +0000 Subject: [PATCH 16/16] fix(api): align data export and db structure column contracts with EE --- api/dms/service/v1/data_export_task.go | 5 +++ api/dms/service/v1/data_export_workflow.go | 15 +++++++++ api/dms/service/v1/db_structure_columns.go | 36 ++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 api/dms/service/v1/db_structure_columns.go diff --git a/api/dms/service/v1/data_export_task.go b/api/dms/service/v1/data_export_task.go index 37b8eb5ae..498616d88 100644 --- a/api/dms/service/v1/data_export_task.go +++ b/api/dms/service/v1/data_export_task.go @@ -3,6 +3,7 @@ package v1 import ( "time" + maskingBiz "github.com/actiontech/dms/internal/data_masking/biz" base "github.com/actiontech/dms/pkg/dms-common/api/base/v1" ) @@ -127,6 +128,10 @@ type ListDataExportTaskSQL struct { ExportSQLType string `json:"export_sql_type"` AuditLevel string `json:"audit_level"` AuditSQLResult []AuditSQLResult `json:"audit_sql_result"` + // 血缘分析快照(与查看原文工单 SQL 详情字段语义一致) + LineageAnalysisSnapshot *maskingBiz.AnalyzeResult `json:"lineage_analysis_snapshot,omitempty"` + // 脱敏配置快照 + MaskingConfigSnapshot []*maskingBiz.ColumnMaskingConfig `json:"masking_config_snapshot,omitempty"` } type AuditSQLResult struct { Level string `json:"level" example:"warn"` diff --git a/api/dms/service/v1/data_export_workflow.go b/api/dms/service/v1/data_export_workflow.go index 3e3c81519..c2721ab58 100644 --- a/api/dms/service/v1/data_export_workflow.go +++ b/api/dms/service/v1/data_export_workflow.go @@ -267,6 +267,21 @@ type ExportDataExportWorkflowReq struct { DataExportWorkflowUid string `param:"data_export_workflow_uid" json:"data_export_workflow_uid" validate:"required"` } +// swagger:parameters DownloadOriginalDataExportWorkflow +type DownloadOriginalDataExportWorkflowReq struct { + // project id + // Required: true + // in:path + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // Required: true + // in:path + DataExportWorkflowUid string `param:"data_export_workflow_uid" json:"data_export_workflow_uid" validate:"required"` + // 已批准的查看原文工单 UID + // Required: true + // in:query + UnmaskingWorkflowUid string `query:"unmasking_workflow_uid" json:"unmasking_workflow_uid" validate:"required"` +} + type RejectDataExportWorkflowPayload struct { // Required: true Reason string `json:"reason" validate:"required"` diff --git a/api/dms/service/v1/db_structure_columns.go b/api/dms/service/v1/db_structure_columns.go new file mode 100644 index 000000000..632e694d6 --- /dev/null +++ b/api/dms/service/v1/db_structure_columns.go @@ -0,0 +1,36 @@ +package v1 + +import ( + base "github.com/actiontech/dms/pkg/dms-common/api/base/v1" +) + +// swagger:parameters ListTableColumns +type ListTableColumnsReq struct { + // Required: true + // in:path + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // Required: true + // in:path + DBServiceUid string `param:"db_service_uid" json:"db_service_uid" validate:"required"` + // Required: true + // in:path + SchemaName string `param:"schema_name" json:"schema_name" validate:"required"` + // Required: true + // in:path + TableName string `param:"table_name" json:"table_name" validate:"required"` +} + +// swagger:model ListTableColumnsReply +type ListTableColumnsReply struct { + Data []*TableColumn `json:"data"` + // Generic reply + base.GenericResp +} + +// swagger:model TableColumn +type TableColumn struct { + Name string `json:"name"` + Type string `json:"type"` + Comment string `json:"comment"` + Nullable bool `json:"nullable"` +}