updateUserDeptScope_audit_test.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. package user
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "math"
  7. "testing"
  8. "time"
  9. "perms-system-server/internal/consts"
  10. "perms-system-server/internal/loaders"
  11. "perms-system-server/internal/middleware"
  12. deptModel "perms-system-server/internal/model/dept"
  13. userModel "perms-system-server/internal/model/user"
  14. "perms-system-server/internal/response"
  15. "perms-system-server/internal/svc"
  16. "perms-system-server/internal/testutil"
  17. "perms-system-server/internal/types"
  18. "github.com/stretchr/testify/assert"
  19. "github.com/stretchr/testify/require"
  20. )
  21. func insertTestDeptForScope(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext, tag, path string) int64 {
  22. t.Helper()
  23. now := time.Now().Unix()
  24. res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{
  25. ParentId: 0, Name: tag + "_" + testutil.UniqueId(), Path: path, Sort: 0,
  26. DeptType: "NORMAL", Remark: "", Status: 1, CreateTime: now, UpdateTime: now,
  27. })
  28. require.NoError(t, err)
  29. id, _ := res.LastInsertId()
  30. return id
  31. }
  32. func insertTestUserWithDept(t *testing.T, ctx context.Context, tag string, deptId int64) int64 {
  33. t.Helper()
  34. now := time.Now().Unix()
  35. return insertTestUserFull(t, ctx, &userModel.SysUser{
  36. Username: "ddu_" + tag + "_" + testutil.UniqueId(),
  37. Password: testutil.HashPassword("pw"),
  38. Nickname: "n",
  39. Avatar: sql.NullString{},
  40. Email: "[email protected]",
  41. Phone: "13800000000",
  42. DeptId: deptId,
  43. IsSuperAdmin: consts.IsSuperAdminNo,
  44. MustChangePassword: 2,
  45. Status: consts.StatusEnabled,
  46. CreateTime: now,
  47. UpdateTime: now,
  48. })
  49. }
  50. // TC-0746: L-F 修复回归 —— DEVELOPER 调用者不得将目标用户的 deptId 调到
  51. // 自己 DeptPath 子树之外的部门。UpdateUser 必须在 req.DeptId 变更时做 Path 前缀校验。
  52. func TestUpdateUser_DeveloperCannotMoveTargetOutsideSubtree(t *testing.T) {
  53. bootstrap := context.Background()
  54. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  55. conn := testutil.GetTestSqlConn()
  56. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "caller", "/100/")
  57. targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "target", "/100/200/")
  58. outsideDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "outside", "/999/")
  59. targetId := insertTestUserWithDept(t, bootstrap, "lf_out", targetDeptId)
  60. mId := insertTestMember(t, svcCtx, "test_product", targetId)
  61. t.Cleanup(func() {
  62. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  63. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  64. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, targetDeptId, outsideDeptId)
  65. })
  66. devCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  67. UserId: 55555, Username: "lf_dev",
  68. IsSuperAdmin: false,
  69. MemberType: consts.MemberTypeDeveloper,
  70. Status: consts.StatusEnabled,
  71. ProductCode: "test_product",
  72. DeptId: callerDeptId,
  73. DeptPath: "/100/",
  74. MinPermsLevel: math.MaxInt64,
  75. })
  76. newDept := outsideDeptId
  77. err := NewUpdateUserLogic(devCtx, svcCtx).UpdateUser(&types.UpdateUserReq{
  78. Id: targetId,
  79. DeptId: &newDept,
  80. })
  81. require.Error(t, err, "调入外部部门应被拒绝")
  82. var ce *response.CodeError
  83. require.True(t, errors.As(err, &ce))
  84. assert.Equal(t, 403, ce.Code())
  85. assert.Contains(t, ce.Error(), "无权将用户调入")
  86. user, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId)
  87. require.NoError(t, err)
  88. assert.Equal(t, targetDeptId, user.DeptId, "被拒绝的请求必须不改动 DB")
  89. }
  90. // TC-0747: L-F 正向回归 —— DEVELOPER 将目标用户调入自己子树下的部门应允许。
  91. func TestUpdateUser_DeveloperCanMoveTargetWithinSubtree(t *testing.T) {
  92. bootstrap := context.Background()
  93. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  94. conn := testutil.GetTestSqlConn()
  95. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "caller_in", "/200/")
  96. srcDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "src_in", "/200/1/")
  97. dstDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "dst_in", "/200/2/")
  98. targetId := insertTestUserWithDept(t, bootstrap, "lf_in", srcDeptId)
  99. mId := insertTestMember(t, svcCtx, "test_product", targetId)
  100. t.Cleanup(func() {
  101. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  102. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  103. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, srcDeptId, dstDeptId)
  104. })
  105. devCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  106. UserId: 66666, Username: "lf_dev_ok",
  107. IsSuperAdmin: false,
  108. MemberType: consts.MemberTypeDeveloper,
  109. Status: consts.StatusEnabled,
  110. ProductCode: "test_product",
  111. DeptId: callerDeptId,
  112. DeptPath: "/200/",
  113. MinPermsLevel: math.MaxInt64,
  114. })
  115. newDept := dstDeptId
  116. require.NoError(t,
  117. NewUpdateUserLogic(devCtx, svcCtx).UpdateUser(&types.UpdateUserReq{
  118. Id: targetId, DeptId: &newDept,
  119. }),
  120. "caller DeptPath 的前缀子部门必须允许调入")
  121. user, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId)
  122. require.NoError(t, err)
  123. assert.Equal(t, dstDeptId, user.DeptId)
  124. }
  125. // TC-0748: L-F —— 产品 ADMIN 调用者被豁免 DeptPath 前缀校验(可跨部门转移)。
  126. func TestUpdateUser_ProductAdminExemptFromSubtreeCheck(t *testing.T) {
  127. bootstrap := context.Background()
  128. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  129. conn := testutil.GetTestSqlConn()
  130. adminDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "admin_home", "/300/")
  131. targetHomeDept := insertTestDeptForScope(t, bootstrap, svcCtx, "target_home", "/400/")
  132. anywhereDept := insertTestDeptForScope(t, bootstrap, svcCtx, "anywhere", "/500/")
  133. targetId := insertTestUserWithDept(t, bootstrap, "lf_admin", targetHomeDept)
  134. mId := insertTestMember(t, svcCtx, "test_product", targetId)
  135. t.Cleanup(func() {
  136. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  137. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  138. testutil.CleanTable(bootstrap, conn, "`sys_dept`", adminDeptId, targetHomeDept, anywhereDept)
  139. })
  140. adminCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  141. UserId: 77777, Username: "lf_admin",
  142. IsSuperAdmin: false, MemberType: consts.MemberTypeAdmin,
  143. Status: consts.StatusEnabled, ProductCode: "test_product",
  144. DeptId: adminDeptId, DeptPath: "/300/", MinPermsLevel: math.MaxInt64,
  145. })
  146. newDept := anywhereDept
  147. require.NoError(t,
  148. NewUpdateUserLogic(adminCtx, svcCtx).UpdateUser(&types.UpdateUserReq{
  149. Id: targetId, DeptId: &newDept,
  150. }),
  151. "产品 ADMIN 在 UpdateUser 的 DeptPath 前缀校验中被豁免")
  152. }