checkPermLevelFailClose_audit_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. package auth
  2. import (
  3. "errors"
  4. "math"
  5. "testing"
  6. "perms-system-server/internal/consts"
  7. "perms-system-server/internal/loaders"
  8. deptModel "perms-system-server/internal/model/dept"
  9. memberModel "perms-system-server/internal/model/productmember"
  10. userModel "perms-system-server/internal/model/user"
  11. "perms-system-server/internal/response"
  12. "perms-system-server/internal/testutil/ctxhelper"
  13. "perms-system-server/internal/testutil/mocks"
  14. "github.com/stretchr/testify/assert"
  15. "github.com/stretchr/testify/require"
  16. "github.com/zeromicro/go-zero/core/stores/sqlx"
  17. "go.uber.org/mock/gomock"
  18. )
  19. // ---------------------------------------------------------------------------
  20. // 覆盖目标:审计 L-4 修复 —— checkPermLevel 在 DB 非 ErrNotFound 错误时必须 fail-close 返回 500,
  21. // 而不是被默默降级为"目标无角色 → 权限最低 → 放行"。
  22. // 该测试用 gomock 伪造 SysRoleModel.FindMinPermsLevelByUserIdAndProductCode 返回一个通用 DB 错误,
  23. // 验证 CheckManageAccess 的响应是 500 CodeError(非 403)。
  24. // ---------------------------------------------------------------------------
  25. // TC-0819: L-4 —— checkPermLevel 遇到非 ErrNotFound 的 DB 错误时必须 500。
  26. func TestCheckManageAccess_DBError_FailCloseWith500(t *testing.T) {
  27. ctrl := gomock.NewController(t)
  28. t.Cleanup(ctrl.Finish)
  29. const targetUserId = int64(42)
  30. const callerDeptId = int64(1)
  31. const targetDeptId = int64(2)
  32. const productCode = "test_product"
  33. // 让 checkDeptHierarchy 顺利放行:target 在 caller 子部门下(path 前缀 /1/)。
  34. mockUser := mocks.NewMockSysUserModel(ctrl)
  35. mockUser.EXPECT().FindOne(gomock.Any(), int64(targetUserId)).
  36. Return(&userModel.SysUser{Id: targetUserId, DeptId: targetDeptId}, nil).AnyTimes()
  37. mockDept := mocks.NewMockSysDeptModel(ctrl)
  38. mockDept.EXPECT().FindOne(gomock.Any(), targetDeptId).
  39. Return(&deptModel.SysDept{Id: targetDeptId, Path: "/1/2/"}, nil).AnyTimes()
  40. // 让 permsLevel 判定路径进入:"target 也是 MEMBER,同级 → 需要 DB 查 permsLevel"。
  41. mockPM := mocks.NewMockSysProductMemberModel(ctrl)
  42. mockPM.EXPECT().FindOneByProductCodeUserId(gomock.Any(), productCode, int64(targetUserId)).
  43. Return(&memberModel.SysProductMember{
  44. UserId: targetUserId, ProductCode: productCode,
  45. MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled,
  46. }, nil).AnyTimes()
  47. // 关键:SysRoleModel 返回非 ErrNotFound 的 DB 错误。
  48. dbErr := errors.New("driver: bad connection")
  49. mockRole := mocks.NewMockSysRoleModel(ctrl)
  50. mockRole.EXPECT().
  51. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(targetUserId), productCode).
  52. Return(int64(0), dbErr).AnyTimes()
  53. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  54. User: mockUser,
  55. Dept: mockDept,
  56. Role: mockRole,
  57. ProductMember: mockPM,
  58. })
  59. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  60. UserId: 100,
  61. Username: "l4_member_caller",
  62. IsSuperAdmin: false,
  63. MemberType: consts.MemberTypeMember,
  64. Status: consts.StatusEnabled,
  65. ProductCode: productCode,
  66. DeptId: callerDeptId,
  67. DeptPath: "/1/",
  68. MinPermsLevel: 100,
  69. })
  70. err := CheckManageAccess(ctx, svcCtx, targetUserId, productCode)
  71. require.Error(t, err, "DB 错误时必须 fail-close")
  72. var ce *response.CodeError
  73. require.True(t, errors.As(err, &ce), "必须是结构化 CodeError")
  74. assert.Equal(t, 500, ce.Code(),
  75. "L-4:DB 非 ErrNotFound 错误绝不能被伪装成'无角色'从而降级为 403/放行;必须是 500")
  76. assert.NotContains(t, ce.Error(), "无权管理",
  77. "错误消息不得看起来像权限判定成功后做出的业务决策(避免误导运维)")
  78. }
  79. // TC-0820: L-4 对照组 —— ErrNotFound 仍应被视作"无角色",即按最低权限处理(由 caller.MinPermsLevel 决定放行还是 403)。
  80. // 这里构造 caller 的 MinPermsLevel=MaxInt64(sentinel),target 无角色(ErrNotFound) →
  81. // caller.MinPermsLevel(=MaxInt64) >= targetLevel(=MaxInt64) → 返回 403。这个分支不是本次回归重点,
  82. // 只是用来证明 ErrNotFound 路径没有被修复误伤为 500。
  83. func TestCheckManageAccess_ErrNotFound_StillTreatedAsNoRole(t *testing.T) {
  84. ctrl := gomock.NewController(t)
  85. t.Cleanup(ctrl.Finish)
  86. const targetUserId = int64(43)
  87. const callerDeptId = int64(1)
  88. const targetDeptId = int64(2)
  89. const productCode = "test_product"
  90. mockUser := mocks.NewMockSysUserModel(ctrl)
  91. mockUser.EXPECT().FindOne(gomock.Any(), int64(targetUserId)).
  92. Return(&userModel.SysUser{Id: targetUserId, DeptId: targetDeptId}, nil).AnyTimes()
  93. mockDept := mocks.NewMockSysDeptModel(ctrl)
  94. mockDept.EXPECT().FindOne(gomock.Any(), targetDeptId).
  95. Return(&deptModel.SysDept{Id: targetDeptId, Path: "/1/2/"}, nil).AnyTimes()
  96. mockPM := mocks.NewMockSysProductMemberModel(ctrl)
  97. mockPM.EXPECT().FindOneByProductCodeUserId(gomock.Any(), productCode, int64(targetUserId)).
  98. Return(&memberModel.SysProductMember{
  99. UserId: targetUserId, ProductCode: productCode,
  100. MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled,
  101. }, nil).AnyTimes()
  102. mockRole := mocks.NewMockSysRoleModel(ctrl)
  103. mockRole.EXPECT().
  104. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(targetUserId), productCode).
  105. Return(int64(0), sqlx.ErrNotFound).AnyTimes()
  106. // 审计 H-2:checkPermLevel 现在也会对 caller 做 fresh read。
  107. // 这里构造"caller 同样无角色 → callerNoRole=true → >= 比较由 callerNoRole 决定,结果仍 403"。
  108. mockRole.EXPECT().
  109. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(101), productCode).
  110. Return(int64(0), sqlx.ErrNotFound).AnyTimes()
  111. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  112. User: mockUser, Dept: mockDept, Role: mockRole, ProductMember: mockPM,
  113. })
  114. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  115. UserId: 101,
  116. Username: "l4_caller_no_role",
  117. IsSuperAdmin: false, MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled,
  118. ProductCode: productCode, DeptId: callerDeptId, DeptPath: "/1/",
  119. // sentinel:自己也没有任何角色。
  120. MinPermsLevel: math.MaxInt64,
  121. })
  122. err := CheckManageAccess(ctx, svcCtx, targetUserId, productCode)
  123. require.Error(t, err, "caller 与 target 都 sentinel → >= 比较应拦截")
  124. var ce *response.CodeError
  125. require.True(t, errors.As(err, &ce))
  126. assert.Equal(t, 403, ce.Code(),
  127. "ErrNotFound 正常降级为 sentinel;结果应是业务 403 而非基础设施 500")
  128. }