diff --git a/api/dms/service/v1/privilege_apply_workflow.go b/api/dms/service/v1/privilege_apply_workflow.go new file mode 100644 index 00000000..80f24810 --- /dev/null +++ b/api/dms/service/v1/privilege_apply_workflow.go @@ -0,0 +1,176 @@ +package v1 + +import ( + privilegeApplyBiz "github.com/actiontech/dms/internal/privilege_apply/biz" + base "github.com/actiontech/dms/pkg/dms-common/api/base/v1" +) + +// swagger:parameters GetPrivilegeApplyAssignees +type GetPrivilegeApplyAssigneesReq struct { + // project id + // Required: true + // in: path + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // datasource uid + // Required: true + // in: query + DBServiceUid string `query:"db_service_uid" json:"db_service_uid" validate:"required"` +} + +// swagger:model GetPrivilegeApplyAssigneesReply +type GetPrivilegeApplyAssigneesReply struct { + Data *GetPrivilegeApplyAssigneesReplyData `json:"data"` + base.GenericResp +} + +// swagger:model GetPrivilegeApplyAssigneesReplyData +type GetPrivilegeApplyAssigneesReplyData struct { + HasAssignee bool `json:"has_assignee"` + Assignees []*UidWithName `json:"assignees"` +} + +// swagger:model CreatePrivilegeApplyWorkflowReq +type CreatePrivilegeApplyWorkflowReq struct { + // swagger:ignore + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + // in: body + // Required: true + PrivilegeApplyWorkflow *CreatePrivilegeApplyWorkflow `json:"privilege_apply_workflow" validate:"required"` +} + +// swagger:model CreatePrivilegeApplyWorkflow +type CreatePrivilegeApplyWorkflow struct { + DBServiceUid string `json:"db_service_uid" validate:"required"` + SourceDBAccountUid string `json:"source_db_account_uid" validate:"required"` + RawSQL string `json:"raw_sql" validate:"required"` + ErrorMessage string `json:"error_message" validate:"required"` + RequestedObjects []privilegeApplyBiz.PrivilegeObject `json:"requested_objects"` + RequestedActions []string `json:"requested_actions"` + ApplyReason string `json:"apply_reason" validate:"required"` + ExpectedExpireDays *int64 `json:"expected_expire_days"` +} + +// swagger:model CreatePrivilegeApplyWorkflowReply +type CreatePrivilegeApplyWorkflowReply struct { + Data *CreatePrivilegeApplyWorkflowReplyData `json:"data"` + base.GenericResp +} + +// swagger:model CreatePrivilegeApplyWorkflowReplyData +type CreatePrivilegeApplyWorkflowReplyData struct { + WorkflowID string `json:"workflow_id"` +} + +// swagger:parameters GetPrivilegeApplyWorkflow +type GetPrivilegeApplyWorkflowReq 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 GetPrivilegeApplyWorkflowReply +type GetPrivilegeApplyWorkflowReply struct { + Data *PrivilegeApplyWorkflowDetail `json:"data"` + base.GenericResp +} + +// swagger:model PrivilegeApplyWorkflowDetail +type PrivilegeApplyWorkflowDetail struct { + WorkflowID string `json:"workflow_id"` + ApprovalStatus privilegeApplyBiz.PrivilegeApplyWorkflowApprovalStatus `json:"approval_status"` + ReissueStatus privilegeApplyBiz.PrivilegeApplyReissueStatus `json:"reissue_status"` + ApplyReason string `json:"apply_reason"` + DBServiceUid string `json:"db_service_uid"` + DBServiceName string `json:"db_service_name"` + SourceDBAccountUid string `json:"source_db_account_uid"` + SourceDBAccountName string `json:"source_db_account_name"` + ApplicantUid string `json:"applicant_uid"` + ApplicantName string `json:"applicant_name"` + RawSQL string `json:"raw_sql"` + ErrorMessage string `json:"error_message"` + RequestedObjects []privilegeApplyBiz.PrivilegeObject `json:"requested_objects"` + RequestedActions []string `json:"requested_actions"` + ApprovedObjects []privilegeApplyBiz.PrivilegeObject `json:"approved_objects"` + ApprovedActions []string `json:"approved_actions"` + ExpectedExpireDays *int64 `json:"expected_expire_days"` + RejectReason string `json:"reject_reason"` + ReissueError string `json:"reissue_error"` + TargetDBAccountUid string `json:"target_db_account_uid"` + TargetDBAccountName string `json:"target_db_account_name"` + CreatedAt string `json:"created_at" example:"2024-01-15T10:30:00Z"` + CurrentAssignees []*UidWithName `json:"current_assignees"` + ImpactPreview *privilegeApplyBiz.PrivilegeApplyImpactPreview `json:"impact_preview"` +} + +// swagger:parameters ListPrivilegeApplyWorkflows +type ListPrivilegeApplyWorkflowsReq struct { + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + PageSize uint32 `query:"page_size" json:"page_size" validate:"required"` + PageIndex uint32 `query:"page_index" json:"page_index"` + FilterByTab string `query:"filter_by_tab" json:"filter_by_tab" validate:"required,oneof=pending handled"` + FilterByDBServiceUid string `query:"filter_by_db_service_uid" json:"filter_by_db_service_uid"` +} + +// swagger:model ListPrivilegeApplyWorkflowsReply +type ListPrivilegeApplyWorkflowsReply struct { + Data []*PrivilegeApplyWorkflowListItem `json:"data"` + Total int64 `json:"total_nums"` + base.GenericResp +} + +// swagger:model PrivilegeApplyWorkflowListItem +type PrivilegeApplyWorkflowListItem struct { + WorkflowID string `json:"workflow_id"` + ApplicantUid string `json:"applicant_uid"` + ApplicantName string `json:"applicant_name"` + DBServiceUid string `json:"db_service_uid"` + DBServiceName string `json:"db_service_name"` + SourceDBAccountName string `json:"source_db_account_name"` + ApplyReason string `json:"apply_reason"` + CreatedAt string `json:"created_at"` + ApprovalStatus privilegeApplyBiz.PrivilegeApplyWorkflowApprovalStatus `json:"approval_status"` + ReissueStatus privilegeApplyBiz.PrivilegeApplyReissueStatus `json:"reissue_status"` + CurrentAssignees []*UidWithName `json:"current_assignees"` +} + +// swagger:parameters ApprovePrivilegeApplyWorkflow +type ApprovePrivilegeApplyWorkflowReq struct { + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + WorkflowID string `param:"workflow_id" json:"workflow_id" validate:"required"` + ApprovePrivilegeApplyWorkflow *ApprovePrivilegeApplyWorkflow `json:"approve_privilege_apply_workflow"` +} + +type ApprovePrivilegeApplyWorkflow struct { + ApproveReason string `json:"approve_reason"` + ApprovedPermissions *privilegeApplyBiz.ApprovedPermissions `json:"approved_permissions"` + ExpectedExpireDays *int64 `json:"expected_expire_days"` +} + +type ApprovePrivilegeApplyWorkflowReply struct { + base.GenericResp +} + +// swagger:parameters RejectPrivilegeApplyWorkflow +type RejectPrivilegeApplyWorkflowReq struct { + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + WorkflowID string `param:"workflow_id" json:"workflow_id" validate:"required"` + RejectReason string `json:"reject_reason" validate:"required"` +} + +type RejectPrivilegeApplyWorkflowReply struct { + base.GenericResp +} + +// swagger:parameters RetryReissuePrivilegeApplyWorkflow +type RetryReissuePrivilegeApplyWorkflowReq struct { + ProjectUid string `param:"project_uid" json:"project_uid" validate:"required"` + WorkflowID string `param:"workflow_id" json:"workflow_id" validate:"required"` +} + +type RetryReissuePrivilegeApplyWorkflowReply struct { + base.GenericResp +} diff --git a/internal/apiserver/service/privilege_apply_controller.go b/internal/apiserver/service/privilege_apply_controller.go new file mode 100644 index 00000000..aa620440 --- /dev/null +++ b/internal/apiserver/service/privilege_apply_controller.go @@ -0,0 +1,186 @@ +package service + +import ( + aV1 "github.com/actiontech/dms/api/dms/service/v1" + apiError "github.com/actiontech/dms/internal/apiserver/pkg/error" + "github.com/actiontech/dms/pkg/dms-common/api/jwt" + "github.com/labstack/echo/v4" +) + +// swagger:operation GET /v1/dms/projects/{project_uid}/privilege-apply-workflows/assignees PrivilegeApply GetPrivilegeApplyAssignees +// +// 预检提权申请审批人。 +// +// --- +// parameters: +// - name: project_uid +// in: path +// required: true +// type: string +// - name: db_service_uid +// in: query +// required: true +// type: string +// +// responses: +// '200': +// description: Get privilege apply assignees successfully +// schema: +// "$ref": "#/definitions/GetPrivilegeApplyAssigneesReply" +// default: +// description: Generic error response +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) GetPrivilegeApplyAssignees(c echo.Context) error { + req := &aV1.GetPrivilegeApplyAssigneesReq{} + if err := bindAndValidateReq(c, req); err != nil { + return NewErrResp(c, err, apiError.BadRequestErr) + } + reply, err := ctl.DMS.GetPrivilegeApplyAssignees(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}/privilege-apply-workflows PrivilegeApply CreatePrivilegeApplyWorkflow +// +// 创建提权申请。 +// +// --- +// parameters: +// - name: project_uid +// in: path +// required: true +// type: string +// - name: privilege_apply_workflow +// in: body +// required: true +// schema: +// "$ref": "#/definitions/CreatePrivilegeApplyWorkflowReq" +// +// responses: +// '200': +// description: Create privilege apply workflow successfully +// schema: +// "$ref": "#/definitions/CreatePrivilegeApplyWorkflowReply" +// default: +// description: Generic error response +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) CreatePrivilegeApplyWorkflow(c echo.Context) error { + req := &aV1.CreatePrivilegeApplyWorkflowReq{} + 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.CreatePrivilegeApplyWorkflow(c.Request().Context(), req, currentUserUID) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, reply) +} + +// swagger:operation GET /v1/dms/projects/{project_uid}/privilege-apply-workflows/{workflow_id} PrivilegeApply GetPrivilegeApplyWorkflow +// +// 查询提权申请详情(申请人只读)。 +// +// --- +// parameters: +// - name: project_uid +// in: path +// required: true +// type: string +// - name: workflow_id +// in: path +// required: true +// type: string +// +// responses: +// '200': +// description: Get privilege apply workflow successfully +// schema: +// "$ref": "#/definitions/GetPrivilegeApplyWorkflowReply" +// default: +// description: Generic error response +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) GetPrivilegeApplyWorkflow(c echo.Context) error { + req := &aV1.GetPrivilegeApplyWorkflowReq{} + 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.GetPrivilegeApplyWorkflow(c.Request().Context(), req, currentUserUID) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, reply) +} + +func (ctl *DMSController) ListPrivilegeApplyWorkflows(c echo.Context) error { + req := &aV1.ListPrivilegeApplyWorkflowsReq{} + 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.ListPrivilegeApplyWorkflows(c.Request().Context(), req, currentUserUID) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, reply) +} + +func (ctl *DMSController) ApprovePrivilegeApplyWorkflow(c echo.Context) error { + req := &aV1.ApprovePrivilegeApplyWorkflowReq{} + 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) + } + if err := ctl.DMS.ApprovePrivilegeApplyWorkflow(c.Request().Context(), req, currentUserUID); err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, &aV1.ApprovePrivilegeApplyWorkflowReply{}) +} + +func (ctl *DMSController) RejectPrivilegeApplyWorkflow(c echo.Context) error { + req := &aV1.RejectPrivilegeApplyWorkflowReq{} + 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) + } + if err := ctl.DMS.RejectPrivilegeApplyWorkflow(c.Request().Context(), req, currentUserUID); err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, &aV1.RejectPrivilegeApplyWorkflowReply{}) +} + +func (ctl *DMSController) RetryReissuePrivilegeApplyWorkflow(c echo.Context) error { + req := &aV1.RetryReissuePrivilegeApplyWorkflowReq{} + 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) + } + if err := ctl.DMS.RetryReissuePrivilegeApplyWorkflow(c.Request().Context(), req, currentUserUID); err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, &aV1.RetryReissuePrivilegeApplyWorkflowReply{}) +} diff --git a/internal/apiserver/service/router.go b/internal/apiserver/service/router.go index 263c9392..508abf77 100644 --- a/internal/apiserver/service/router.go +++ b/internal/apiserver/service/router.go @@ -329,6 +329,10 @@ func (s *APIServer) initRouter() error { SqlWorkbenchService: s.SqlWorkbenchController.SqlWorkbenchService, })) + sqlWorkbenchV1.Use(sqlWorkbenchService.GetPrivilegeDeniedMiddleware(sqlWorkbenchService.PrivilegeDeniedMiddlewareConfig{ + SqlWorkbenchService: s.SqlWorkbenchController.SqlWorkbenchService, + })) + sqlWorkbenchV1.Use(s.SqlWorkbenchController.SqlWorkbenchService.AuditMiddleware()) sqlWorkbenchV1.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ Skipper: middleware.DefaultSkipper, diff --git a/internal/dms/biz/op_permission.go b/internal/dms/biz/op_permission.go index 04ee7fd0..9df5050d 100644 --- a/internal/dms/biz/op_permission.go +++ b/internal/dms/biz/op_permission.go @@ -330,6 +330,13 @@ func initOpPermission() []*OpPermission { Module: DesensitizationRule, Service: v1.ServiceDMS, }, + { + UID: pkgConst.UIdOfOpPermissionPrivilegeApplyAudit, + Name: "提权审批", + RangeType: OpRangeTypeProject, + Module: AccountManagement, + Service: v1.ServiceDMS, + }, } } diff --git a/internal/dms/biz/op_permission_init_test.go b/internal/dms/biz/op_permission_init_test.go new file mode 100644 index 00000000..ed0d07bb --- /dev/null +++ b/internal/dms/biz/op_permission_init_test.go @@ -0,0 +1,123 @@ +package biz + +import ( + "context" + "io" + "testing" + + pkgConst "github.com/actiontech/dms/internal/dms/pkg/constant" + pkgErr "github.com/actiontech/dms/internal/dms/pkg/errors" + utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" + "github.com/stretchr/testify/assert" +) + +// fakeOpPermissionRepo 表驱动 InitOpPermissions:已存在 skip / 缺失插入。 +type fakeOpPermissionRepo struct { + byUID map[string]*OpPermission + saved []string +} + +func newFakeOpPermissionRepo(existing ...*OpPermission) *fakeOpPermissionRepo { + m := make(map[string]*OpPermission) + for _, op := range existing { + cp := *op + m[op.UID] = &cp + } + return &fakeOpPermissionRepo{byUID: m} +} + +func (f *fakeOpPermissionRepo) SaveOpPermission(_ context.Context, op *OpPermission) error { + cp := *op + f.byUID[op.UID] = &cp + f.saved = append(f.saved, op.UID) + return nil +} +func (f *fakeOpPermissionRepo) UpdateOpPermission(context.Context, *OpPermission) error { return nil } +func (f *fakeOpPermissionRepo) CheckOpPermissionExist(context.Context, []string) (bool, error) { + return true, nil +} +func (f *fakeOpPermissionRepo) ListOpPermissions(context.Context, *ListOpPermissionsOption) ([]*OpPermission, int64, error) { + return nil, 0, nil +} +func (f *fakeOpPermissionRepo) DelOpPermission(context.Context, string) error { return nil } +func (f *fakeOpPermissionRepo) GetOpPermission(_ context.Context, uid string) (*OpPermission, error) { + if op, ok := f.byUID[uid]; ok { + cp := *op + return &cp, nil + } + return nil, pkgErr.ErrStorageNoData +} + +// TestInitOpPermissions_PrivilegeApplyAudit_Idempotent 对应方案 B-privilege_apply_audit_op_permission: +// 已存在 UID skip;缺失则插入;重复 Init 不产生第二行。 +func TestInitOpPermissions_PrivilegeApplyAudit_Idempotent(t *testing.T) { + logger := utilLog.NewMyLogger(io.Discard) + auditSeed := &OpPermission{ + UID: pkgConst.UIdOfOpPermissionPrivilegeApplyAudit, + Name: "提权申请审批", + RangeType: OpRangeTypeDBService, + } + + cases := []struct { + name string + existing []*OpPermission + candidates []*OpPermission + wantSavedUIDs []string + wantPresent []string + }{ + { + name: "missing_privilege_apply_audit_inserts", + existing: nil, + candidates: []*OpPermission{auditSeed}, + wantSavedUIDs: []string{pkgConst.UIdOfOpPermissionPrivilegeApplyAudit}, + wantPresent: []string{pkgConst.UIdOfOpPermissionPrivilegeApplyAudit}, + }, + { + name: "existing_uid_skips_insert", + existing: []*OpPermission{{ + UID: pkgConst.UIdOfOpPermissionPrivilegeApplyAudit, + Name: "提权申请审批", + RangeType: OpRangeTypeDBService, + }}, + candidates: []*OpPermission{auditSeed}, + wantSavedUIDs: nil, + wantPresent: []string{pkgConst.UIdOfOpPermissionPrivilegeApplyAudit}, + }, + { + name: "table_driven_mix_skip_and_insert", + existing: []*OpPermission{{ + UID: pkgConst.UIDOfOpPermissionProjectAdmin, + Name: "项目管理", + RangeType: OpRangeTypeProject, + }}, + candidates: []*OpPermission{ + {UID: pkgConst.UIDOfOpPermissionProjectAdmin, Name: "项目管理", RangeType: OpRangeTypeProject}, + auditSeed, + }, + wantSavedUIDs: []string{pkgConst.UIdOfOpPermissionPrivilegeApplyAudit}, + wantPresent: []string{ + pkgConst.UIDOfOpPermissionProjectAdmin, + pkgConst.UIdOfOpPermissionPrivilegeApplyAudit, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + repo := newFakeOpPermissionRepo(tc.existing...) + uc := NewOpPermissionUsecase(logger, nil, repo, nil) + assert.NoError(t, uc.InitOpPermissions(context.Background(), tc.candidates)) + assert.Equal(t, tc.wantSavedUIDs, repo.saved) + + // 再次 Init:幂等,不再 Save + savedBefore := len(repo.saved) + assert.NoError(t, uc.InitOpPermissions(context.Background(), tc.candidates)) + assert.Equal(t, savedBefore, len(repo.saved), "second Init must not insert again") + + for _, uid := range tc.wantPresent { + _, err := repo.GetOpPermission(context.Background(), uid) + assert.NoError(t, err, "uid %s should exist", uid) + } + }) + } +} diff --git a/internal/dms/biz/prepare.go b/internal/dms/biz/prepare.go index 28811166..d80bad87 100644 --- a/internal/dms/biz/prepare.go +++ b/internal/dms/biz/prepare.go @@ -30,11 +30,11 @@ func EnvPrepare(ctx context.Context, logger utilLog.Logger, return fmt.Errorf("failed to get dms config: %v", err) } - // 如果找到了,则判断是否需要初始化数据对象操作 + // 权限点种子:每次启动幂等补齐缺失项(InitOpPermissions 对已存在 UID 跳过) + if err := opPermissionUsecase.InitOpPermissions(tx, initOpPermission()); nil != err { + return err + } if dmsConfig.NeedInitOpPermissions { - if err := opPermissionUsecase.InitOpPermissions(tx, initOpPermission()); nil != err { - return err - } dmsConfig.NeedInitOpPermissions = false if err := config.UpdateDMSConfig(tx, dmsConfig); nil != err { return fmt.Errorf("failed to update dms config: %v", err) diff --git a/internal/dms/pkg/constant/const.go b/internal/dms/pkg/constant/const.go index ab8340ce..4e8ae6f7 100644 --- a/internal/dms/pkg/constant/const.go +++ b/internal/dms/pkg/constant/const.go @@ -48,6 +48,7 @@ const ( UIdOfOpPermissionDesensitization = "700036" UIDOfOpPermissionViewSQLInsight = "700037" UIdOfOpPermissionMaskingAudit = "700038" + UIdOfOpPermissionPrivilegeApplyAudit = "700039" UIDOfDMSConfig = "700100" @@ -128,6 +129,8 @@ func ConvertPermissionIdToType(opPermissionUid string) (apiOpPermissionTyp dmsCo apiOpPermissionTyp = dmsCommonV1.OpPermissionDesensitization case UIdOfOpPermissionMaskingAudit: apiOpPermissionTyp = dmsCommonV1.OpPermissionMaskingAudit + case UIdOfOpPermissionPrivilegeApplyAudit: + apiOpPermissionTyp = dmsCommonV1.OpPermissionPrivilegeApplyAudit case UIDOfOrdinaryUser: apiOpPermissionTyp = dmsCommonV1.OpPermissionTypeNone default: @@ -207,6 +210,8 @@ func ConvertPermissionTypeToId(opPermissionType dmsCommonV1.OpPermissionType) (p permissionId = UIdOfOpPermissionDesensitization case dmsCommonV1.OpPermissionMaskingAudit: permissionId = UIdOfOpPermissionMaskingAudit + case dmsCommonV1.OpPermissionPrivilegeApplyAudit: + permissionId = UIdOfOpPermissionPrivilegeApplyAudit case dmsCommonV1.OpPermissionTypeNone: permissionId = UIDOfOrdinaryUser default: diff --git a/internal/dms/service/op_permission.go b/internal/dms/service/op_permission.go index 755c178e..1998e1b8 100644 --- a/internal/dms/service/op_permission.go +++ b/internal/dms/service/op_permission.go @@ -51,6 +51,7 @@ var OpPermissionNameByUID = map[string]*i18n.Message{ pkgConst.UIdOfOpPermissionManageRoleMange: locale.NameOpPermissionManageRoleMange, pkgConst.UIdOfOpPermissionDesensitization: locale.NameOpPermissionDesensitization, pkgConst.UIdOfOpPermissionMaskingAudit: locale.NameOpPermissionMaskingAudit, + pkgConst.UIdOfOpPermissionPrivilegeApplyAudit: locale.NameOpPermissionPrivilegeApplyAudit, } var OpPermissionDescByUID = map[string]*i18n.Message{ @@ -91,6 +92,7 @@ var OpPermissionDescByUID = map[string]*i18n.Message{ pkgConst.UIdOfOpPermissionManageRoleMange: locale.DescOpPermissionManageRoleMange, pkgConst.UIdOfOpPermissionDesensitization: locale.DescOpPermissionDesensitization, pkgConst.UIdOfOpPermissionMaskingAudit: locale.DescOpPermissionMaskingAudit, + pkgConst.UIdOfOpPermissionPrivilegeApplyAudit: locale.DescOpPermissionPrivilegeApplyAudit, } func (d *DMSService) ListOpPermissions(ctx context.Context, req *dmsV1.ListOpPermissionReq) (reply *dmsV1.ListOpPermissionReply, err error) { diff --git a/internal/dms/service/privilege_apply_workflow_ce.go b/internal/dms/service/privilege_apply_workflow_ce.go new file mode 100644 index 00000000..a319b754 --- /dev/null +++ b/internal/dms/service/privilege_apply_workflow_ce.go @@ -0,0 +1,50 @@ +//go:build !dms + +package service + +import ( + "context" + "errors" + + v1 "github.com/actiontech/dms/api/dms/service/v1" + privilegeApplyBiz "github.com/actiontech/dms/internal/privilege_apply/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 errNotSupportPrivilegeApplyWorkflow = errors.New("privilege apply workflow related functions are enterprise version functions") + +type privilegeApplyWorkflowUsecase = privilegeApplyBiz.PrivilegeApplyWorkflowUsecase + +func initPrivilegeApplyWorkflowUsecase(_ utilLog.Logger, _ *storage.Storage, _ dmsBiz.ProxyTargetRepo, _ *dmsBiz.OpPermissionVerifyUsecase, _ *dmsBiz.UserUsecase, _ *dmsBiz.DBServiceUsecase) (*privilegeApplyWorkflowUsecase, error) { + return nil, nil +} + +func (d *DMSService) GetPrivilegeApplyAssignees(ctx context.Context, req *v1.GetPrivilegeApplyAssigneesReq) (*v1.GetPrivilegeApplyAssigneesReply, error) { + return nil, errNotSupportPrivilegeApplyWorkflow +} + +func (d *DMSService) CreatePrivilegeApplyWorkflow(ctx context.Context, req *v1.CreatePrivilegeApplyWorkflowReq, currentUserUID string) (*v1.CreatePrivilegeApplyWorkflowReply, error) { + return nil, errNotSupportPrivilegeApplyWorkflow +} + +func (d *DMSService) GetPrivilegeApplyWorkflow(ctx context.Context, req *v1.GetPrivilegeApplyWorkflowReq, currentUserUID string) (*v1.GetPrivilegeApplyWorkflowReply, error) { + return nil, errNotSupportPrivilegeApplyWorkflow +} + +func (d *DMSService) ListPrivilegeApplyWorkflows(ctx context.Context, req *v1.ListPrivilegeApplyWorkflowsReq, currentUserUID string) (*v1.ListPrivilegeApplyWorkflowsReply, error) { + return nil, errNotSupportPrivilegeApplyWorkflow +} + +func (d *DMSService) ApprovePrivilegeApplyWorkflow(ctx context.Context, req *v1.ApprovePrivilegeApplyWorkflowReq, currentUserUID string) error { + return errNotSupportPrivilegeApplyWorkflow +} + +func (d *DMSService) RejectPrivilegeApplyWorkflow(ctx context.Context, req *v1.RejectPrivilegeApplyWorkflowReq, currentUserUID string) error { + return errNotSupportPrivilegeApplyWorkflow +} + +func (d *DMSService) RetryReissuePrivilegeApplyWorkflow(ctx context.Context, req *v1.RetryReissuePrivilegeApplyWorkflowReq, currentUserUID string) error { + return errNotSupportPrivilegeApplyWorkflow +} diff --git a/internal/dms/service/service.go b/internal/dms/service/service.go index cc4dafb7..c3b87d1b 100644 --- a/internal/dms/service/service.go +++ b/internal/dms/service/service.go @@ -43,6 +43,7 @@ type DMSService struct { ClusterUsecase *biz.ClusterUsecase DataExportWorkflowUsecase *biz.DataExportWorkflowUsecase UnmaskingWorkflowUsecase *unmaskingWorkflowUsecase + PrivilegeApplyWorkflowUsecase *privilegeApplyWorkflowUsecase CbOperationLogUsecase *biz.CbOperationLogUsecase DataMaskingUsecase *dataMaskingUsecase FunctionSupportRegistry *biz.FunctionSupportRegistry @@ -167,6 +168,10 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer if err != nil { return nil, fmt.Errorf("failed to initialize unmasking workflow usecase: %v", err) } + privilegeApplyWorkflowUsecase, err := initPrivilegeApplyWorkflowUsecase(logger, st, dmsProxyTargetRepo, opPermissionVerifyUsecase, userUsecase, dbServiceUseCase) + if err != nil { + return nil, fmt.Errorf("failed to initialize privilege apply workflow usecase: %v", err) + } DataExportWorkflowUsecase := biz.NewDataExportWorkflowUsecase(logger, tx, workflowRepo, dataExportTaskRepo, dbServiceRepo, dataExportMaskingConfigRepo, dataExportMaskingRuleRepo, opPermissionVerifyUsecase, projectUsecase, &opsTypeUsecase, 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 { @@ -219,6 +224,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer ClusterUsecase: clusterUsecase, DataExportWorkflowUsecase: DataExportWorkflowUsecase, UnmaskingWorkflowUsecase: unmaskingWorkflowUsecase, + PrivilegeApplyWorkflowUsecase: privilegeApplyWorkflowUsecase, CbOperationLogUsecase: CbOperationLogUsecase, DataMaskingUsecase: dataMaskingUsecase, FunctionSupportRegistry: functionSupportRegistry, diff --git a/internal/dms/service/user.go b/internal/dms/service/user.go index d8d82c7b..dffe8539 100644 --- a/internal/dms/service/user.go +++ b/internal/dms/service/user.go @@ -850,6 +850,8 @@ func convertBizOpPermission(opPermissionUid string) (apiOpPermissionTyp dmsCommo apiOpPermissionTyp = dmsCommonV1.OpPermissionDesensitization case pkgConst.UIdOfOpPermissionMaskingAudit: apiOpPermissionTyp = dmsCommonV1.OpPermissionMaskingAudit + case pkgConst.UIdOfOpPermissionPrivilegeApplyAudit: + apiOpPermissionTyp = dmsCommonV1.OpPermissionPrivilegeApplyAudit case pkgConst.UIDOfOrdinaryUser: apiOpPermissionTyp = dmsCommonV1.OpPermissionTypeNone default: diff --git a/internal/pkg/locale/active.zh.toml b/internal/pkg/locale/active.zh.toml index f1b5bc5c..7136d2b0 100644 --- a/internal/pkg/locale/active.zh.toml +++ b/internal/pkg/locale/active.zh.toml @@ -67,6 +67,7 @@ DescOpPermissionManageRoleMange = "角色管理权限;拥有该权限的用户 DescOpPermissionManageSQLMangeWhiteList = "管控SQL例外;拥有该权限的用户可以管理管控SQL例外" DescOpPermissionMangeAuditSQLWhiteList = "审核SQL例外;拥有该权限的用户可以管理审核SQL例外" DescOpPermissionMaskingAudit = "脱敏审核;拥有该权限的用户可以查看和处理脱敏审批请求" +DescOpPermissionPrivilegeApplyAudit = "提权审批;拥有该权限的用户可以审批工作台提权申请" DescOpPermissionOrdinaryUser = "基础功能操作权限,可进行日常业务操作" DescOpPermissionProjectAdmin = "项目管理;拥有该权限的用户可以管理项目下的所有资源" DescOpPermissionPushRule = "管理推送规则;拥有该权限的用户可以管理推送规则" @@ -134,6 +135,7 @@ NameOpPermissionManageRoleMange = "角色管理权限" NameOpPermissionManageSQLMangeWhiteList = "管控SQL例外" NameOpPermissionMangeAuditSQLWhiteList = "审核SQL例外" NameOpPermissionMaskingAudit = "脱敏审核" +NameOpPermissionPrivilegeApplyAudit = "提权审批" NameOpPermissionOrdinaryUser = "普通用户" NameOpPermissionProjectAdmin = "项目管理" NameOpPermissionPushRule = "管理推送规则" diff --git a/internal/pkg/locale/message_zh.go b/internal/pkg/locale/message_zh.go index df602e19..bc982b2f 100644 --- a/internal/pkg/locale/message_zh.go +++ b/internal/pkg/locale/message_zh.go @@ -61,6 +61,7 @@ var ( NameOpPermissionManageRoleMange = &i18n.Message{ID: "NameOpPermissionManageRoleMange", Other: "角色管理权限"} NameOpPermissionDesensitization = &i18n.Message{ID: "NameOpPermissionDesensitization", Other: "配置脱敏任务"} NameOpPermissionMaskingAudit = &i18n.Message{ID: "NameOpPermissionMaskingAudit", Other: "脱敏审核"} + NameOpPermissionPrivilegeApplyAudit = &i18n.Message{ID: "NameOpPermissionPrivilegeApplyAudit", Other: "提权审批"} DescOpPermissionGlobalManagement = &i18n.Message{ID: "DescOpPermissionGlobalManagement", Other: "具备系统最高权限,可进行系统配置、用户管理等操作"} DescOpPermissionGlobalView = &i18n.Message{ID: "DescOpPermissionGlobalView", Other: "负责系统操作审计、数据合规检查等工作"} @@ -99,6 +100,7 @@ var ( DescOpPermissionManageRoleMange = &i18n.Message{ID: "DescOpPermissionManageRoleMange", Other: "角色管理权限;拥有该权限的用户可以管理角色"} DescOpPermissionDesensitization = &i18n.Message{ID: "DescOpPermissionDesensitization", Other: "配置脱敏任务;拥有该权限的用户可以管理脱敏模板、脱敏发现任务和规则配置"} DescOpPermissionMaskingAudit = &i18n.Message{ID: "DescOpPermissionMaskingAudit", Other: "脱敏审核;拥有该权限的用户可以查看和处理脱敏审批请求"} + DescOpPermissionPrivilegeApplyAudit = &i18n.Message{ID: "DescOpPermissionPrivilegeApplyAudit", Other: "提权审批;拥有该权限的用户可以审批工作台提权申请"} ) // role diff --git a/internal/privilege_apply/biz/privilege_apply_workflow.go b/internal/privilege_apply/biz/privilege_apply_workflow.go new file mode 100644 index 00000000..89aae893 --- /dev/null +++ b/internal/privilege_apply/biz/privilege_apply_workflow.go @@ -0,0 +1,230 @@ +package biz + +import ( + "context" + "errors" + "time" +) + +// PrivilegeApplyWorkflowApprovalStatus 审批状态 +// swagger:enum PrivilegeApplyWorkflowApprovalStatus +type PrivilegeApplyWorkflowApprovalStatus string + +const ( + PrivilegeApplyWorkflowApprovalStatusPending PrivilegeApplyWorkflowApprovalStatus = "pending" + PrivilegeApplyWorkflowApprovalStatusApproved PrivilegeApplyWorkflowApprovalStatus = "approved" + PrivilegeApplyWorkflowApprovalStatusRejected PrivilegeApplyWorkflowApprovalStatus = "rejected" + PrivilegeApplyWorkflowApprovalStatusCancelled PrivilegeApplyWorkflowApprovalStatus = "cancelled" +) + +func (s PrivilegeApplyWorkflowApprovalStatus) String() string { + return string(s) +} + +// PrivilegeApplyReissueStatus 换发状态 +// swagger:enum PrivilegeApplyReissueStatus +type PrivilegeApplyReissueStatus string + +const ( + PrivilegeApplyReissueStatusNone PrivilegeApplyReissueStatus = "none" + PrivilegeApplyReissueStatusRunning PrivilegeApplyReissueStatus = "running" + PrivilegeApplyReissueStatusSucceeded PrivilegeApplyReissueStatus = "succeeded" + PrivilegeApplyReissueStatusFailed PrivilegeApplyReissueStatus = "failed" +) + +func (s PrivilegeApplyReissueStatus) String() string { + return string(s) +} + +// PrivilegeObject 所需权限对象 +// swagger:model PrivilegeObject +type PrivilegeObject struct { + Schema string `json:"schema"` + ObjectName string `json:"object_name"` + ObjectType string `json:"object_type"` +} + +// PrivilegeApplyAction 操作动作 +// swagger:enum PrivilegeApplyAction +type PrivilegeApplyAction string + +const ( + PrivilegeApplyActionSubmit PrivilegeApplyAction = "submit" + PrivilegeApplyActionApprove PrivilegeApplyAction = "approve" + PrivilegeApplyActionReject PrivilegeApplyAction = "reject" + PrivilegeApplyActionRetry PrivilegeApplyAction = "retry_reissue" +) + +func (a PrivilegeApplyAction) String() string { + return string(a) +} + +var ( + ErrNoAssignee = errors.New("privilege_apply.no_assignee") + ErrAccountMismatch = errors.New("privilege_apply.account_mismatch") + ErrInvalidArgument = errors.New("privilege_apply.invalid_argument") + ErrForbidden = errors.New("privilege_apply.forbidden") + ErrNotPending = errors.New("privilege_apply.not_pending") + ErrReissueFailed = errors.New("privilege_apply.reissue_failed") + ErrEmptyPermissions = errors.New("privilege_apply.empty_permissions") +) + +// AssigneeResolveMode 审批人解析模式 +type AssigneeResolveMode int + +const ( + AssigneeResolveModePreview AssigneeResolveMode = iota + AssigneeResolveModeSubmit +) + +// PrivilegeApplyImpactPreview 落地影响预览 +type PrivilegeApplyImpactPreview struct { + WillCreateAccount bool `json:"will_create_account"` + SourceAccountUID string `json:"source_account_uid"` + SourceAccountName string `json:"source_account_name"` + PermissionUnionSummary string `json:"permission_union_summary"` + UnbindSourceThenBindTarget bool `json:"unbind_source_then_bind_target"` + OtherUsersOnSourceUnaffected bool `json:"other_users_on_source_unaffected"` + SourceAccountGrantsUnchanged bool `json:"source_account_grants_unchanged"` +} + +// ApprovedPermissions 审批人修正后的权限集合 +type ApprovedPermissions struct { + Objects []PrivilegeObject `json:"objects"` + Actions []string `json:"actions"` +} + +// PrivilegeApplyWorkflow 提权申请工单 +type PrivilegeApplyWorkflow struct { + UID string + ProjectUID string + ApplicantUID string + ApplicantName string + CreatedAt time.Time + DBServiceUID string + DBServiceName string + SourceDBAccountUID string + SourceDBAccountName string + RawSQL string + ErrorMessage string + RequestedObjects []PrivilegeObject + RequestedActions []string + ApprovedObjects []PrivilegeObject + ApprovedActions []string + ApplyReason string + ExpectedExpireDays *int64 + ApprovalStatus PrivilegeApplyWorkflowApprovalStatus + ReissueStatus PrivilegeApplyReissueStatus + CurrentAssigneeUIDs []string + CurrentAssignees []UIDWithName + RejectReason string + ApproverUID string + ApprovedAt *time.Time + TargetDBAccountUID string + TargetDBAccountName string + ReissueError string + ImpactPreview *PrivilegeApplyImpactPreview + OperationLogs []*PrivilegeApplyOperationLog +} + +// UIDWithName 用户 UID 与展示名 +type UIDWithName struct { + UID string `json:"uid"` + Name string `json:"name"` +} + +// PrivilegeApplyOperationLog 操作日志 +type PrivilegeApplyOperationLog struct { + UID string + WorkflowUID string + OperatorUID string + OperatorName string + ActionType PrivilegeApplyAction + ActionTime time.Time + ExtraMessage string +} + +// ListPrivilegeApplyWorkflowsOption 列表查询选项 +type ListPrivilegeApplyWorkflowsOption struct { + PageSize uint32 + PageIndex uint32 + ProjectUID string + FilterByTab string // pending | handled + FilterByDBServiceUID string + ViewerUID string + CanAuditDBServiceUIDs map[string]struct{} +} + +// PrivilegeApplyWorkflowRepository 提权申请持久化 +type PrivilegeApplyWorkflowRepository interface { + CreateWorkflow(ctx context.Context, workflow *PrivilegeApplyWorkflow) error + GetWorkflow(ctx context.Context, workflowUID string) (*PrivilegeApplyWorkflow, error) + UpdateWorkflow(ctx context.Context, workflow *PrivilegeApplyWorkflow) error + ListWorkflows(ctx context.Context, opt *ListPrivilegeApplyWorkflowsOption) ([]*PrivilegeApplyWorkflow, int64, error) +} + +// PrivilegeApplyOperationLogRepository 操作日志持久化 +type PrivilegeApplyOperationLogRepository interface { + CreateOperationLog(ctx context.Context, log *PrivilegeApplyOperationLog) error + ListOperationLogs(ctx context.Context, workflowUID string) ([]*PrivilegeApplyOperationLog, error) +} + +// PrivilegeApplyOpPermissionVerifier 操作权限校验 +type PrivilegeApplyOpPermissionVerifier interface { + GetCanOpDBUsers(ctx context.Context, projectUID, dbServiceUID string, needOpPermissionTypes []string, isBusinessWrite bool) ([]string, error) + UserCanAuditPrivilegeApply(ctx context.Context, projectUID, dbServiceUID, userUID string) (bool, error) +} + +// PrivilegeApplyProvisionGateway provision 编排网关 +type PrivilegeApplyProvisionGateway interface { + RunReissueOrchestration(ctx context.Context, operatorUID string, workflow *PrivilegeApplyWorkflow, approved *ApprovedPermissions, effectiveDays int64) (targetUID, targetName string, err error) + BuildImpactPreview(ctx context.Context, operatorUID string, workflow *PrivilegeApplyWorkflow, approved *ApprovedPermissions) (*PrivilegeApplyImpactPreview, error) +} + +// PrivilegeApplyUserDirectory 用户展示名解析 +type PrivilegeApplyUserDirectory interface { + GetUserNamesByUIDs(ctx context.Context, uids []string) (map[string]string, error) +} + +// PrivilegeApplyDBServiceDirectory 数据源展示名解析 +type PrivilegeApplyDBServiceDirectory interface { + GetDBServiceNamesByUIDs(ctx context.Context, uids []string) (map[string]string, error) +} + +// PrivilegeApplyAccountVerifier 托管账号绑定校验 +type PrivilegeApplyAccountVerifier interface { + VerifyUserBoundAccount(ctx context.Context, projectUID, userUID, dbServiceUID, accountUID string) error +} + +// ApprovePrivilegeApplyWorkflowArgs 批准参数 +type ApprovePrivilegeApplyWorkflowArgs struct { + ProjectUID string + WorkflowUID string + OperatorUID string + ApproveReason string + ApprovedPermissions *ApprovedPermissions + ExpectedExpireDays *int64 +} + +// RejectPrivilegeApplyWorkflowArgs 驳回参数 +type RejectPrivilegeApplyWorkflowArgs struct { + ProjectUID string + WorkflowUID string + OperatorUID string + RejectReason string +} + +// CreatePrivilegeApplyWorkflowArgs 创建提权申请参数 +type CreatePrivilegeApplyWorkflowArgs struct { + ProjectUID string + ApplicantUID string + DBServiceUID string + SourceDBAccountUID string + SourceDBAccountName string + RawSQL string + ErrorMessage string + RequestedObjects []PrivilegeObject + RequestedActions []string + ApplyReason string + ExpectedExpireDays *int64 +} diff --git a/internal/privilege_apply/biz/privilege_apply_workflow_ce.go b/internal/privilege_apply/biz/privilege_apply_workflow_ce.go new file mode 100644 index 00000000..891195ee --- /dev/null +++ b/internal/privilege_apply/biz/privilege_apply_workflow_ce.go @@ -0,0 +1,29 @@ +//go:build !dms + +package biz + +import ( + "context" +) + +type PrivilegeApplyWorkflowUsecase struct{} + +func NewPrivilegeApplyWorkflowUsecase(ctx context.Context) *PrivilegeApplyWorkflowUsecase { + return &PrivilegeApplyWorkflowUsecase{} +} + +func (u *PrivilegeApplyWorkflowUsecase) GetPrivilegeApplyAssignees(ctx context.Context, projectUID, dbServiceUID, applicantUID string, mode AssigneeResolveMode) ([]string, error) { + return nil, nil +} + +func (u *PrivilegeApplyWorkflowUsecase) ListPrivilegeApplyAssigneesWithNames(ctx context.Context, projectUID, dbServiceUID string) ([]UIDWithName, bool, error) { + return nil, false, nil +} + +func (u *PrivilegeApplyWorkflowUsecase) CreatePrivilegeApplyWorkflow(ctx context.Context, args *CreatePrivilegeApplyWorkflowArgs) (string, error) { + return "", nil +} + +func (u *PrivilegeApplyWorkflowUsecase) GetPrivilegeApplyWorkflow(ctx context.Context, projectUID, workflowUID, userUID string) (*PrivilegeApplyWorkflow, error) { + return nil, nil +} diff --git a/internal/privilege_apply/biz/privilege_denied.go b/internal/privilege_apply/biz/privilege_denied.go new file mode 100644 index 00000000..6fad3f87 --- /dev/null +++ b/internal/privilege_apply/biz/privilege_denied.go @@ -0,0 +1,95 @@ +package biz + +import ( + "regexp" + "strings" +) + +var ( + privilegeDeniedVendorCodes = map[string]struct{}{ + "1044": {}, + "1142": {}, + "1227": {}, + } + + privilegeDeniedMessagePatterns = []string{ + "access denied for user", + "command denied to user", + } + + sqlActionPattern = regexp.MustCompile(`(?i)\b(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TRUNCATE|GRANT|REVOKE)\b`) + tableFromPattern = regexp.MustCompile(`(?i)(?:FROM|INTO|UPDATE|TABLE)\s+[` + "`" + `"]?([a-zA-Z0-9_]+)[` + "`" + `"]?(?:\.[` + "`" + `"]?([a-zA-Z0-9_]+)[` + "`" + `"]?)?`) +) + +// PrivilegeDeniedContext 缺权结构化上下文 +type PrivilegeDeniedContext struct { + PrivilegeDenied bool `json:"privilege_denied"` + ProjectUID string `json:"project_uid"` + DBServiceUID string `json:"db_service_uid"` + DBServiceName string `json:"db_service_name,omitempty"` + DBAccountUID string `json:"db_account_uid"` + DBAccountName string `json:"db_account_name"` + RawSQL string `json:"raw_sql"` + ErrorMessage string `json:"error_message"` + VendorCode *string `json:"vendor_code"` + SQLState *string `json:"sql_state"` + RequestedObjects []PrivilegeObject `json:"requested_objects,omitempty"` + RequestedActions []string `json:"requested_actions,omitempty"` +} + +// IsPrivilegeDeniedError 判断是否为库账号缺权类错误 +func IsPrivilegeDeniedError(errorMessage string, vendorCode string) bool { + code := strings.TrimSpace(vendorCode) + if code != "" { + if _, ok := privilegeDeniedVendorCodes[code]; ok { + return true + } + } + msg := strings.ToLower(strings.TrimSpace(errorMessage)) + if msg == "" { + return false + } + for _, pattern := range privilegeDeniedMessagePatterns { + if strings.Contains(msg, pattern) { + return true + } + } + return false +} + +// ParseSQLObjectsActions 尽力从 SQL 解析对象与动作 +func ParseSQLObjectsActions(sql string) (objects []PrivilegeObject, actions []string) { + trimmed := strings.TrimSpace(sql) + if trimmed == "" { + return nil, nil + } + + actionSet := make(map[string]struct{}) + for _, match := range sqlActionPattern.FindAllString(trimmed, -1) { + action := strings.ToUpper(match) + actionSet[action] = struct{}{} + } + for action := range actionSet { + actions = append(actions, action) + } + + objectSet := make(map[string]PrivilegeObject) + for _, match := range tableFromPattern.FindAllStringSubmatch(trimmed, -1) { + if len(match) < 2 { + continue + } + obj := PrivilegeObject{ObjectType: "table"} + if match[2] != "" { + obj.Schema = match[1] + obj.ObjectName = match[2] + } else { + obj.ObjectName = match[1] + } + key := obj.Schema + "." + obj.ObjectName + objectSet[key] = obj + } + for _, obj := range objectSet { + objects = append(objects, obj) + } + return objects, actions +} diff --git a/internal/sql_workbench/service/privilege_denied_middleware_ce.go b/internal/sql_workbench/service/privilege_denied_middleware_ce.go new file mode 100644 index 00000000..4fc4d03e --- /dev/null +++ b/internal/sql_workbench/service/privilege_denied_middleware_ce.go @@ -0,0 +1,18 @@ +//go:build !enterprise +// +build !enterprise + +package sql_workbench + +import "github.com/labstack/echo/v4" + +// PrivilegeDeniedMiddlewareConfig 非 enterprise 构建占位。 +type PrivilegeDeniedMiddlewareConfig struct { + SqlWorkbenchService *SqlWorkbenchService +} + +// GetPrivilegeDeniedMiddleware 非 enterprise 构建下为空操作。 +func GetPrivilegeDeniedMiddleware(_ PrivilegeDeniedMiddlewareConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return next + } +} diff --git a/pkg/dms-common/api/dms/v1/user.go b/pkg/dms-common/api/dms/v1/user.go index 6191af4c..4508fb7f 100644 --- a/pkg/dms-common/api/dms/v1/user.go +++ b/pkg/dms-common/api/dms/v1/user.go @@ -260,6 +260,8 @@ const ( OpPermissionDesensitization OpPermissionType = "desensitization" // 脱敏审核;拥有该权限的用户可以查看和处理脱敏审批请求 OpPermissionMaskingAudit OpPermissionType = "masking_audit" + // 提权审批;拥有该权限的用户可以审批工作台提权申请 + OpPermissionPrivilegeApplyAudit OpPermissionType = "privilege_apply_audit" // 无任何权限 OpPermissionTypeNone OpPermissionType = "none" ) @@ -338,6 +340,8 @@ func ParseOpPermissionType(typ string) (OpPermissionType, error) { return OpPermissionDesensitization, nil case string(OpPermissionMaskingAudit): return OpPermissionMaskingAudit, nil + case string(OpPermissionPrivilegeApplyAudit): + return OpPermissionPrivilegeApplyAudit, nil case string(OpPermissionTypeNone): return OpPermissionTypeNone, nil default: @@ -379,6 +383,8 @@ func GetOperationTypeDesc(opType OpPermissionType) string { return "配置流水线" case OpPermissionMaskingAudit: return "脱敏审核" + case OpPermissionPrivilegeApplyAudit: + return "提权审批" default: return "未知操作类型" }