bindRolesEqualLevel_audit_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. package user
  2. import (
  3. "errors"
  4. "testing"
  5. "time"
  6. "perms-system-server/internal/consts"
  7. "perms-system-server/internal/loaders"
  8. userModel "perms-system-server/internal/model/user"
  9. userroleModel "perms-system-server/internal/model/userrole"
  10. "perms-system-server/internal/response"
  11. "perms-system-server/internal/svc"
  12. "perms-system-server/internal/testutil"
  13. "perms-system-server/internal/testutil/ctxhelper"
  14. "perms-system-server/internal/types"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. )
  18. // seedCallerWithRoleLevel 为调用者落地真实 DB 记录(user + role + user_role),
  19. // 保证审计 M-3 修复(GuardRoleLevelAssignable 的 fresh DB 读)生效后,
  20. // FindMinPermsLevelByUserIdAndProductCode 能命中调用者真实的 permsLevel。
  21. // 修复前的测试用假 UserId 即可走通,修复后必须落地真实关系链才能触发越级拦截。
  22. func seedCallerWithRoleLevel(t *testing.T, svcCtx *svc.ServiceContext, productCode string, callerLevel int64) (int64, func()) {
  23. t.Helper()
  24. superCtx := ctxhelper.SuperAdminCtx()
  25. conn := testutil.GetTestSqlConn()
  26. callerUserId := insertTestUserFull(t, superCtx, &userModel.SysUser{
  27. Username: "caller_" + testutil.UniqueId(), Password: testutil.HashPassword("pass"),
  28. Nickname: "caller_seed", DeptId: 0,
  29. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: 2, Status: consts.StatusEnabled,
  30. })
  31. mId := insertTestMember(t, svcCtx, productCode, callerUserId)
  32. roleId := insertTestRoleWithLevel(t, svcCtx, productCode, consts.StatusEnabled, callerLevel)
  33. now := time.Now().Unix()
  34. _, err := svcCtx.SysUserRoleModel.Insert(superCtx, &userroleModel.SysUserRole{
  35. UserId: callerUserId,
  36. RoleId: roleId,
  37. CreateTime: now,
  38. UpdateTime: now,
  39. })
  40. require.NoError(t, err)
  41. cleanup := func() {
  42. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", callerUserId)
  43. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  44. testutil.CleanTable(superCtx, conn, "`sys_user`", callerUserId)
  45. testutil.CleanTable(superCtx, conn, "`sys_role`", roleId)
  46. }
  47. return callerUserId, cleanup
  48. }
  49. // ---------------------------------------------------------------------------
  50. // 覆盖目标:审计 H-3 修复 —— "不能分配与自己同级(或更高)的角色"。
  51. // 修复前代码仅拦 `>` 严格高于,允许 MEMBER 调用者把同级角色分配给别人,继而下一次 BindRoles 时
  52. // 由于同级权限集相同,可用后续 upgrade 路径放大;修复后变为 `<=`(含同级)拦截。
  53. // 本文件作为"同级也必须 403"的契约锚点。
  54. // ---------------------------------------------------------------------------
  55. // TC-0813: H-3 —— MEMBER 调用者不能分配与自己同 permsLevel 的角色。
  56. func TestBindRoles_EqualPermsLevel_Rejected(t *testing.T) {
  57. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  58. conn := testutil.GetTestSqlConn()
  59. superCtx := ctxhelper.SuperAdminCtx()
  60. deptId, deptPath, cleanupDept := setupDeptForCaller(t, svcCtx)
  61. t.Cleanup(cleanupDept)
  62. productCode := "test_product"
  63. username := testutil.UniqueId()
  64. targetUserId := insertTestUserFull(t, superCtx, &userModel.SysUser{
  65. Username: username, Password: testutil.HashPassword("pass"),
  66. Nickname: "tgt_eq", DeptId: deptId,
  67. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: 2, Status: consts.StatusEnabled,
  68. })
  69. mId := insertTestMember(t, svcCtx, productCode, targetUserId)
  70. const callerLevel int64 = 50
  71. sameLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, consts.StatusEnabled, callerLevel)
  72. // M-3 修复后 GuardRoleLevelAssignable 走 DB 强一致读取 caller 的 MinPermsLevel,
  73. // 因此需要在 DB 里为调用者落地真实的 user + role + user_role 关系链。
  74. callerUserId, callerCleanup := seedCallerWithRoleLevel(t, svcCtx, productCode, callerLevel)
  75. t.Cleanup(callerCleanup)
  76. t.Cleanup(func() {
  77. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", targetUserId)
  78. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  79. testutil.CleanTable(superCtx, conn, "`sys_user`", targetUserId)
  80. testutil.CleanTable(superCtx, conn, "`sys_role`", sameLevelRole)
  81. })
  82. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  83. UserId: callerUserId,
  84. Username: "member_eq_level",
  85. IsSuperAdmin: false,
  86. MemberType: consts.MemberTypeMember,
  87. Status: consts.StatusEnabled,
  88. ProductCode: productCode,
  89. DeptId: deptId,
  90. DeptPath: deptPath,
  91. MinPermsLevel: callerLevel,
  92. })
  93. err := NewBindRolesLogic(ctx, svcCtx).BindRoles(&types.BindRolesReq{
  94. UserId: targetUserId,
  95. RoleIds: []int64{sameLevelRole},
  96. })
  97. require.Error(t, err, "H-3 防线:同级角色分配必须被拒绝(含同级)")
  98. var ce *response.CodeError
  99. require.True(t, errors.As(err, &ce))
  100. assert.Equal(t, 403, ce.Code())
  101. assert.Contains(t, ce.Error(), "不能分配权限级别高于自身的角色",
  102. "错误消息应当明确点出'含同级'的拦截语义")
  103. // 同时验证 DB 未产生任何 user-role 关系。
  104. rids, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserIdForProduct(ctx, targetUserId, productCode)
  105. require.NoError(t, err)
  106. assert.Empty(t, rids, "被拒绝的 BindRoles 不得落地任何行")
  107. }