createUserDeptChain_audit_test.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. package user
  2. import (
  3. "context"
  4. "errors"
  5. "math"
  6. "testing"
  7. "time"
  8. "perms-system-server/internal/consts"
  9. "perms-system-server/internal/loaders"
  10. "perms-system-server/internal/middleware"
  11. deptModel "perms-system-server/internal/model/dept"
  12. "perms-system-server/internal/response"
  13. "perms-system-server/internal/svc"
  14. "perms-system-server/internal/testutil"
  15. "perms-system-server/internal/testutil/ctxhelper"
  16. "perms-system-server/internal/types"
  17. "github.com/stretchr/testify/assert"
  18. "github.com/stretchr/testify/require"
  19. )
  20. // ---------------------------------------------------------------------------
  21. // 覆盖目标:审计 M-N4 修复 —— CreateUser 必须做 caller.DeptPath → newDept.Path 前缀校验,
  22. // 并且目标部门必须处于 Enabled(审计 L-N2),非超管 DeptId=0 必须拒绝,
  23. // 避免 Product ADMIN 为"非自己管辖的部门"预埋 admin_* / ops_* 关键用户名并借 AddMember
  24. // 的协同路径挂进产品。
  25. // ---------------------------------------------------------------------------
  26. func callerAdminCtx(callerUserId, deptId int64, deptPath string) context.Context {
  27. return middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  28. UserId: callerUserId,
  29. Username: "prod_admin_caller",
  30. IsSuperAdmin: false,
  31. MemberType: consts.MemberTypeAdmin,
  32. Status: consts.StatusEnabled,
  33. ProductCode: "test_product",
  34. DeptId: deptId,
  35. DeptPath: deptPath,
  36. MinPermsLevel: math.MaxInt64,
  37. })
  38. }
  39. // TC-0994: M-N4 —— 产品 ADMIN 为非自己管辖部门创建用户必须 403。
  40. func TestCreateUser_MN4_AdminCannotCreateOutsideDeptSubtree(t *testing.T) {
  41. bootstrap := context.Background()
  42. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  43. conn := testutil.GetTestSqlConn()
  44. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_caller", "/100/")
  45. outsideDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_outside", "/999/")
  46. t.Cleanup(func() {
  47. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, outsideDeptId)
  48. })
  49. adminCtx := callerAdminCtx(777771, callerDeptId, "/100/")
  50. _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
  51. Username: "mn4_seed_" + testutil.UniqueId(),
  52. Password: "Pass123456",
  53. DeptId: outsideDeptId,
  54. })
  55. require.Error(t, err)
  56. var ce *response.CodeError
  57. require.True(t, errors.As(err, &ce))
  58. assert.Equal(t, 403, ce.Code(),
  59. "M-N4:产品 ADMIN 跨部门树创建用户必须 403,防止占用关键用户名等 AddMember 合谋挂进产品")
  60. assert.Contains(t, ce.Error(), "无权在非自己管辖的部门下创建用户")
  61. }
  62. // TC-0995: M-N4 正向 —— 产品 ADMIN 在自己子树下的部门创建用户放行。
  63. func TestCreateUser_MN4_AdminCanCreateInsideDeptSubtree(t *testing.T) {
  64. bootstrap := context.Background()
  65. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  66. conn := testutil.GetTestSqlConn()
  67. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_ok_caller", "/200/")
  68. childDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_ok_child", "/200/1/")
  69. t.Cleanup(func() {
  70. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, childDeptId)
  71. })
  72. adminCtx := callerAdminCtx(777772, callerDeptId, "/200/")
  73. username := "mn4ok_" + testutil.UniqueId()
  74. resp, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
  75. Username: username,
  76. Password: "Pass123456",
  77. DeptId: childDeptId,
  78. })
  79. require.NoError(t, err)
  80. require.NotNil(t, resp)
  81. t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", resp.Id) })
  82. user, err := svcCtx.SysUserModel.FindOne(bootstrap, resp.Id)
  83. require.NoError(t, err)
  84. assert.Equal(t, username, user.Username)
  85. assert.Equal(t, childDeptId, user.DeptId, "用户必须真实落在指定子部门")
  86. }
  87. // TC-0996: M-N4 —— SuperAdmin 可跨一切部门(含 DeptId=0),继续允许创建系统级账号。
  88. func TestCreateUser_MN4_SuperAdminCanCreateAnywhere(t *testing.T) {
  89. bootstrap := context.Background()
  90. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  91. conn := testutil.GetTestSqlConn()
  92. randomDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_sa", "/1000/")
  93. t.Cleanup(func() {
  94. testutil.CleanTable(bootstrap, conn, "`sys_dept`", randomDeptId)
  95. })
  96. // A) 超管创建在任意部门
  97. usernameDept := "mn4sa_dept_" + testutil.UniqueId()
  98. resp, err := NewCreateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateUser(&types.CreateUserReq{
  99. Username: usernameDept,
  100. Password: "Pass123456",
  101. DeptId: randomDeptId,
  102. })
  103. require.NoError(t, err)
  104. require.NotNil(t, resp)
  105. t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", resp.Id) })
  106. // B) 超管创建 DeptId=0 的系统级账号(历史跨组织账号语义保留)
  107. usernameZero := "mn4sa_zero_" + testutil.UniqueId()
  108. respZero, err := NewCreateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateUser(&types.CreateUserReq{
  109. Username: usernameZero,
  110. Password: "Pass123456",
  111. DeptId: 0,
  112. })
  113. require.NoError(t, err)
  114. require.NotNil(t, respZero)
  115. t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", respZero.Id) })
  116. }
  117. // TC-0997: M-N4 —— 非超管 caller 的 DeptPath 为空时必须 403,不得在部门树外开口。
  118. func TestCreateUser_MN4_EmptyCallerDeptPathRejected(t *testing.T) {
  119. bootstrap := context.Background()
  120. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  121. conn := testutil.GetTestSqlConn()
  122. dstDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_empty", "/500/")
  123. t.Cleanup(func() {
  124. testutil.CleanTable(bootstrap, conn, "`sys_dept`", dstDeptId)
  125. })
  126. // caller 是产品 ADMIN 但历史账号 DeptPath=="" —— 必须 403 "未归属任何部门"
  127. adminCtx := callerAdminCtx(777773, 0, "")
  128. _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
  129. Username: "mn4empty_" + testutil.UniqueId(),
  130. Password: "Pass123456",
  131. DeptId: dstDeptId,
  132. })
  133. require.Error(t, err)
  134. var ce *response.CodeError
  135. require.True(t, errors.As(err, &ce))
  136. assert.Equal(t, 403, ce.Code(),
  137. "M-N4:caller.DeptPath=='' 属于 legacy 账号,fail-close 不允许创建用户")
  138. assert.Contains(t, ce.Error(), "您未归属任何部门")
  139. }
  140. // TC-0998: M-N4 —— 非超管 caller 传 DeptId=0 必须 400(禁止非超管创建"无部门"账号)。
  141. func TestCreateUser_MN4_NonSuperAdminMustSpecifyDept(t *testing.T) {
  142. bootstrap := context.Background()
  143. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  144. conn := testutil.GetTestSqlConn()
  145. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_mustspec", "/300/")
  146. t.Cleanup(func() {
  147. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId)
  148. })
  149. adminCtx := callerAdminCtx(777774, callerDeptId, "/300/")
  150. _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
  151. Username: "mn4must_" + testutil.UniqueId(),
  152. Password: "Pass123456",
  153. DeptId: 0,
  154. })
  155. require.Error(t, err)
  156. var ce *response.CodeError
  157. require.True(t, errors.As(err, &ce))
  158. assert.Equal(t, 400, ce.Code(),
  159. "M-N4:非超管 CreateUser 必须显式指定部门,禁止在部门树外创建账号")
  160. assert.Contains(t, ce.Error(), "必须指定部门")
  161. }
  162. // TC-0999: L-N2 —— 目标部门已停用时 CreateUser 必须 400 "目标部门已停用",与 UpdateDept 闭环。
  163. func TestCreateUser_LN2_TargetDeptDisabled(t *testing.T) {
  164. bootstrap := context.Background()
  165. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  166. conn := testutil.GetTestSqlConn()
  167. now := time.Now().Unix()
  168. disRes, err := svcCtx.SysDeptModel.Insert(bootstrap, &deptModel.SysDept{
  169. ParentId: 0, Name: "ln2_dis_" + testutil.UniqueId(), Path: "/900/", Sort: 0,
  170. DeptType: "NORMAL", Remark: "", Status: 2, CreateTime: now, UpdateTime: now,
  171. })
  172. require.NoError(t, err)
  173. disabledId, _ := disRes.LastInsertId()
  174. t.Cleanup(func() {
  175. testutil.CleanTable(bootstrap, conn, "`sys_dept`", disabledId)
  176. })
  177. // 超管也必须被拒绝:L-N2 的规则针对"所有调用方",防止 disabled 部门被意外重新填人
  178. _, err = NewCreateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateUser(&types.CreateUserReq{
  179. Username: "ln2_" + testutil.UniqueId(),
  180. Password: "Pass123456",
  181. DeptId: disabledId,
  182. })
  183. require.Error(t, err)
  184. var ce *response.CodeError
  185. require.True(t, errors.As(err, &ce))
  186. assert.Equal(t, 400, ce.Code())
  187. assert.Equal(t, "目标部门已停用", ce.Error(),
  188. "L-N2:目标部门 status!=Enabled 必须拒绝,与 UpdateDept 禁用语义闭环")
  189. }