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/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"` +} 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" 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 560b9ab8b..45054c5e5 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: 处理动作信息 -// in: body +// 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 (optional, only carries approve_reason) +// in: body +// required: false // 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/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 +} 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" +) diff --git a/internal/dms/biz/cloudbeaver.go b/internal/dms/biz/cloudbeaver.go index 24766f0f7..166e30322 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 { @@ -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, @@ -339,6 +337,7 @@ type taskMaskingContext struct { Enabled bool DBServiceUID string SchemaName string + ProjectUID string } var ( @@ -466,11 +465,12 @@ 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, DBServiceUID: dbService.UID, + ProjectUID: dbService.ProjectUID, }) } @@ -495,10 +495,11 @@ 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, + ProjectUID: dbService.ProjectUID, } if ep, epErr := cu.getWorkflowExecParams(c, params); epErr == nil { maskCtx.SchemaName = ep.instanceSchema @@ -564,11 +565,12 @@ 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, DBServiceUID: dbService.UID, + ProjectUID: dbService.ProjectUID, }) } @@ -698,11 +700,12 @@ 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, SchemaName: maskingSchemaName, + ProjectUID: dbService.ProjectUID, }); err != nil { return nil, err } @@ -754,7 +757,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/dms/biz/data_export_workflow.go b/internal/dms/biz/data_export_workflow.go index 148acaf2c..be58e7063 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") @@ -134,12 +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, 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, @@ -154,7 +157,8 @@ 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/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/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/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/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 +} + diff --git a/internal/dms/service/service.go b/internal/dms/service/service.go index b0f23dfb5..10cd8c75e 100644 --- a/internal/dms/service/service.go +++ b/internal/dms/service/service.go @@ -41,6 +41,7 @@ type DMSService struct { LicenseUsecase *biz.LicenseUsecase ClusterUsecase *biz.ClusterUsecase DataExportWorkflowUsecase *biz.DataExportWorkflowUsecase + UnmaskingWorkflowUsecase *unmaskingWorkflowUsecase CbOperationLogUsecase *biz.CbOperationLogUsecase DataMaskingUsecase *dataMaskingUsecase FunctionSupportRegistry *biz.FunctionSupportRegistry @@ -153,7 +154,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, 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, 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) @@ -202,6 +207,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer LicenseUsecase: LicenseUsecase, ClusterUsecase: clusterUsecase, DataExportWorkflowUsecase: DataExportWorkflowUsecase, + 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 +} 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: "未找到对应的查看原文工单"} ) 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..621dca64c --- /dev/null +++ b/internal/sql_workbench/service/data_masking_middleware.go @@ -0,0 +1,15 @@ +package sql_workbench + +import ( + dataMaskingBiz "github.com/actiontech/dms/internal/data_masking/biz" + "github.com/actiontech/dms/internal/dms/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) + } + } +} diff --git a/internal/sql_workbench/service/sql_workbench_service.go b/internal/sql_workbench/service/sql_workbench_service.go index c86563ede..dcd3a658b 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) { @@ -1035,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 @@ -1138,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) } @@ -1427,14 +1441,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 +1461,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 +1475,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 +1489,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 +1498,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 +1750,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) +} 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