From 81c7f7acad2c3fbdda6087112f5b522054979737 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Sat, 1 Aug 2026 01:08:29 +0800 Subject: [PATCH] fix: honor project-range op permissions in UserCanOpDB Treat OpRangeType=project as full project DB hit, merge member project permission slots, and include pending unmasking assignees in export pending_for_me. --- internal/dms/biz/member.go | 10 +- internal/dms/biz/op_permission_verify.go | 17 ++- internal/dms/biz/op_permission_verify_test.go | 126 +++++++++++++++++- internal/dms/storage/op_permission_verify.go | 16 ++- internal/dms/storage/workflow.go | 13 +- 5 files changed, 174 insertions(+), 8 deletions(-) diff --git a/internal/dms/biz/member.go b/internal/dms/biz/member.go index d9008e284..8283d5042 100644 --- a/internal/dms/biz/member.go +++ b/internal/dms/biz/member.go @@ -240,9 +240,13 @@ func (m *MemberUsecase) CheckRoleAndOpRanges(ctx context.Context, roleAndOpRange } else if !exist { return fmt.Errorf("db service not exist") } - // 角色目前与成员绑定,只支持配置数据源范围的权限 - case OpRangeTypeProject, OpRangeTypeGlobal: - return fmt.Errorf("role currently only support the db service op range type, but got type: %v", op.RangeType) + case OpRangeTypeProject: + // 纯项目范围角色(如单独「脱敏审核」)允许以 project 挂载;RangeUIDs 为项目 UID + if len(r.RangeUIDs) == 0 { + return fmt.Errorf("project op range requires project uid in range_uids") + } + case OpRangeTypeGlobal: + return fmt.Errorf("role currently only support the db service or project op range type, but got type: %v", op.RangeType) default: return fmt.Errorf("unsupported range type: %v", op.RangeType) } diff --git a/internal/dms/biz/op_permission_verify.go b/internal/dms/biz/op_permission_verify.go index 7be525a73..0ea97010a 100644 --- a/internal/dms/biz/op_permission_verify.go +++ b/internal/dms/biz/op_permission_verify.go @@ -410,13 +410,20 @@ func (o *OpPermissionVerifyUsecase) UserCanOpDB(userOpPermissions []OpPermission // 动作权限(创建、审核、上线工单等) for _, needOpPermission := range needOpPermissionTypes { - if needOpPermission == userOpPermission.OpPermissionUID && userOpPermission.OpRangeType == OpRangeType(dmsV1.OpRangeTypeDBService) { + if needOpPermission != userOpPermission.OpPermissionUID { + continue + } + switch userOpPermission.OpRangeType { + case OpRangeType(dmsV1.OpRangeTypeDBService): // 对象权限(指定数据源) for _, id := range userOpPermission.RangeUIDs { if id == dbServiceUid { return true } } + case OpRangeType(dmsV1.OpRangeTypeProject): + // 项目范围权限视为本项目全部数据源命中(RangeUIDs 为项目 UID,已由授权写入保证同项目) + return true } } } @@ -489,12 +496,18 @@ func (o *OpPermissionVerifyUsecase) userCanOpDBWithoutAdminPrivilege(userOpPermi } for _, needOpPermission := range needOpPermissionTypes { - if needOpPermission == userOpPermission.OpPermissionUID && userOpPermission.OpRangeType == OpRangeType(dmsV1.OpRangeTypeDBService) { + if needOpPermission != userOpPermission.OpPermissionUID { + continue + } + switch userOpPermission.OpRangeType { + case OpRangeType(dmsV1.OpRangeTypeDBService): for _, id := range userOpPermission.RangeUIDs { if id == dbServiceUid { return true } } + case OpRangeType(dmsV1.OpRangeTypeProject): + return true } } } diff --git a/internal/dms/biz/op_permission_verify_test.go b/internal/dms/biz/op_permission_verify_test.go index c48f155dd..f5de0ad67 100644 --- a/internal/dms/biz/op_permission_verify_test.go +++ b/internal/dms/biz/op_permission_verify_test.go @@ -323,6 +323,7 @@ func TestGetCanOpDBUsers(t *testing.T) { members []ListMembersOpPermissionItem users map[string]*User isBusinessWrite bool + needPermUID string wantUserUIDs []string }{ { @@ -340,6 +341,7 @@ func TestGetCanOpDBUsers(t *testing.T) { pkgConst.UIDOfUserAdmin: {UID: pkgConst.UIDOfUserAdmin, BusinessWritePermission: true}, }, isBusinessWrite: true, + needPermUID: opPermExportApproval, wantUserUIDs: []string{pkgConst.UIDOfUserAdmin}, }, { @@ -357,6 +359,7 @@ func TestGetCanOpDBUsers(t *testing.T) { pkgConst.UIDOfUserAdmin: {UID: pkgConst.UIDOfUserAdmin, BusinessWritePermission: false}, }, isBusinessWrite: true, + needPermUID: opPermExportApproval, // Admin has ProjectAdmin but BWP=false skips admin privilege; // userCanOpDBWithoutAdminPrivilege skips ProjectAdmin -> not included wantUserUIDs: []string{}, @@ -381,6 +384,7 @@ func TestGetCanOpDBUsers(t *testing.T) { pkgConst.UIDOfUserAdmin: {UID: pkgConst.UIDOfUserAdmin, BusinessWritePermission: false}, }, isBusinessWrite: true, + needPermUID: opPermExportApproval, // Admin has BWP=false but also has explicit DB permission -> included via project auth wantUserUIDs: []string{pkgConst.UIDOfUserAdmin}, }, @@ -411,6 +415,7 @@ func TestGetCanOpDBUsers(t *testing.T) { "normal_user_1": {UID: "normal_user_1", BusinessWritePermission: true}, }, isBusinessWrite: true, + needPermUID: opPermExportApproval, // Only normal user has explicit DB permission; admin BWP=false without explicit auth wantUserUIDs: []string{"normal_user_1"}, }, @@ -429,6 +434,7 @@ func TestGetCanOpDBUsers(t *testing.T) { pkgConst.UIDOfUserAdmin: {UID: pkgConst.UIDOfUserAdmin, BusinessWritePermission: false}, }, isBusinessWrite: false, + needPermUID: opPermExportApproval, // Resource config: BWP doesn't affect, admin privilege applies wantUserUIDs: []string{pkgConst.UIDOfUserAdmin}, }, @@ -451,6 +457,7 @@ func TestGetCanOpDBUsers(t *testing.T) { "normal_user_1": {UID: "normal_user_1", BusinessWritePermission: true}, }, isBusinessWrite: true, + needPermUID: opPermExportApproval, wantUserUIDs: []string{"normal_user_1"}, }, { @@ -466,8 +473,89 @@ func TestGetCanOpDBUsers(t *testing.T) { "normal_user_1": {UID: "normal_user_1", BusinessWritePermission: true}, }, isBusinessWrite: true, + needPermUID: opPermExportApproval, wantUserUIDs: []string{}, }, + { + name: "project_range_masking_audit_hits_all_db_in_project", + members: []ListMembersOpPermissionItem{ + { + UserUid: "masking_approver_b", + UserName: "approver_b", + OpPermissions: []OpPermissionWithOpRange{ + { + OpPermissionUID: pkgConst.UIdOfOpPermissionMaskingAudit, + OpRangeType: OpRangeType(dmsV1.OpRangeTypeProject), + RangeUIDs: []string{testProjectUID}, + }, + }, + }, + }, + users: map[string]*User{ + "masking_approver_b": {UID: "masking_approver_b", BusinessWritePermission: true}, + }, + isBusinessWrite: true, + needPermUID: pkgConst.UIdOfOpPermissionMaskingAudit, + wantUserUIDs: []string{"masking_approver_b"}, + }, + { + name: "project_range_masking_audit_not_selected_for_export_approval", + members: []ListMembersOpPermissionItem{ + { + UserUid: "masking_only_user", + UserName: "masking_only", + OpPermissions: []OpPermissionWithOpRange{ + { + OpPermissionUID: pkgConst.UIdOfOpPermissionMaskingAudit, + OpRangeType: OpRangeType(dmsV1.OpRangeTypeProject), + RangeUIDs: []string{testProjectUID}, + }, + }, + }, + { + UserUid: "export_approver", + UserName: "export_approver", + OpPermissions: []OpPermissionWithOpRange{ + { + OpPermissionUID: opPermExportApproval, + OpRangeType: OpRangeType(dmsV1.OpRangeTypeDBService), + RangeUIDs: []string{testDBServiceUID}, + }, + }, + }, + }, + users: map[string]*User{ + "masking_only_user": {UID: "masking_only_user", BusinessWritePermission: true}, + "export_approver": {UID: "export_approver", BusinessWritePermission: true}, + }, + isBusinessWrite: true, + needPermUID: opPermExportApproval, + // need=ExportApproval:仅有 project·700038 者不得入选 + wantUserUIDs: []string{"export_approver"}, + }, + { + name: "admin_bwp_off_with_project_range_masking_audit", + members: []ListMembersOpPermissionItem{ + { + UserUid: pkgConst.UIDOfUserAdmin, + UserName: "admin", + OpPermissions: []OpPermissionWithOpRange{ + {OpPermissionUID: pkgConst.UIDOfOpPermissionProjectAdmin}, + { + OpPermissionUID: pkgConst.UIdOfOpPermissionMaskingAudit, + OpRangeType: OpRangeType(dmsV1.OpRangeTypeProject), + RangeUIDs: []string{testProjectUID}, + }, + }, + }, + }, + users: map[string]*User{ + pkgConst.UIDOfUserAdmin: {UID: pkgConst.UIDOfUserAdmin, BusinessWritePermission: false}, + }, + isBusinessWrite: true, + needPermUID: pkgConst.UIdOfOpPermissionMaskingAudit, + wantUserUIDs: []string{pkgConst.UIDOfUserAdmin}, + }, } for _, tc := range cases { @@ -485,7 +573,7 @@ func TestGetCanOpDBUsers(t *testing.T) { context.Background(), testProjectUID, testDBServiceUID, - []string{opPermExportApproval}, + []string{tc.needPermUID}, tc.isBusinessWrite, ) assert.NoError(t, err) @@ -493,3 +581,39 @@ func TestGetCanOpDBUsers(t *testing.T) { }) } } + +// TestUserCanOpDB_ProjectRange covers S2: project-scoped permission hits all DB services in project. +func TestUserCanOpDB_ProjectRange(t *testing.T) { + uc := newTestOpPermissionVerifyUsecase(&mockUserRepo{users: map[string]*User{}}, &mockOpPermissionVerifyRepo{}) + maskingAudit := pkgConst.UIdOfOpPermissionMaskingAudit + exportApproval := pkgConst.UIDOfOpPermissionExportApprovalReject + + t.Run("project_range_matching_uid_hits", func(t *testing.T) { + perms := []OpPermissionWithOpRange{{ + OpPermissionUID: maskingAudit, + OpRangeType: OpRangeType(dmsV1.OpRangeTypeProject), + RangeUIDs: []string{"project_1"}, + }} + assert.True(t, uc.UserCanOpDB(perms, []string{maskingAudit}, "db_any")) + assert.True(t, uc.userCanOpDBWithoutAdminPrivilege(perms, []string{maskingAudit}, "db_any")) + }) + + t.Run("project_range_wrong_uid_misses", func(t *testing.T) { + perms := []OpPermissionWithOpRange{{ + OpPermissionUID: maskingAudit, + OpRangeType: OpRangeType(dmsV1.OpRangeTypeProject), + RangeUIDs: []string{"project_1"}, + }} + assert.False(t, uc.UserCanOpDB(perms, []string{exportApproval}, "db_1")) + }) + + t.Run("db_service_range_still_requires_uid_match", func(t *testing.T) { + perms := []OpPermissionWithOpRange{{ + OpPermissionUID: exportApproval, + OpRangeType: OpRangeType(dmsV1.OpRangeTypeDBService), + RangeUIDs: []string{"db_1"}, + }} + assert.True(t, uc.UserCanOpDB(perms, []string{exportApproval}, "db_1")) + assert.False(t, uc.UserCanOpDB(perms, []string{exportApproval}, "db_2")) + }) +} diff --git a/internal/dms/storage/op_permission_verify.go b/internal/dms/storage/op_permission_verify.go index 1016c97e7..a9fd91c90 100644 --- a/internal/dms/storage/op_permission_verify.go +++ b/internal/dms/storage/op_permission_verify.go @@ -442,6 +442,7 @@ func (o *OpPermissionVerifyRepo) ListUsersOpPermissionInProject(ctx context.Cont } { + // 角色挂载权限 UNION 成员/成员组项目权限槽(与 GetUserOpPermissionInProject 合并语义一致) if err = tx.WithContext(ctx).Raw(` SELECT m.user_uid, p.op_permission_uid, r.op_range_type, r.range_uids @@ -455,7 +456,20 @@ func (o *OpPermissionVerifyRepo) ListUsersOpPermissionInProject(ctx context.Cont JOIN member_group_users mgu ON mg.uid = mgu.member_group_uid JOIN member_group_role_op_ranges mgror ON mgu.member_group_uid = mgror.member_group_uid JOIN role_op_permissions rop ON mgror.role_uid = rop.role_uid - WHERE mg.project_uid = ? and mgu.user_uid in (?)`, userIds, projectUid, projectUid, userIds).Scan(&permissionResults).Error; err != nil { + WHERE mg.project_uid = ? and mgu.user_uid in (?) + UNION + SELECT + m.user_uid, mop.op_permission_uid, 'project' AS op_range_type, m.project_uid AS range_uids + FROM members AS m + JOIN member_op_permissions AS mop ON m.uid = mop.member_uid AND m.user_uid IN (?) AND m.project_uid = ? + UNION + SELECT + DISTINCT mgu.user_uid, mgop.op_permission_uid, 'project' AS op_range_type, mg.project_uid AS range_uids + FROM member_groups mg + JOIN member_group_users mgu ON mg.uid = mgu.member_group_uid + JOIN member_group_op_permissions AS mgop ON mg.uid = mgop.member_group_uid + WHERE mg.project_uid = ? AND mgu.user_uid IN (?)`, + userIds, projectUid, projectUid, userIds, userIds, projectUid, projectUid, userIds).Scan(&permissionResults).Error; err != nil { return fmt.Errorf("failed to get user op permission in project: %v", err) } } diff --git a/internal/dms/storage/workflow.go b/internal/dms/storage/workflow.go index 9cd784991..f945ceb76 100644 --- a/internal/dms/storage/workflow.go +++ b/internal/dms/storage/workflow.go @@ -490,7 +490,18 @@ AND wr.status IN (:filter_status) {{- end }} {{- if .filter_current_step_assignee_user_id }} -AND ((wr.status != 'rejected' AND curr_ws.assignees REGEXP :filter_current_step_assignee_user_id) OR (wr.status = 'rejected' AND w.create_user_uid = :filter_current_step_assignee_user_id)) +AND ( + ((wr.status != 'rejected' AND curr_ws.assignees REGEXP :filter_current_step_assignee_user_id) OR (wr.status = 'rejected' AND w.create_user_uid = :filter_current_step_assignee_user_id)) + OR EXISTS ( + SELECT 1 FROM unmasking_workflows uw + WHERE uw.deleted_at IS NULL + AND uw.source_type = 'data_export' + AND uw.source_uid = w.uid + AND uw.approval_status = 'pending' + AND JSON_VALID(uw.current_assignee_uids) + AND JSON_CONTAINS(uw.current_assignee_uids, JSON_QUOTE(:filter_current_step_assignee_user_id), '$') + ) +) {{- end }} {{- if .filter_db_service_uid }}