updateUserDeptZero_audit_test.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package user
  2. import (
  3. "context"
  4. "errors"
  5. "math"
  6. "testing"
  7. "perms-system-server/internal/consts"
  8. "perms-system-server/internal/loaders"
  9. "perms-system-server/internal/middleware"
  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. // ---------------------------------------------------------------------------
  19. // 覆盖目标:审计 H-4 修复 —— "把用户移出部门树(deptId=0)" 的破坏组织结构操作,
  20. // 仅能由 SuperAdmin 或产品 ADMIN 执行。DEVELOPER / MEMBER 执行必须 403,并且 DB 不得发生变更。
  21. // 修复前这是横向越权点:下级把上级拉出部门树后,后续 checkDeptHierarchy 对该目标彻底失效。
  22. // ---------------------------------------------------------------------------
  23. // TC-0814: H-4 —— DEVELOPER 调用者执行 deptId=0 的 UpdateUser 必须 403,且 target.DeptId 不动。
  24. func TestUpdateUser_DeveloperCannotMoveTargetOutOfDept(t *testing.T) {
  25. bootstrap := context.Background()
  26. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  27. conn := testutil.GetTestSqlConn()
  28. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_caller_dev", "/700/")
  29. targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_target_dev", "/700/1/")
  30. targetId := insertTestUserWithDept(t, bootstrap, "h4_dev", targetDeptId)
  31. mId := insertTestMember(t, svcCtx, "test_product", targetId)
  32. t.Cleanup(func() {
  33. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  34. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  35. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, targetDeptId)
  36. })
  37. devCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  38. UserId: 88881,
  39. Username: "h4_dev",
  40. IsSuperAdmin: false,
  41. MemberType: consts.MemberTypeDeveloper,
  42. Status: consts.StatusEnabled,
  43. ProductCode: "test_product",
  44. DeptId: callerDeptId,
  45. DeptPath: "/700/",
  46. MinPermsLevel: math.MaxInt64,
  47. })
  48. zero := int64(0)
  49. err := NewUpdateUserLogic(devCtx, svcCtx).UpdateUser(&types.UpdateUserReq{
  50. Id: targetId,
  51. DeptId: &zero,
  52. })
  53. require.Error(t, err, "H-4:DEVELOPER 不得把目标移出部门树")
  54. var ce *response.CodeError
  55. require.True(t, errors.As(err, &ce))
  56. assert.Equal(t, 403, ce.Code())
  57. assert.Contains(t, ce.Error(), "仅超级管理员或产品管理员可将用户移出部门")
  58. u, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId)
  59. require.NoError(t, err)
  60. assert.Equal(t, targetDeptId, u.DeptId, "被拒绝的请求对 DB 零副作用")
  61. }
  62. // TC-0815: H-4 —— MEMBER 调用者同理被拒(即便是修改自身的其他字段也不能顺手把自己移出部门)。
  63. // 用户修改自身时,路由层 if caller.UserId == req.Id 分支只拦 DeptId != nil/Status != 0;
  64. // 但修改他人为 deptId=0 的分支仍必须 403,以防任何下级调用者漂白组织结构。
  65. func TestUpdateUser_MemberCannotMoveOtherOutOfDept(t *testing.T) {
  66. bootstrap := context.Background()
  67. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  68. conn := testutil.GetTestSqlConn()
  69. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_member_caller", "/800/")
  70. targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_member_target", "/800/1/")
  71. targetId := insertTestUserWithDept(t, bootstrap, "h4_mem", targetDeptId)
  72. mId := insertTestMember(t, svcCtx, "test_product", targetId)
  73. t.Cleanup(func() {
  74. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  75. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  76. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, targetDeptId)
  77. })
  78. memberCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  79. UserId: 88882,
  80. Username: "h4_mem",
  81. IsSuperAdmin: false,
  82. MemberType: consts.MemberTypeMember,
  83. Status: consts.StatusEnabled,
  84. ProductCode: "test_product",
  85. DeptId: callerDeptId,
  86. DeptPath: "/800/",
  87. MinPermsLevel: 10,
  88. })
  89. zero := int64(0)
  90. err := NewUpdateUserLogic(memberCtx, svcCtx).UpdateUser(&types.UpdateUserReq{
  91. Id: targetId,
  92. DeptId: &zero,
  93. })
  94. require.Error(t, err, "H-4:MEMBER 更不得移出他人")
  95. var ce *response.CodeError
  96. require.True(t, errors.As(err, &ce))
  97. assert.Equal(t, 403, ce.Code())
  98. u, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId)
  99. require.NoError(t, err)
  100. assert.Equal(t, targetDeptId, u.DeptId)
  101. }
  102. // TC-0816: H-4 —— 产品 ADMIN 有权将他人移出部门(功能不应被修复路径误伤)。
  103. func TestUpdateUser_ProductAdminCanMoveTargetOutOfDept(t *testing.T) {
  104. bootstrap := context.Background()
  105. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  106. conn := testutil.GetTestSqlConn()
  107. adminDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_admin", "/900/")
  108. targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_admin_target", "/900/1/")
  109. targetId := insertTestUserWithDept(t, bootstrap, "h4_admin_tgt", targetDeptId)
  110. mId := insertTestMember(t, svcCtx, "test_product", targetId)
  111. t.Cleanup(func() {
  112. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  113. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  114. testutil.CleanTable(bootstrap, conn, "`sys_dept`", adminDeptId, targetDeptId)
  115. })
  116. adminCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  117. UserId: 88883, Username: "h4_admin",
  118. IsSuperAdmin: false, MemberType: consts.MemberTypeAdmin,
  119. Status: consts.StatusEnabled, ProductCode: "test_product",
  120. DeptId: adminDeptId, DeptPath: "/900/", MinPermsLevel: math.MaxInt64,
  121. })
  122. zero := int64(0)
  123. require.NoError(t,
  124. NewUpdateUserLogic(adminCtx, svcCtx).UpdateUser(&types.UpdateUserReq{
  125. Id: targetId, DeptId: &zero,
  126. }),
  127. "产品 ADMIN 必须仍能执行 deptId=0 的合法运维操作")
  128. u, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId)
  129. require.NoError(t, err)
  130. assert.Equal(t, int64(0), u.DeptId, "ADMIN 的合法 deptId=0 操作必须落盘")
  131. }
  132. // TC-0817: H-4 —— SuperAdmin 有权将他人移出部门(豁免路径)。
  133. func TestUpdateUser_SuperAdminCanMoveTargetOutOfDept(t *testing.T) {
  134. bootstrap := context.Background()
  135. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  136. conn := testutil.GetTestSqlConn()
  137. targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_sa_target", "/950/")
  138. targetId := insertTestUserWithDept(t, bootstrap, "h4_sa_tgt", targetDeptId)
  139. mId := insertTestMember(t, svcCtx, "test_product", targetId)
  140. t.Cleanup(func() {
  141. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  142. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  143. testutil.CleanTable(bootstrap, conn, "`sys_dept`", targetDeptId)
  144. })
  145. zero := int64(0)
  146. require.NoError(t,
  147. NewUpdateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).UpdateUser(&types.UpdateUserReq{
  148. Id: targetId, DeptId: &zero,
  149. }),
  150. "SuperAdmin 的 deptId=0 操作是合法的顶层运维")
  151. u, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId)
  152. require.NoError(t, err)
  153. assert.Equal(t, int64(0), u.DeptId)
  154. }