From 11b83c2090bfd605ff23878a265c2445441c1ec7 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Tue, 18 Aug 2026 03:13:46 +0000 Subject: [PATCH] fix: honor project-range op permissions in UserCanOpDB Permissions registered with project range, such as masking audit 700038, are granted through the member/member-group project-manage slot and are stored with op_range_type=project. UserCanOpDB only matched db_service ranges, so holders were never recognised on any datasource and could not be resolved as unmasking approvers. Treat project range as a hit on every datasource in the project, apply the same rule in userCanOpDBWithoutAdminPrivilege so BWP-disabled administrators are not under-matched, and union the project-manage slots into ListUsersOpPermissionInProject so batch lookups return the same permission set as the per-user query already does. No other caller changes behaviour: every needOpPermissionTypes passed to UserCanOpDB or GetCanOpDBUsers elsewhere is a db_service-range permission, and ListDBServiceTips restricts functional_module to three db_service-range values. --- internal/dms/biz/op_permission_verify.go | 28 +++- internal/dms/biz/op_permission_verify_test.go | 126 +++++++++++++++++- internal/dms/storage/op_permission_verify.go | 16 ++- 3 files changed, 166 insertions(+), 4 deletions(-) diff --git a/internal/dms/biz/op_permission_verify.go b/internal/dms/biz/op_permission_verify.go index 7be525a73..a6c5a7156 100644 --- a/internal/dms/biz/op_permission_verify.go +++ b/internal/dms/biz/op_permission_verify.go @@ -401,6 +401,16 @@ func (o *OpPermissionVerifyUsecase) GetUserProject(ctx context.Context, userUid return projects, nil } +// UserCanOpDB 判断用户在 dbServiceUid 上是否具备 needOpPermissionTypes 中的任一权限。 +// +// 两种授权范围的判定语义不同: +// - db_service:授予到具体数据源,需 RangeUIDs 命中 dbServiceUid; +// - project:授予到整个项目,视为命中项目内全部数据源(如「脱敏审核」700038)。 +// +// project 分支不会放大数据源级权限:范围为 project 的记录只可能来自成员/成员组的 +// 「项目管理权限」槽,而该槽的可选项由 ListProjectOpPermissions 限定为注册范围是 +// project 的权限;数据源级授权走角色,其可选项由 ListMemberOpPermissions 明确排除 +// project 范围。两个入口互斥,db_service 权限不会被标记成 project。 func (o *OpPermissionVerifyUsecase) UserCanOpDB(userOpPermissions []OpPermissionWithOpRange, needOpPermissionTypes []string, dbServiceUid string) bool { for _, userOpPermission := range userOpPermissions { // 项目管理员可以查看所有数据源 @@ -410,13 +420,19 @@ 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): + return true } } } @@ -481,6 +497,8 @@ func (o *OpPermissionVerifyUsecase) GetCanOpDBUsers(ctx context.Context, project // userCanOpDBWithoutAdminPrivilege checks DB operation permission without // considering ProjectAdmin privilege (used for BWP-disabled system administrators). +// 范围判定与 UserCanOpDB 保持一致:project 范围视为命中项目内全部数据源, +// 否则 BWP 关闭的管理员会因项目级授权被漏判。 func (o *OpPermissionVerifyUsecase) userCanOpDBWithoutAdminPrivilege(userOpPermissions []OpPermissionWithOpRange, needOpPermissionTypes []string, dbServiceUid string) bool { for _, userOpPermission := range userOpPermissions { // Skip ProjectAdmin check - that's the admin privilege we're excluding @@ -489,12 +507,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) } }