diff --git a/api/dms/service/v1/operation_record.go b/api/dms/service/v1/operation_record.go index 1ab6d268..ad3ce8c2 100644 --- a/api/dms/service/v1/operation_record.go +++ b/api/dms/service/v1/operation_record.go @@ -40,9 +40,15 @@ type GetOperationRecordListReq struct { // in:query FuzzySearchOperateUserName string `json:"fuzzy_search_operate_user_name" query:"fuzzy_search_operate_user_name"` // in:query - FilterOperateTypeName string `json:"filter_operate_type_name" query:"filter_operate_type_name"` + FuzzySearchOperateContent string `json:"fuzzy_search_operate_content" query:"fuzzy_search_operate_content"` // in:query - FilterOperateAction string `json:"filter_operate_action" query:"filter_operate_action"` + FilterFuzzyOperateUserName string `json:"filter_fuzzy_operate_user_name" query:"filter_fuzzy_operate_user_name"` + // in:query + FilterOperateTypeNames []string `json:"filter_operate_type_names" query:"filter_operate_type_names"` + // in:query + FilterOperateActions []string `json:"filter_operate_actions" query:"filter_operate_actions"` + // in:query + FilterOperateStatus string `json:"filter_operate_status" query:"filter_operate_status"` // in:query // Required: true PageIndex uint32 `json:"page_index" query:"page_index" validate:"required"` @@ -67,7 +73,7 @@ type OperationRecordListItem struct { OperationAction string `json:"operation_action"` OperationContent string `json:"operation_content"` ProjectName string `json:"project_name"` - // enum: ["succeeded","failed"] + // enum: succeeded,failed Status string `json:"status"` } @@ -76,6 +82,47 @@ type OperationUser struct { IP string `json:"ip"` } +// swagger:model GetOperationTypeNameListReply +type GetOperationTypeNameListReply struct { + Data []OperationTypeNameListItem `json:"data"` + base.GenericResp +} + +type OperationTypeNameListItem struct { + OperationTypeName string `json:"operation_type_name"` + Desc string `json:"desc"` +} + +// swagger:model GetOperationActionListReply +type GetOperationActionListReply struct { + Data []OperationActionListItem `json:"data"` + base.GenericResp +} + +type OperationActionListItem struct { + OperationType string `json:"operation_type"` + OperationAction string `json:"operation_action"` + Desc string `json:"desc"` +} + +// swagger:model GetOperationUserNameListReply +type GetOperationUserNameListReply struct { + Data []OperationUserNameListItem `json:"data"` + base.GenericResp +} + +type OperationUserNameListItem struct { + OperationUserName string `json:"operation_user_name"` + OperationReqIP string `json:"operation_req_ip"` + Desc string `json:"desc"` +} + +// swagger:parameters GetOperationUserNameList +type GetOperationUserNameListReq struct { + // in:query + FilterOperateProjectName *string `json:"filter_operate_project_name" query:"filter_operate_project_name"` +} + // swagger:parameters ExportOperationRecordList type ExportOperationRecordListReq struct { // in:query @@ -87,9 +134,15 @@ type ExportOperationRecordListReq struct { // in:query FuzzySearchOperateUserName string `json:"fuzzy_search_operate_user_name" query:"fuzzy_search_operate_user_name"` // in:query - FilterOperateTypeName string `json:"filter_operate_type_name" query:"filter_operate_type_name"` + FuzzySearchOperateContent string `json:"fuzzy_search_operate_content" query:"fuzzy_search_operate_content"` + // in:query + FilterFuzzyOperateUserName string `json:"filter_fuzzy_operate_user_name" query:"filter_fuzzy_operate_user_name"` + // in:query + FilterOperateTypeNames []string `json:"filter_operate_type_names" query:"filter_operate_type_names"` + // in:query + FilterOperateActions []string `json:"filter_operate_actions" query:"filter_operate_actions"` // in:query - FilterOperateAction string `json:"filter_operate_action" query:"filter_operate_action"` + FilterOperateStatus string `json:"filter_operate_status" query:"filter_operate_status"` } // swagger:response ExportOperationRecordListReply diff --git a/internal/apiserver/service/dms_controller.go b/internal/apiserver/service/dms_controller.go index d0d77577..c9ebcf36 100644 --- a/internal/apiserver/service/dms_controller.go +++ b/internal/apiserver/service/dms_controller.go @@ -333,17 +333,11 @@ func (ctl *DMSController) ListGlobalDBServices(c echo.Context) error { // 200: body:ListGlobalDBServicesTipsReply // default: body:GenericResp func (ctl *DMSController) ListGlobalDBServicesTips(c echo.Context) error { - req := new(aV1.ListGlobalDBServicesTipsReq) - err := bindAndValidateReq(c, req) - if nil != err { - return NewErrResp(c, err, apiError.BadRequestErr) - } - currentUserUid, err := jwt.GetUserUidStrFromContext(c) if err != nil { return NewErrResp(c, err, apiError.DMSServiceErr) } - reply, err := ctl.DMS.ListGlobalDBServicesTips(c.Request().Context(), req, currentUserUid) + reply, err := ctl.DMS.ListGlobalDBServicesTips(c.Request().Context(), currentUserUid) if nil != err { return NewErrResp(c, err, apiError.DMSServiceErr) } @@ -1562,11 +1556,8 @@ func (ctl *DMSController) ListMembers(c echo.Context) error { if nil != err { return NewErrResp(c, err, apiError.BadRequestErr) } - currentUserUid, err := jwt.GetUserUidStrFromContext(c) - if err != nil { - return NewErrResp(c, err, apiError.DMSServiceErr) - } - reply, err := ctl.DMS.ListMembers(c.Request().Context(), req, currentUserUid) + + reply, err := ctl.DMS.ListMembers(c.Request().Context(), req) if nil != err { return NewErrResp(c, err, apiError.DMSServiceErr) } @@ -1685,32 +1676,8 @@ func (ctl *DMSController) ListMemberGroups(c echo.Context) error { if nil != err { return NewErrResp(c, err, apiError.BadRequestErr) } - currentUserUid, err := jwt.GetUserUidStrFromContext(c) - if err != nil { - return NewErrResp(c, err, apiError.DMSServiceErr) - } - reply, err := ctl.DMS.ListMemberGroups(c.Request().Context(), req, currentUserUid) - if nil != err { - return NewErrResp(c, err, apiError.DMSServiceErr) - } - return NewOkRespWithReply(c, reply) -} -// swagger:route GET /v1/dms/projects/{project_uid}/member_groups/tips MemberGroup ListMemberGroupTips -// -// List member group tips. -// -// responses: -// 200: body:ListMemberGroupTipsReply -// default: body:GenericResp -func (ctl *DMSController) ListMemberGroupTips(c echo.Context) error { - req := new(aV1.ListMemberGroupTipsReq) - err := bindAndValidateReq(c, req) - if nil != err { - return NewErrResp(c, err, apiError.BadRequestErr) - } - - reply, err := ctl.DMS.ListMemberGroupTips(c.Request().Context(), req.ProjectUid) + reply, err := ctl.DMS.ListMemberGroups(c.Request().Context(), req) if nil != err { return NewErrResp(c, err, apiError.DMSServiceErr) } @@ -2742,10 +2709,8 @@ func (ctl *DMSController) oauth2Callback(c echo.Context) error { return NewErrResp(c, err, apiError.APIServerErr) } - // 1. callbackData.Error 有错误时,前端会回到登录页并展示错误信息 - // 2. callbackData.UserExist 为false时,前端会进入手动绑定页面,绑定时调用绑定接口签发tokens - // 3. 没错误且用户存在时,签发tokens登录成功 - if callbackData.Error == "" && callbackData.UserExist { + // 只有在用户存在才签发tokens,不存在时后续会重定向到用户绑定页,绑定成功后再签发tokens + if callbackData.UserExist { dmsToken, dmsCookieExp, err := claims.DmsToken() if err != nil { return NewErrResp(c, err, apiError.APIServerErr) @@ -3976,32 +3941,6 @@ func (ctl *DMSController) ExportDataExportWorkflow(c echo.Context) error { return NewOkResp(c) } -// swagger:route GET /v1/dms/projects/{project_uid}/data_export_workflows/{data_export_workflow_uid}/original-export/download DataExportWorkflows DownloadOriginalDataExportWorkflow -// -// Download unmasked SQL query results as a zip file. Each request runs export in memory; files are not persisted. -// -// responses: -// 200: DownloadOriginalDataExportWorkflowReply -// default: body:GenericResp -func (ctl *DMSController) DownloadOriginalDataExportWorkflow(c echo.Context) error { - req := new(aV1.DownloadOriginalDataExportWorkflowReq) - 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.DMSServiceErr) - } - fileName, content, err := ctl.DMS.DownloadOriginalDataExportWorkflow(c.Request().Context(), req, currentUserUid) - if err != nil { - return NewErrResp(c, err, apiError.DMSServiceErr) - } - c.Response().Header().Set(echo.HeaderContentDisposition, - mime.FormatMediaType("attachment", map[string]string{"filename": fileName})) - - return c.Blob(http.StatusOK, "application/zip", content) -} - // swagger:operation POST /v1/dms/projects/{project_uid}/data_export_tasks DataExportTask AddDataExportTask // // Add data_export task. @@ -4189,6 +4128,26 @@ func (ctl *DMSController) proxyDownloadDataExportTask(c echo.Context, reportHost return } +// swagger:route GET /v1/dms/masking/rules Masking ListMaskingRules +// +// List masking rules. +// +// responses: +// 200: body:ListMaskingRulesReply +// default: body:GenericResp +func (ctl *DMSController) ListMaskingRules(c echo.Context) error { + req := &aV1.ListMaskingRulesReq{} + err := bindAndValidateReq(c, req) + if nil != err { + return NewErrResp(c, err, apiError.BadRequestErr) + } + + reply, err := ctl.DMS.ListMaskingRules(c.Request().Context()) + if nil != err { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, reply) +} // swagger:route GET /v1/dms/projects/{project_uid}/cb_operation_logs CBOperationLogs ListCBOperationLogs // @@ -5044,6 +5003,84 @@ func (ctl *DMSController) GetOperationRecordList(c echo.Context) error { return NewOkRespWithReply(c, reply) } +// swagger:operation GET /v1/dms/operation_records/operation_type_names OperationRecord GetOperationTypeNameList +// +// Get operation type name list. +// +// --- +// responses: +// +// '200': +// description: GetOperationTypeNameListReply +// schema: +// "$ref": "#/definitions/GetOperationTypeNameListReply" +// default: +// description: GenericResp +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) GetOperationTypeNameList(c echo.Context) error { + reply := getOperationTypeNameListReply(c.Request().Context()) + return NewOkRespWithReply(c, reply) +} + +// swagger:operation GET /v1/dms/operation_records/operation_actions OperationRecord GetOperationActionList +// +// Get operation action list. +// +// --- +// responses: +// +// '200': +// description: GetOperationActionListReply +// schema: +// "$ref": "#/definitions/GetOperationActionListReply" +// default: +// description: GenericResp +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) GetOperationActionList(c echo.Context) error { + reply := getOperationActionListReply(c.Request().Context()) + return NewOkRespWithReply(c, reply) +} + +// swagger:operation GET /v1/dms/operation_records/operation_user_names OperationRecord GetOperationUserNameList +// +// Get operation user name list. +// +// --- +// parameters: +// - name: filter_operate_project_name +// in: query +// type: string +// responses: +// +// '200': +// description: GetOperationUserNameListReply +// schema: +// "$ref": "#/definitions/GetOperationUserNameListReply" +// default: +// description: GenericResp +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) GetOperationUserNameList(c echo.Context) error { + req := new(aV1.GetOperationUserNameListReq) + err := bindAndValidateReq(c, req) + if nil != err { + return NewErrResp(c, err, apiError.BadRequestErr) + } + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + reply, err := ctl.DMS.GetOperationUserNameList(c.Request().Context(), currentUserUid, req.FilterOperateProjectName) + if nil != err { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, reply) +} + // swagger:operation GET /v1/dms/operation_records/exports OperationRecord ExportOperationRecordList // // Export operation record list. diff --git a/internal/apiserver/service/operation_record_metadata_ce.go b/internal/apiserver/service/operation_record_metadata_ce.go new file mode 100644 index 00000000..f51ec1e6 --- /dev/null +++ b/internal/apiserver/service/operation_record_metadata_ce.go @@ -0,0 +1,17 @@ +//go:build !enterprise + +package service + +import ( + "context" + + aV1 "github.com/actiontech/dms/api/dms/service/v1" +) + +func getOperationTypeNameListReply(context.Context) *aV1.GetOperationTypeNameListReply { + return &aV1.GetOperationTypeNameListReply{Data: []aV1.OperationTypeNameListItem{}} +} + +func getOperationActionListReply(context.Context) *aV1.GetOperationActionListReply { + return &aV1.GetOperationActionListReply{Data: []aV1.OperationActionListItem{}} +} diff --git a/internal/apiserver/service/router.go b/internal/apiserver/service/router.go index 4d85a029..31894384 100644 --- a/internal/apiserver/service/router.go +++ b/internal/apiserver/service/router.go @@ -1,22 +1,24 @@ package service import ( + "bytes" + "compress/gzip" "fmt" + "io" "net/http" "strings" dmsMiddleware "github.com/actiontech/dms/internal/apiserver/middleware" "github.com/actiontech/dms/internal/dms/biz" - dmsService "github.com/actiontech/dms/internal/dms/service" - "github.com/actiontech/dms/internal/dms/storage" "github.com/actiontech/dms/internal/pkg/locale" - "github.com/actiontech/dms/internal/pkg/utils" sqlWorkbenchService "github.com/actiontech/dms/internal/sql_workbench/service" dmsV1 "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" dmsV2 "github.com/actiontech/dms/pkg/dms-common/api/dms/v2" "github.com/actiontech/dms/pkg/dms-common/api/jwt" "github.com/actiontech/dms/pkg/dms-common/i18nPkg" commonLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" + pkgLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" + "github.com/go-kratos/kratos/v2/log" echojwt "github.com/labstack/echo-jwt/v4" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" @@ -26,9 +28,6 @@ func (s *APIServer) initRouter() error { s.echo.GET("/swagger/*", s.DMSController.SwaggerHandler, SwaggerMiddleWare) v1 := s.echo.Group(dmsV1.CurrentGroupVersion) - if err := s.initRouterDMS(v1); err != nil { - return err - } v2 := s.echo.Group(dmsV2.CurrentGroupVersion) // DMS RESTful resource { @@ -126,7 +125,6 @@ func (s *APIServer) initRouter() error { memberGroupV1 := v1.Group("/dms/projects/:project_uid/member_groups") memberGroupV1.GET("", s.DMSController.ListMemberGroups) - memberGroupV1.GET("/tips", s.DMSController.ListMemberGroupTips) memberGroupV1.GET("/:member_group_uid", s.DMSController.GetMemberGroup) memberGroupV1.POST("", s.DMSController.AddMemberGroup) memberGroupV1.PUT("/:member_group_uid", s.DMSController.UpdateMemberGroup) @@ -225,7 +223,6 @@ func (s *APIServer) initRouter() error { dataExportWorkflowsV1.POST("/:data_export_workflow_uid/approve", s.DMSController.ApproveDataExportWorkflow) dataExportWorkflowsV1.POST("/:data_export_workflow_uid/reject", s.DMSController.RejectDataExportWorkflow) dataExportWorkflowsV1.POST("/:data_export_workflow_uid/export", s.DMSController.ExportDataExportWorkflow) - dataExportWorkflowsV1.GET("/:data_export_workflow_uid/original-export/download", s.DMSController.DownloadOriginalDataExportWorkflow) dataExportWorkflowsV1.POST("/cancel", s.DMSController.CancelDataExportWorkflow) // 内部接口,仅允许sys用户访问 @@ -250,8 +247,14 @@ func (s *APIServer) initRouter() error { operationRecordV1 := v1.Group("/dms/operation_records") operationRecordV1.POST("", s.DMSController.AddOperationRecord) operationRecordV1.GET("", s.DMSController.GetOperationRecordList) + operationRecordV1.GET("/operation_type_names", s.DMSController.GetOperationTypeNameList) + operationRecordV1.GET("/operation_actions", s.DMSController.GetOperationActionList) + operationRecordV1.GET("/operation_user_names", s.DMSController.GetOperationUserNameList) operationRecordV1.GET("/exports", s.DMSController.ExportOperationRecordList) + maskingV1 := v1.Group("/dms/masking") + maskingV1.GET("/rules", s.DMSController.ListMaskingRules) + gatewayV1 := v1.Group("/dms/gateways") gatewayV1.POST("", s.DMSController.AddGateway) @@ -272,7 +275,6 @@ func (s *APIServer) initRouter() error { cloudbeaverV1.Use(s.SqlWorkbenchController.CloudbeaverService.CloudbeaverUsecase.Login()) cloudbeaverV1.Use(s.SqlWorkbenchController.CloudbeaverService.CloudbeaverUsecase.GraphQLDistributor()) cloudbeaverV1.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ - Skipper: middleware.DefaultSkipper, Balancer: middleware.NewRandomBalancer(targets), })) } @@ -286,21 +288,6 @@ func (s *APIServer) initRouter() error { sqlWorkbenchV1.Use(s.SqlWorkbenchController.SqlWorkbenchService.Login()) - // 添加数据脱敏中间件 - sqlWorkbenchV1.Use(sqlWorkbenchService.GetDataMaskingMiddleware(sqlWorkbenchService.DataMaskingMiddlewareConfig{ - SqlResultMasker: s.SqlWorkbenchController.SqlWorkbenchService.GetSqlResultMasker(), - DBServiceUsecase: s.DMSController.DMS.DBServiceUsecase, - SqlWorkbenchService: s.SqlWorkbenchController.SqlWorkbenchService, - UnmaskingWorkflowUsecase: s.DMSController.DMS.UnmaskingWorkflowUsecase, - })) - - // 添加查看原文 SQL 替换中间件 - sqlWorkbenchV1.Use(sqlWorkbenchService.GetUnmaskingWorkflowMiddleware(sqlWorkbenchService.DataMaskingMiddlewareConfig{ - DBServiceUsecase: s.DMSController.DMS.DBServiceUsecase, - SqlWorkbenchService: s.SqlWorkbenchController.SqlWorkbenchService, - UnmaskingWorkflowUsecase: s.DMSController.DMS.UnmaskingWorkflowUsecase, - })) - // 添加操作日志记录中间件 sqlWorkbenchV1.Use(sqlWorkbenchService.GetOperationLogBodyDumpMiddleware(sqlWorkbenchService.OperationLogMiddlewareConfig{ CbOperationLogUsecase: s.DMSController.DMS.CbOperationLogUsecase, @@ -308,7 +295,6 @@ func (s *APIServer) initRouter() error { SqlWorkbenchService: s.SqlWorkbenchController.SqlWorkbenchService, })) - sqlWorkbenchV1.Use(s.SqlWorkbenchController.SqlWorkbenchService.AuditMiddleware()) sqlWorkbenchV1.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ Skipper: middleware.DefaultSkipper, Balancer: middleware.NewRandomBalancer(targets), @@ -348,13 +334,24 @@ func SwaggerMiddleWare(next echo.HandlerFunc) echo.HandlerFunc { } } +// 检查 reply 是否是 Gzip 数据 +func isGzip(data []byte) bool { + return len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b +} + // 解码 Gzip 数据 func decodeGzip(data []byte) string { - gzipBytes, err := utils.DecodeGzipBytes(data) + reader, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { return fmt.Sprintf("Gzip decode error: %v", err) } - return string(gzipBytes) + defer reader.Close() + + decoded, err := io.ReadAll(reader) + if err != nil { + return fmt.Sprintf("Read Gzip data error: %v", err) + } + return string(decoded) } func (s *APIServer) installMiddleware() error { @@ -375,36 +372,25 @@ func (s *APIServer) installMiddleware() error { } }(allowedMethods)) - var skipJWTPaths = []string{ - dmsV1.SessionRouterGroup, - "/v1/dms/sessions/refresh", - "/v1/dms/oauth2", - "/v1/dms/configurations/login/tips", - "/v1/dms/personalization/logo", - "/v1/dms/configurations/license", - "/v1/dms/users/verify_user_login", - "/v1/dms/configurations/sms/send_code", - "/v1/dms/configurations/sms/verify_code", - "/v1/dms/basic_info", - } - var notSkipJWTPaths = []string{ - sqlWorkbenchService.SQL_WORKBENCH_URL, - } s.echo.Use(dmsMiddleware.JWTTokenAdapter(), echojwt.WithConfig(echojwt.Config{ Skipper: middleware.Skipper(func(c echo.Context) bool { - uri := c.Request().RequestURI - for _, skipPath := range skipJWTPaths { - if strings.HasSuffix(uri, skipPath) || strings.HasPrefix(uri, skipPath) { - return true - } - } - for _, notSkipPath := range notSkipJWTPaths { - if strings.HasSuffix(uri, notSkipPath) || strings.HasPrefix(uri, notSkipPath) { - return false - } + logger := log.NewHelper(log.With(pkgLog.NewKLogWrapper(s.logger), "middleware", "jwt")) + if strings.HasSuffix(c.Request().RequestURI, dmsV1.SessionRouterGroup) || + strings.HasPrefix(c.Request().RequestURI, "/v1/dms/sessions/refresh" /* TODO 使用统一方法skip */) || + strings.HasPrefix(c.Request().RequestURI, "/v1/dms/oauth2" /* TODO 使用统一方法skip */) || + strings.HasPrefix(c.Request().RequestURI, "/v1/dms/configurations/login/tips" /* TODO 使用统一方法skip */) || + strings.HasPrefix(c.Request().RequestURI, "/v1/dms/personalization/logo") || + strings.HasPrefix(c.Request().RequestURI, "/v1/dms/configurations/license" /* TODO 使用统一方法skip */) || + strings.HasPrefix(c.Request().RequestURI, "/v1/dms/users/verify_user_login" /* TODO 使用统一方法skip */) || + strings.HasPrefix(c.Request().RequestURI, "/v1/dms/configurations/sms/send_code" /* TODO 使用统一方法skip */) || + strings.HasPrefix(c.Request().RequestURI, "/v1/dms/configurations/sms/verify_code" /* TODO 使用统一方法skip */) || + !(strings.HasPrefix(c.Request().RequestURI, dmsV1.CurrentGroupVersion) || + strings.HasPrefix(c.Request().RequestURI, dmsV2.CurrentGroupVersion)) && + !strings.HasPrefix(c.Request().RequestURI, sqlWorkbenchService.SQL_WORKBENCH_URL) { + logger.Debugf("skipper url jwt check: %v", c.Request().RequestURI) + return true } - // Non-DMS component's own uri - return !(strings.HasPrefix(uri, dmsV1.CurrentGroupVersion) || strings.HasPrefix(uri, dmsV2.CurrentGroupVersion)) + return false }), SigningKey: dmsV1.JwtSigningKey, TokenLookup: "cookie:dms-token,header:Authorization:Bearer ", // tell the middleware where to get token: from cookie and header, @@ -419,6 +405,14 @@ func (s *APIServer) installMiddleware() error { // Middleware s.echo.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{ Skipper: func(c echo.Context) bool { + // 跳过WebSocket请求,避免干扰连接升级 + if c.IsWebSocket() { + return true + } + // 如果未启用请求dump,跳过所有请求 + if !s.opts.ServiceOpts.Log.EnableRequestDump { + return true + } return !strings.HasPrefix(c.Request().RequestURI, dmsV1.GroupV1) }, Handler: func(context echo.Context, req []byte, reply []byte) { @@ -428,7 +422,7 @@ func (s *APIServer) installMiddleware() error { reqStr := string(req) // 尝试解码 reply(gzip 格式) var replyStr string - if utils.IsGzip(reply) { + if isGzip(reply) { replyStr = decodeGzip(reply) } else { replyStr = string(reply) @@ -445,19 +439,19 @@ func (s *APIServer) installMiddleware() error { })) s.echo.Use(middleware.GzipWithConfig(middleware.GzipConfig{ - Skipper: middleware.DefaultSkipper, - Level: 5, + Skipper: func(c echo.Context) bool { + // 跳过CloudBeaver路径,让CloudBeaver路由组自己处理压缩 + if s.SqlWorkbenchController != nil && + strings.HasPrefix(c.Request().URL.Path, s.SqlWorkbenchController.CloudbeaverService.CloudbeaverUsecase.GetRootUri()) { + return true + } + return false + }, + Level: 5, })) s.echo.Use(middleware.StaticWithConfig(middleware.StaticConfig{ Skipper: middleware.Skipper(func(c echo.Context) bool { - // 必须先跳过 /odc_query,避免 DMS 自身的 static fallback 把 - // `/odc_query/`、`/odc_query/index.html` 等子路径返回为 DMS index.html, - // 导致 ODC SQL 工作台跳转被截获。无尾斜杠的 /odc_query 由 Group route - // 直接走 ProxyConfig 代理到 ODC 8989,本 Skipper 不影响。 - if strings.HasPrefix(c.Request().URL.Path, s.SqlWorkbenchController.SqlWorkbenchService.GetRootUri()) { - return true - } if strings.HasPrefix(c.Request().URL.Path, s.SqlWorkbenchController.CloudbeaverService.CloudbeaverUsecase.GetRootUri()) { return true } @@ -497,7 +491,7 @@ func (s *APIServer) installMiddleware() error { } func (s *APIServer) installController() error { - sqlWorkbenchController, err := NewSqlWorkbenchController(s.logger, s.opts) + sqlWorkbenchController, err := NewSqlWorkbenchController(s.logger, s.opts, s.configFilePath) if nil != err { return fmt.Errorf("failed to create SqlWorkbenchController: %v", err) } @@ -510,25 +504,6 @@ func (s *APIServer) installController() error { s.DMSController = DMSController s.SqlWorkbenchController = sqlWorkbenchController - // 初始化 SQL Workbench 脱敏组件,与 DMS 共用同一套存储配置 - st, err := storage.NewStorage(s.logger, &storage.StorageConfig{ - User: s.opts.ServiceOpts.Database.UserName, - Password: s.opts.ServiceOpts.Database.Password, - Host: s.opts.ServiceOpts.Database.Host, - Port: s.opts.ServiceOpts.Database.Port, - Schema: s.opts.ServiceOpts.Database.Database, - Debug: s.opts.ServiceOpts.Database.Debug, - AutoMigrate: s.opts.ServiceOpts.Database.AutoMigrate, - }) - if err != nil { - return fmt.Errorf("failed to initialize storage for masker: %v", err) - } - masker, err := dmsService.NewSQLWorkbenchSQLResultMasker(s.logger, st) - if err != nil { - return fmt.Errorf("failed to create sql result masker: %v", err) - } - s.SqlWorkbenchController.SqlWorkbenchService.SetSqlResultMasker(masker) - // s.AuthController.RegisterPlugin(s.DMSController.GetRegisterPluginFn()) return nil } diff --git a/internal/dms/biz/operation_record.go b/internal/dms/biz/operation_record.go index 289f9df2..a1279e8c 100644 --- a/internal/dms/biz/operation_record.go +++ b/internal/dms/biz/operation_record.go @@ -9,10 +9,16 @@ import ( utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" ) +type OperationRecordUserNameItem struct { + OperationUserName string + OperationReqIP string +} + type OperationRecordRepo interface { SaveOperationRecord(ctx context.Context, record *OperationRecord) error ListOperationRecords(ctx context.Context, opt *ListOperationRecordOption) ([]*OperationRecord, uint64, error) ExportOperationRecords(ctx context.Context, opt *ListOperationRecordOption) ([]*OperationRecord, error) + ListDistinctOperationUserNames(ctx context.Context, opt *ListOperationRecordOption) ([]*OperationRecordUserNameItem, error) CleanOperationRecordOpTimeBefore(ctx context.Context, t time.Time) (rowsAffected int64, err error) } @@ -36,8 +42,11 @@ type ListOperationRecordOption struct { FilterOperateTimeTo string FilterOperateProjectName *string FuzzySearchOperateUserName string - FilterOperateTypeName string - FilterOperateAction string + FuzzySearchOperateContent string + FilterFuzzyOperateUserName string + FilterOperateTypeNames []string + FilterOperateActions []string + FilterOperateStatus string // 权限相关字段 CanViewGlobal bool // 是否有全局查看权限(admin/sys/全局权限) AccessibleProjectNames []string // 可访问的项目名称列表(项目管理员) diff --git a/internal/dms/service/operation_record_ce.go b/internal/dms/service/operation_record_ce.go index 2bbe50e2..06d3517a 100644 --- a/internal/dms/service/operation_record_ce.go +++ b/internal/dms/service/operation_record_ce.go @@ -26,3 +26,7 @@ func (d *DMSService) GetOperationRecordList(ctx context.Context, req *aV1.GetOpe func (d *DMSService) ExportOperationRecordList(ctx context.Context, req *aV1.ExportOperationRecordListReq, currentUserUid string) (reply []byte, err error) { return nil, errNotSupportOperationRecord } + +func (d *DMSService) GetOperationUserNameList(ctx context.Context, currentUserUid string, filterOperateProjectName *string) (reply *aV1.GetOperationUserNameListReply, err error) { + return nil, errNotSupportOperationRecord +} diff --git a/internal/dms/storage/operation_record.go b/internal/dms/storage/operation_record.go index 90a7ae96..5ed4673e 100644 --- a/internal/dms/storage/operation_record.go +++ b/internal/dms/storage/operation_record.go @@ -13,6 +13,9 @@ import ( var _ biz.OperationRecordRepo = (*operationRecordRepo)(nil) +// 内置系统账号产生的操作记录在列表/导出中不展示 +const operationRecordUserNameSys = "sys" + type operationRecordRepo struct { *Storage log *utilLog.Helper @@ -37,32 +40,56 @@ func (d *operationRecordRepo) SaveOperationRecord(ctx context.Context, record *b return nil } -func applyOperationRecordFilters(db *gorm.DB, opt *biz.ListOperationRecordOption) *gorm.DB { +func applyOperationRecordBaseFilters(db *gorm.DB, opt *biz.ListOperationRecordOption) *gorm.DB { + db = db.Where("operation_user_name <> ?", operationRecordUserNameSys) if opt.FilterOperateTimeFrom != "" { db = db.Where("operation_time > ?", opt.FilterOperateTimeFrom) } if opt.FilterOperateTimeTo != "" { db = db.Where("operation_time < ?", opt.FilterOperateTimeTo) } - // 项目过滤:如果指定了项目,已经通过权限校验,直接使用该过滤条件 if opt.FilterOperateProjectName != nil { db = db.Where("operation_project_name = ?", *opt.FilterOperateProjectName) } else { - // 如果没指定项目,根据权限过滤 if !opt.CanViewGlobal && len(opt.AccessibleProjectNames) > 0 { - // 项目管理员只能查看对应项目下的操作记录 db = db.Where("operation_project_name IN ?", opt.AccessibleProjectNames) } - // 如果 CanViewGlobal 为 true,不添加项目过滤(可以查看所有项目,包括空字符串) } - if opt.FuzzySearchOperateUserName != "" { - db = db.Where("operation_user_name LIKE ?", "%"+opt.FuzzySearchOperateUserName+"%") + return db +} + +func applyOperationRecordFilters(db *gorm.DB, opt *biz.ListOperationRecordOption) *gorm.DB { + db = applyOperationRecordBaseFilters(db, opt) + + if opt.FuzzySearchOperateUserName != "" && + opt.FuzzySearchOperateUserName == opt.FuzzySearchOperateContent { + kw := "%" + opt.FuzzySearchOperateUserName + "%" + db = db.Where( + "(operation_user_name LIKE ? OR operation_req_ip LIKE ? OR operation_i18n_content LIKE ?)", + kw, kw, kw, + ) + } else { + if opt.FuzzySearchOperateUserName != "" { + kw := "%" + opt.FuzzySearchOperateUserName + "%" + db = db.Where("(operation_user_name LIKE ? OR operation_req_ip LIKE ?)", kw, kw) + } + if opt.FuzzySearchOperateContent != "" { + db = db.Where("operation_i18n_content LIKE ?", "%"+opt.FuzzySearchOperateContent+"%") + } + } + + if opt.FilterFuzzyOperateUserName != "" { + kw := "%" + opt.FilterFuzzyOperateUserName + "%" + db = db.Where("(operation_user_name LIKE ? OR operation_req_ip LIKE ?)", kw, kw) + } + if len(opt.FilterOperateTypeNames) > 0 { + db = db.Where("operation_type_name IN ?", opt.FilterOperateTypeNames) } - if opt.FilterOperateTypeName != "" { - db = db.Where("operation_type_name = ?", opt.FilterOperateTypeName) + if len(opt.FilterOperateActions) > 0 { + db = db.Where("operation_action IN ?", opt.FilterOperateActions) } - if opt.FilterOperateAction != "" { - db = db.Where("operation_action = ?", opt.FilterOperateAction) + if opt.FilterOperateStatus != "" { + db = db.Where("operation_status = ?", opt.FilterOperateStatus) } return db } @@ -137,6 +164,31 @@ func (d *operationRecordRepo) ExportOperationRecords(ctx context.Context, opt *b return ret, nil } +func (d *operationRecordRepo) ListDistinctOperationUserNames(ctx context.Context, opt *biz.ListOperationRecordOption) ([]*biz.OperationRecordUserNameItem, error) { + var userNames []string + + if err := transaction(d.log, ctx, d.db, func(tx *gorm.DB) error { + db := tx.WithContext(ctx).Model(&model.OperationRecord{}) + db = applyOperationRecordBaseFilters(db, opt) + if err := db.Select("DISTINCT operation_user_name"). + Order("operation_user_name ASC"). + Find(&userNames).Error; err != nil { + return fmt.Errorf("failed to list distinct operation user names: %v", err) + } + return nil + }); err != nil { + return nil, err + } + + ret := make([]*biz.OperationRecordUserNameItem, 0, len(userNames)) + for _, userName := range userNames { + ret = append(ret, &biz.OperationRecordUserNameItem{ + OperationUserName: userName, + }) + } + return ret, nil +} + func (d *operationRecordRepo) CleanOperationRecordOpTimeBefore(ctx context.Context, t time.Time) (rowsAffected int64, err error) { err = transaction(d.log, ctx, d.db, func(tx *gorm.DB) error { result := tx.WithContext(ctx).Unscoped().Delete(&model.OperationRecord{}, "operation_time < ?", t)