bindRolesLogic_test.go 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021
  1. package user
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. "math"
  9. "perms-system-server/internal/consts"
  10. "perms-system-server/internal/loaders"
  11. roleLogic "perms-system-server/internal/logic/role"
  12. deptModel "perms-system-server/internal/model/dept"
  13. memberModel "perms-system-server/internal/model/productmember"
  14. roleModel "perms-system-server/internal/model/role"
  15. userModel "perms-system-server/internal/model/user"
  16. userroleModel "perms-system-server/internal/model/userrole"
  17. "perms-system-server/internal/response"
  18. "perms-system-server/internal/svc"
  19. "perms-system-server/internal/testutil"
  20. "perms-system-server/internal/testutil/ctxhelper"
  21. "perms-system-server/internal/types"
  22. "sync"
  23. "sync/atomic"
  24. "testing"
  25. "time"
  26. )
  27. func insertTestMember(t *testing.T, svcCtx *svc.ServiceContext, productCode string, userId int64) int64 {
  28. t.Helper()
  29. now := time.Now().Unix()
  30. res, err := svcCtx.SysProductMemberModel.Insert(ctxhelper.SuperAdminCtx(), &memberModel.SysProductMember{
  31. ProductCode: productCode,
  32. UserId: userId,
  33. MemberType: "MEMBER",
  34. Status: 1,
  35. CreateTime: now,
  36. UpdateTime: now,
  37. })
  38. require.NoError(t, err)
  39. id, _ := res.LastInsertId()
  40. return id
  41. }
  42. func insertTestRole(t *testing.T, svcCtx *svc.ServiceContext, productCode string, status int64) int64 {
  43. t.Helper()
  44. now := time.Now().Unix()
  45. res, err := svcCtx.SysRoleModel.Insert(ctxhelper.SuperAdminCtx(), &roleModel.SysRole{
  46. ProductCode: productCode,
  47. Name: "role_" + testutil.UniqueId(),
  48. Status: status,
  49. PermsLevel: 1,
  50. CreateTime: now,
  51. UpdateTime: now,
  52. })
  53. require.NoError(t, err)
  54. id, _ := res.LastInsertId()
  55. return id
  56. }
  57. // TC-0184: 正常绑定
  58. func TestBindRoles_Success(t *testing.T) {
  59. ctx := ctxhelper.SuperAdminCtx()
  60. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  61. conn := testutil.GetTestSqlConn()
  62. username := testutil.UniqueId()
  63. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  64. mId := insertTestMember(t, svcCtx, "test_product", userId)
  65. r1 := insertTestRole(t, svcCtx, "test_product", 1)
  66. r2 := insertTestRole(t, svcCtx, "test_product", 1)
  67. t.Cleanup(func() {
  68. testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId)
  69. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  70. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  71. testutil.CleanTable(ctx, conn, "`sys_role`", r1, r2)
  72. })
  73. logic := NewBindRolesLogic(ctx, svcCtx)
  74. err := logic.BindRoles(&types.BindRolesReq{
  75. UserId: userId,
  76. RoleIds: []int64{r1, r2},
  77. ProductCode: "test_product",
  78. })
  79. require.NoError(t, err)
  80. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  81. require.NoError(t, err)
  82. assert.ElementsMatch(t, []int64{r1, r2}, roleIds)
  83. }
  84. // TC-0185: 用户不存在
  85. func TestBindRoles_UserNotFound(t *testing.T) {
  86. ctx := ctxhelper.SuperAdminCtx()
  87. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  88. logic := NewBindRolesLogic(ctx, svcCtx)
  89. err := logic.BindRoles(&types.BindRolesReq{
  90. UserId: 999999999,
  91. RoleIds: []int64{1},
  92. ProductCode: "any_product",
  93. })
  94. require.Error(t, err)
  95. var codeErr *response.CodeError
  96. require.True(t, errors.As(err, &codeErr))
  97. assert.Equal(t, 404, codeErr.Code())
  98. assert.Equal(t, "用户不存在", codeErr.Error())
  99. }
  100. // TC-0186: 清空角色
  101. func TestBindRoles_EmptyRoleIds_ClearsAll(t *testing.T) {
  102. ctx := ctxhelper.SuperAdminCtx()
  103. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  104. conn := testutil.GetTestSqlConn()
  105. username := testutil.UniqueId()
  106. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  107. mId := insertTestMember(t, svcCtx, "test_product", userId)
  108. r1 := insertTestRole(t, svcCtx, "test_product", 1)
  109. t.Cleanup(func() {
  110. testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId)
  111. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  112. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  113. testutil.CleanTable(ctx, conn, "`sys_role`", r1)
  114. })
  115. logic := NewBindRolesLogic(ctx, svcCtx)
  116. err := logic.BindRoles(&types.BindRolesReq{
  117. UserId: userId,
  118. RoleIds: []int64{r1},
  119. ProductCode: "test_product",
  120. })
  121. require.NoError(t, err)
  122. err = logic.BindRoles(&types.BindRolesReq{
  123. UserId: userId,
  124. RoleIds: []int64{},
  125. ProductCode: "test_product",
  126. })
  127. require.NoError(t, err)
  128. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  129. require.NoError(t, err)
  130. assert.Empty(t, roleIds)
  131. }
  132. // TC-0184: 正常重新绑定
  133. func TestBindRoles_Rebind(t *testing.T) {
  134. ctx := ctxhelper.SuperAdminCtx()
  135. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  136. conn := testutil.GetTestSqlConn()
  137. username := testutil.UniqueId()
  138. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  139. mId := insertTestMember(t, svcCtx, "test_product", userId)
  140. r1 := insertTestRole(t, svcCtx, "test_product", 1)
  141. r2 := insertTestRole(t, svcCtx, "test_product", 1)
  142. r3 := insertTestRole(t, svcCtx, "test_product", 1)
  143. t.Cleanup(func() {
  144. testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId)
  145. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  146. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  147. testutil.CleanTable(ctx, conn, "`sys_role`", r1, r2, r3)
  148. })
  149. logic := NewBindRolesLogic(ctx, svcCtx)
  150. err := logic.BindRoles(&types.BindRolesReq{
  151. UserId: userId,
  152. RoleIds: []int64{r1, r2},
  153. ProductCode: "test_product",
  154. })
  155. require.NoError(t, err)
  156. err = logic.BindRoles(&types.BindRolesReq{
  157. UserId: userId,
  158. RoleIds: []int64{r2, r3},
  159. ProductCode: "test_product",
  160. })
  161. require.NoError(t, err)
  162. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  163. require.NoError(t, err)
  164. assert.ElementsMatch(t, []int64{r2, r3}, roleIds)
  165. }
  166. // TC-0188: 角色不属于当前产品 —— L-R14-2 后文案折叠为 "包含无效的角色ID",
  167. // 禁止以"跨产品"独立文案成为跨产品 roleId 存在性 oracle。
  168. func TestBindRoles_RoleBelongsToOtherProduct(t *testing.T) {
  169. ctx := ctxhelper.SuperAdminCtx()
  170. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  171. conn := testutil.GetTestSqlConn()
  172. username := testutil.UniqueId()
  173. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  174. mId := insertTestMember(t, svcCtx, "test_product", userId)
  175. otherRole := insertTestRole(t, svcCtx, "other_product", 1)
  176. t.Cleanup(func() {
  177. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  178. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  179. testutil.CleanTable(ctx, conn, "`sys_role`", otherRole)
  180. })
  181. logic := NewBindRolesLogic(ctx, svcCtx)
  182. err := logic.BindRoles(&types.BindRolesReq{
  183. UserId: userId,
  184. RoleIds: []int64{otherRole},
  185. ProductCode: "test_product",
  186. })
  187. require.Error(t, err)
  188. var codeErr *response.CodeError
  189. require.True(t, errors.As(err, &codeErr))
  190. assert.Equal(t, 400, codeErr.Code())
  191. assert.Equal(t, "包含无效的角色ID", codeErr.Error(),
  192. "L-R14-2:跨产品 roleId 必须与 不存在/已禁用 共用同一文案,避免枚举 oracle")
  193. }
  194. // TC-0189: 角色已禁用 —— L-R14-2 后折叠为统一文案 "包含无效的角色ID"。
  195. func TestBindRoles_RoleDisabled(t *testing.T) {
  196. ctx := ctxhelper.SuperAdminCtx()
  197. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  198. conn := testutil.GetTestSqlConn()
  199. username := testutil.UniqueId()
  200. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  201. mId := insertTestMember(t, svcCtx, "test_product", userId)
  202. disabledRole := insertTestRole(t, svcCtx, "test_product", 2)
  203. t.Cleanup(func() {
  204. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  205. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  206. testutil.CleanTable(ctx, conn, "`sys_role`", disabledRole)
  207. })
  208. logic := NewBindRolesLogic(ctx, svcCtx)
  209. err := logic.BindRoles(&types.BindRolesReq{
  210. UserId: userId,
  211. RoleIds: []int64{disabledRole},
  212. ProductCode: "test_product",
  213. })
  214. require.Error(t, err)
  215. var codeErr *response.CodeError
  216. require.True(t, errors.As(err, &codeErr))
  217. assert.Equal(t, 400, codeErr.Code())
  218. assert.Equal(t, "包含无效的角色ID", codeErr.Error(),
  219. "L-R14-2:已禁用角色不得以独立文案暴露启停状态")
  220. }
  221. // TC-1127: L-R14-2 三路径(跨产品 / 已禁用 / 不存在)必须返回同一 400 + 同一文案,
  222. // 阻止已通过 CheckManageAccess 的调用方借文案差异枚举他产品 roleId 分布 / 启停状态。
  223. func TestBindRoles_L_R14_2_InvalidIdsUnifiedMessage(t *testing.T) {
  224. ctx := ctxhelper.SuperAdminCtx()
  225. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  226. conn := testutil.GetTestSqlConn()
  227. username := testutil.UniqueId()
  228. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  229. mId := insertTestMember(t, svcCtx, "test_product", userId)
  230. crossRole := insertTestRole(t, svcCtx, "l_r14_2_other_"+testutil.UniqueId(), 1)
  231. disabledRole := insertTestRole(t, svcCtx, "test_product", 2)
  232. t.Cleanup(func() {
  233. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  234. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  235. testutil.CleanTable(ctx, conn, "`sys_role`", crossRole, disabledRole)
  236. })
  237. logic := NewBindRolesLogic(ctx, svcCtx)
  238. cases := []struct {
  239. name string
  240. roleIds []int64
  241. }{
  242. {"cross_product", []int64{crossRole}},
  243. {"disabled", []int64{disabledRole}},
  244. {"missing_id", []int64{9_999_999_999}},
  245. }
  246. var codes []int
  247. var msgs []string
  248. for _, c := range cases {
  249. err := logic.BindRoles(&types.BindRolesReq{UserId: userId, RoleIds: c.roleIds, ProductCode: "test_product"})
  250. require.Error(t, err, c.name)
  251. var ce *response.CodeError
  252. require.True(t, errors.As(err, &ce), c.name)
  253. codes = append(codes, ce.Code())
  254. msgs = append(msgs, ce.Error())
  255. }
  256. for i := 1; i < len(cases); i++ {
  257. assert.Equal(t, codes[0], codes[i],
  258. "L-R14-2:三路径 Code 必须全等,避免成为枚举 oracle")
  259. assert.Equal(t, msgs[0], msgs[i],
  260. "L-R14-2:三路径文案必须全等(实际:%s vs %s)", msgs[0], msgs[i])
  261. }
  262. assert.Equal(t, 400, codes[0])
  263. assert.Equal(t, "包含无效的角色ID", msgs[0])
  264. }
  265. // TC-0190: 角色不存在
  266. func TestBindRoles_RoleNotExists(t *testing.T) {
  267. ctx := ctxhelper.SuperAdminCtx()
  268. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  269. conn := testutil.GetTestSqlConn()
  270. username := testutil.UniqueId()
  271. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  272. mId := insertTestMember(t, svcCtx, "test_product", userId)
  273. t.Cleanup(func() {
  274. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  275. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  276. })
  277. logic := NewBindRolesLogic(ctx, svcCtx)
  278. err := logic.BindRoles(&types.BindRolesReq{
  279. UserId: userId,
  280. RoleIds: []int64{999999999},
  281. ProductCode: "test_product",
  282. })
  283. require.Error(t, err)
  284. var codeErr *response.CodeError
  285. require.True(t, errors.As(err, &codeErr))
  286. assert.Equal(t, 400, codeErr.Code())
  287. assert.Contains(t, codeErr.Error(), "无效的角色ID")
  288. }
  289. func insertTestRoleWithLevel(t *testing.T, svcCtx *svc.ServiceContext, productCode string, status int64, permsLevel int64) int64 {
  290. t.Helper()
  291. now := time.Now().Unix()
  292. res, err := svcCtx.SysRoleModel.Insert(ctxhelper.SuperAdminCtx(), &roleModel.SysRole{
  293. ProductCode: productCode,
  294. Name: "role_" + testutil.UniqueId(),
  295. Status: status,
  296. PermsLevel: permsLevel,
  297. CreateTime: now,
  298. UpdateTime: now,
  299. })
  300. require.NoError(t, err)
  301. id, _ := res.LastInsertId()
  302. return id
  303. }
  304. // setupDeptForCaller 插入一个 dept,同时构造 caller(使用该 dept)与 target(同 dept 下)的环境
  305. // 返回 deptId、deptPath(caller 使用)、cleanup function
  306. func setupDeptForCaller(t *testing.T, svcCtx *svc.ServiceContext) (int64, string, func()) {
  307. t.Helper()
  308. now := time.Now().Unix()
  309. superCtx := ctxhelper.SuperAdminCtx()
  310. res, err := svcCtx.SysDeptModel.Insert(superCtx, &deptModel.SysDept{
  311. Name: "dept_" + testutil.UniqueId(),
  312. ParentId: 0,
  313. Path: "/",
  314. DeptType: consts.DeptTypeNormal,
  315. Status: consts.StatusEnabled,
  316. CreateTime: now,
  317. UpdateTime: now,
  318. })
  319. require.NoError(t, err)
  320. deptId, _ := res.LastInsertId()
  321. // 先占位再用真实 deptId 构造 path:"/{deptId}/"
  322. path := fmt.Sprintf("/%d/", deptId)
  323. _, err = svcCtx.SysDeptModel.Insert(superCtx, &deptModel.SysDept{}) // noop — keep linter happy
  324. _ = err
  325. // 更新 path
  326. dept, _ := svcCtx.SysDeptModel.FindOne(superCtx, deptId)
  327. dept.Path = path
  328. dept.UpdateTime = time.Now().Unix()
  329. require.NoError(t, svcCtx.SysDeptModel.Update(superCtx, dept))
  330. conn := testutil.GetTestSqlConn()
  331. cleanup := func() {
  332. testutil.CleanTable(superCtx, conn, "`sys_dept`", deptId)
  333. }
  334. return deptId, path, cleanup
  335. }
  336. // TC-0208: MEMBER 调用者不能分配权限级别高于自身的角色 (audit 修复后 permsLevel 仅对 MEMBER 生效)
  337. func TestBindRoles_PermsLevelEscalation_Rejected(t *testing.T) {
  338. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  339. conn := testutil.GetTestSqlConn()
  340. superCtx := ctxhelper.SuperAdminCtx()
  341. deptId, deptPath, cleanupDept := setupDeptForCaller(t, svcCtx)
  342. t.Cleanup(cleanupDept)
  343. productCode := "test_product"
  344. // 目标用户:放进 dept 下,MEMBER 产品成员
  345. username := testutil.UniqueId()
  346. targetUserId := insertTestUserFull(t, superCtx, &userModel.SysUser{
  347. Username: username, Password: testutil.HashPassword("pass"),
  348. Nickname: "tgt", DeptId: deptId,
  349. IsSuperAdmin: 2, MustChangePassword: 2, Status: 1,
  350. })
  351. mId := insertTestMember(t, svcCtx, productCode, targetUserId)
  352. highLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1)
  353. // 修复后 GuardRoleLevelAssignable 走 DB 强一致读取 caller 的 MinPermsLevel,
  354. // 因此需要在 DB 里为调用者落地真实的 user + role + user_role 关系链(permsLevel=50)。
  355. callerUserId, callerCleanup := seedCallerWithRoleLevel(t, svcCtx, productCode, 50)
  356. t.Cleanup(callerCleanup)
  357. t.Cleanup(func() {
  358. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", targetUserId)
  359. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  360. testutil.CleanTable(superCtx, conn, "`sys_user`", targetUserId)
  361. testutil.CleanTable(superCtx, conn, "`sys_role`", highLevelRole)
  362. })
  363. // MEMBER 调用者与 target 同 dept,DB 中 MinPermsLevel=50,目标角色 permsLevel=1 → 越级
  364. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  365. UserId: callerUserId,
  366. Username: "member_caller",
  367. IsSuperAdmin: false,
  368. MemberType: consts.MemberTypeMember,
  369. Status: consts.StatusEnabled,
  370. ProductCode: productCode,
  371. DeptId: deptId,
  372. DeptPath: deptPath,
  373. MinPermsLevel: 50,
  374. })
  375. logic := NewBindRolesLogic(ctx, svcCtx)
  376. err := logic.BindRoles(&types.BindRolesReq{
  377. UserId: targetUserId,
  378. RoleIds: []int64{highLevelRole},
  379. ProductCode: productCode,
  380. })
  381. require.Error(t, err)
  382. var ce *response.CodeError
  383. require.True(t, errors.As(err, &ce))
  384. assert.Equal(t, 403, ce.Code())
  385. assert.Contains(t, ce.Error(), "不能分配权限级别高于自身的角色")
  386. }
  387. // TC-0711: ADMIN 调用者(MinPermsLevel=math.MaxInt64)不受 permsLevel 校验约束 (audit 回归)
  388. // 修复前:ADMIN 通过 member_type 获得权限,MinPermsLevel 保持 math.MaxInt64,
  389. //
  390. // r.PermsLevel < math.MaxInt64 必然成立 → ADMIN 无法绑定任何角色。
  391. //
  392. // 修复后:代码显式豁免 ADMIN/DEVELOPER 的 permsLevel 校验。
  393. func TestBindRoles_AdminBypassesPermsLevelCheck(t *testing.T) {
  394. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  395. conn := testutil.GetTestSqlConn()
  396. superCtx := ctxhelper.SuperAdminCtx()
  397. productCode := "test_product"
  398. username := testutil.UniqueId()
  399. userId := insertTestUser(t, superCtx, username, testutil.HashPassword("pass"))
  400. mId := insertTestMember(t, svcCtx, productCode, userId)
  401. lowLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1)
  402. t.Cleanup(func() {
  403. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  404. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  405. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  406. testutil.CleanTable(superCtx, conn, "`sys_role`", lowLevelRole)
  407. })
  408. // 关键:模拟 loader 真实产出——ADMIN 没有自定义角色,MinPermsLevel=math.MaxInt64
  409. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  410. UserId: 999997,
  411. Username: "admin_caller",
  412. IsSuperAdmin: false,
  413. MemberType: consts.MemberTypeAdmin,
  414. Status: consts.StatusEnabled,
  415. ProductCode: productCode,
  416. DeptId: 1,
  417. DeptPath: "/1/",
  418. MinPermsLevel: math.MaxInt64, // 默认 sentinel
  419. })
  420. logic := NewBindRolesLogic(ctx, svcCtx)
  421. err := logic.BindRoles(&types.BindRolesReq{
  422. UserId: userId,
  423. RoleIds: []int64{lowLevelRole},
  424. ProductCode: productCode,
  425. })
  426. require.NoError(t, err, "ADMIN 调用者应当能绑定任意级别的角色")
  427. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  428. require.NoError(t, err)
  429. assert.Contains(t, roleIds, lowLevelRole)
  430. }
  431. // TC-0712: DEVELOPER 调用者同样不受 permsLevel 校验约束 (audit 回归)
  432. func TestBindRoles_DeveloperBypassesPermsLevelCheck(t *testing.T) {
  433. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  434. conn := testutil.GetTestSqlConn()
  435. superCtx := ctxhelper.SuperAdminCtx()
  436. deptId, deptPath, cleanupDept := setupDeptForCaller(t, svcCtx)
  437. t.Cleanup(cleanupDept)
  438. productCode := "test_product"
  439. username := testutil.UniqueId()
  440. userId := insertTestUserFull(t, superCtx, &userModel.SysUser{
  441. Username: username, Password: testutil.HashPassword("pass"),
  442. Nickname: "tgt_dev", DeptId: deptId,
  443. IsSuperAdmin: 2, MustChangePassword: 2, Status: 1,
  444. })
  445. mId := insertTestMember(t, svcCtx, productCode, userId)
  446. lowLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1)
  447. t.Cleanup(func() {
  448. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  449. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  450. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  451. testutil.CleanTable(superCtx, conn, "`sys_role`", lowLevelRole)
  452. })
  453. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  454. UserId: 999996,
  455. Username: "developer_caller",
  456. IsSuperAdmin: false,
  457. MemberType: consts.MemberTypeDeveloper,
  458. Status: consts.StatusEnabled,
  459. ProductCode: productCode,
  460. DeptId: deptId,
  461. DeptPath: deptPath,
  462. MinPermsLevel: math.MaxInt64,
  463. })
  464. logic := NewBindRolesLogic(ctx, svcCtx)
  465. err := logic.BindRoles(&types.BindRolesReq{
  466. UserId: userId,
  467. RoleIds: []int64{lowLevelRole},
  468. ProductCode: productCode,
  469. })
  470. require.NoError(t, err, "DEVELOPER 调用者应当能绑定任意级别的角色")
  471. }
  472. // TC-0713: MinPermsLevel == math.MaxInt64 的 MEMBER 调用者也必须被豁免
  473. // (sentinel 判定路径:既不是 ADMIN/DEVELOPER,也没有角色,此时 r.PermsLevel<MaxInt64 的逐字面比较
  474. //
  475. // 曾经误伤此类 MEMBER;修复后代码用 MinPermsLevel==MaxInt64 做短路)
  476. func TestBindRoles_MemberWithSentinelMinLevel_NotBlocked(t *testing.T) {
  477. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  478. conn := testutil.GetTestSqlConn()
  479. superCtx := ctxhelper.SuperAdminCtx()
  480. productCode := "test_product"
  481. username := testutil.UniqueId()
  482. userId := insertTestUser(t, superCtx, username, testutil.HashPassword("pass"))
  483. mId := insertTestMember(t, svcCtx, productCode, userId)
  484. role := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 100)
  485. t.Cleanup(func() {
  486. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  487. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  488. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  489. testutil.CleanTable(superCtx, conn, "`sys_role`", role)
  490. })
  491. // MEMBER 调用者没有绑定任何启用角色,MinPermsLevel=MaxInt64(sentinel)
  492. // 正式语义:"我自己无级别" → 不应触发越级校验(否则所有无角色 MEMBER 都永远无法分配角色)
  493. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  494. UserId: 999995,
  495. Username: "member_no_role",
  496. IsSuperAdmin: false,
  497. MemberType: consts.MemberTypeMember,
  498. Status: consts.StatusEnabled,
  499. ProductCode: productCode,
  500. DeptId: 1,
  501. DeptPath: "/1/",
  502. MinPermsLevel: math.MaxInt64,
  503. })
  504. logic := NewBindRolesLogic(ctx, svcCtx)
  505. // 注意:业务层早期就会用 `RequireProductAdminFor` 拦住非 ADMIN/SUPER 的调用;此处是为了单独验证
  506. // bindRoles 内部的 permsLevel 分支。实际发生于 ADMIN 通过上层校验但 MemberType 上下文异常时的防御。
  507. // 这里只断言:"sentinel 路径不应报 403 '不能分配权限级别高于自身的角色'"。
  508. err := logic.BindRoles(&types.BindRolesReq{
  509. UserId: userId,
  510. RoleIds: []int64{role},
  511. ProductCode: productCode,
  512. })
  513. // 调用者非 ADMIN,且是 MEMBER,上游会拦 403 "仅ADMIN/超管可绑定角色";
  514. // 此处我们只校验"即使走到 permsLevel 分支,sentinel MinPermsLevel 不应命中"
  515. if err != nil {
  516. var ce *response.CodeError
  517. require.True(t, errors.As(err, &ce))
  518. assert.NotContains(t, ce.Error(), "不能分配权限级别高于自身的角色",
  519. "sentinel MinPermsLevel=math.MaxInt64 不应触发越级错误")
  520. }
  521. }
  522. // TC-0209: 超管可以分配任意权限级别的角色
  523. func TestBindRoles_SuperAdminCanAssignAnyLevel(t *testing.T) {
  524. ctx := ctxhelper.SuperAdminCtx()
  525. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  526. conn := testutil.GetTestSqlConn()
  527. productCode := "test_product"
  528. username := testutil.UniqueId()
  529. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  530. mId := insertTestMember(t, svcCtx, productCode, userId)
  531. highLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1)
  532. t.Cleanup(func() {
  533. testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId)
  534. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  535. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  536. testutil.CleanTable(ctx, conn, "`sys_role`", highLevelRole)
  537. })
  538. logic := NewBindRolesLogic(ctx, svcCtx)
  539. err := logic.BindRoles(&types.BindRolesReq{
  540. UserId: userId,
  541. RoleIds: []int64{highLevelRole},
  542. ProductCode: productCode,
  543. })
  544. require.NoError(t, err)
  545. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  546. require.NoError(t, err)
  547. assert.Contains(t, roleIds, highLevelRole)
  548. }
  549. // TC-0191: 目标用户不是当前产品成员时拒绝绑定角色(修复验证)
  550. func TestBindRoles_NonMemberRejected(t *testing.T) {
  551. ctx := ctxhelper.SuperAdminCtx()
  552. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  553. conn := testutil.GetTestSqlConn()
  554. username := testutil.UniqueId()
  555. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  556. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
  557. logic := NewBindRolesLogic(ctx, svcCtx)
  558. err := logic.BindRoles(&types.BindRolesReq{
  559. UserId: userId,
  560. RoleIds: []int64{},
  561. ProductCode: "test_product",
  562. })
  563. require.Error(t, err)
  564. var codeErr2 *response.CodeError
  565. require.True(t, errors.As(err, &codeErr2))
  566. assert.Equal(t, 400, codeErr2.Code())
  567. assert.Contains(t, codeErr2.Error(), "不是当前产品的成员")
  568. }
  569. func setupBindRolesOrphanFixture(t *testing.T, svcCtx *svc.ServiceContext, productCode string) (
  570. userId, memberId, roleId int64, cleanup func(),
  571. ) {
  572. t.Helper()
  573. superCtx := ctxhelper.SuperAdminCtx()
  574. conn := testutil.GetTestSqlConn()
  575. username := testutil.UniqueId()
  576. userId = insertTestUser(t, superCtx, username, testutil.HashPassword("pass"))
  577. memberId = insertTestMember(t, svcCtx, productCode, userId)
  578. now := time.Now().Unix()
  579. res, err := svcCtx.SysRoleModel.Insert(superCtx, &roleModel.SysRole{
  580. ProductCode: productCode,
  581. Name: "r12_1_" + testutil.UniqueId(),
  582. Status: consts.StatusEnabled,
  583. PermsLevel: 10,
  584. CreateTime: now,
  585. UpdateTime: now,
  586. })
  587. require.NoError(t, err)
  588. roleId, _ = res.LastInsertId()
  589. cleanup = func() {
  590. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  591. testutil.CleanTable(superCtx, conn, "`sys_product_member`", memberId)
  592. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  593. testutil.CleanTable(superCtx, conn, "`sys_role`", roleId)
  594. }
  595. return
  596. }
  597. // TC-1078: BindRoles 和 DeleteRole 并发:终态无孤儿
  598. // 用真实 MySQL + go-zero 事务跑多轮并发,断言每一轮都能落在以下两个合法终态之一:
  599. //
  600. // A) BindRoles 胜出 + DeleteRole 胜出(先后串行):
  601. // - 最常见:BindRoles 先提交,sys_user_role 出现新行;DeleteRole 随后提交,
  602. // 级联 DELETE 把新行一并带走;sys_role[R] 消失、sys_user_role 无 R 行 → 无孤儿。
  603. // B) DeleteRole 先提交 + BindRoles 收 400:
  604. // - sys_role[R] 消失;BindRoles 事务内 LockRolesForShareTx 读不到 → ErrNotFound → 400;
  605. // - sys_user_role 无 R 行 → 无孤儿。
  606. //
  607. // 不允许任何一轮出现:sys_role[R] 不在 + sys_user_role 仍有 (userId, R) —— 这就是 orphan。
  608. func TestBindRoles_Vs_DeleteRole_NoOrphanRows(t *testing.T) {
  609. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  610. conn := testutil.GetTestSqlConn()
  611. superCtx := ctxhelper.SuperAdminCtx()
  612. productCode := "test_product"
  613. const rounds = 6
  614. for round := 0; round < rounds; round++ {
  615. userId, memberId, roleId, cleanup := setupBindRolesOrphanFixture(t, svcCtx, productCode)
  616. _ = memberId
  617. var (
  618. wg sync.WaitGroup
  619. bindErr atomic.Value
  620. delErr atomic.Value
  621. bindOK atomic.Bool
  622. deleteOK atomic.Bool
  623. )
  624. start := make(chan struct{})
  625. wg.Add(2)
  626. go func() {
  627. defer wg.Done()
  628. <-start
  629. err := NewBindRolesLogic(superCtx, svcCtx).BindRoles(&types.BindRolesReq{
  630. UserId: userId,
  631. RoleIds: []int64{roleId},
  632. ProductCode: productCode,
  633. })
  634. if err == nil {
  635. bindOK.Store(true)
  636. } else {
  637. bindErr.Store(err)
  638. }
  639. }()
  640. go func() {
  641. defer wg.Done()
  642. <-start
  643. err := roleLogic.NewDeleteRoleLogic(superCtx, svcCtx).DeleteRole(&types.DeleteRoleReq{
  644. Id: roleId,
  645. })
  646. if err == nil {
  647. deleteOK.Store(true)
  648. } else {
  649. delErr.Store(err)
  650. }
  651. }()
  652. close(start)
  653. wg.Wait()
  654. // 终态:绕过 go-zero cache 直接查 DB,避免 cache 把 DeleteRole 的真实删除遮住造成假阳。
  655. var roleCount, urCount int64
  656. require.NoError(t, conn.QueryRowCtx(context.Background(), &roleCount,
  657. "SELECT COUNT(*) FROM `sys_role` WHERE `id` = ?", roleId))
  658. require.NoError(t, conn.QueryRowCtx(context.Background(), &urCount,
  659. "SELECT COUNT(*) FROM `sys_user_role` WHERE `userId` = ? AND `roleId` = ?", userId, roleId))
  660. // 最严 orphan 判定:sys_role 不在 且 sys_user_role 仍在 → 孤儿
  661. if roleCount == 0 && urCount > 0 {
  662. t.Fatalf(
  663. "(轮次 %d):产生 orphan —— sys_role[%d] 已被 DeleteRole 删除,"+
  664. "但 sys_user_role 仍有 (userId=%d, roleId=%d) 行。bindOK=%v delOK=%v "+
  665. "bindErr=%v delErr=%v", round, roleId, userId, roleId, bindOK.Load(),
  666. deleteOK.Load(), bindErr.Load(), delErr.Load(),
  667. )
  668. }
  669. // 其它合法终态一并回归:至少一端做了有效操作(不能都失败)
  670. if !bindOK.Load() && !deleteOK.Load() {
  671. t.Logf("轮次 %d: bindErr=%v delErr=%v", round, bindErr.Load(), delErr.Load())
  672. t.Fatalf("轮次 %d:两端都失败,不是预期的并发交错(至少 DeleteRole 应成功,"+
  673. "因为它持有 FindOne 之后所有行的独占链路)", round)
  674. }
  675. // L-R14-2 后,所有"角色无效"分支(race_deleted / disabled / cross-product / missing)
  676. // 被统一合并为 400 "包含无效的角色ID",详细 reason 仅落审计日志,避免枚举 oracle。
  677. // 这里的并发 race 本质仍是"角色已不再是合法绑定目标",落到统一文案后即可。
  678. if raw := bindErr.Load(); raw != nil {
  679. var ce *response.CodeError
  680. if errors.As(raw.(error), &ce) {
  681. assert.Equal(t, 400, ce.Code(),
  682. "BindRoles 在 DeleteRole 先成功时必须 400,不得泄漏为 500")
  683. assert.Equal(t, "包含无效的角色ID", ce.Error(),
  684. "L-R14-2:race_deleted 与 other_product / disabled / missing 必须共用同一份"+
  685. "对外文案,否则攻击者能通过文案差分猜出 roleId 的具体状态(枚举 oracle)")
  686. }
  687. }
  688. cleanup()
  689. }
  690. }
  691. func seedCallerWithRoleLevel(t *testing.T, svcCtx *svc.ServiceContext, productCode string, callerLevel int64) (int64, func()) {
  692. t.Helper()
  693. superCtx := ctxhelper.SuperAdminCtx()
  694. conn := testutil.GetTestSqlConn()
  695. callerUserId := insertTestUserFull(t, superCtx, &userModel.SysUser{
  696. Username: "caller_" + testutil.UniqueId(), Password: testutil.HashPassword("pass"),
  697. Nickname: "caller_seed", DeptId: 0,
  698. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: 2, Status: consts.StatusEnabled,
  699. })
  700. mId := insertTestMember(t, svcCtx, productCode, callerUserId)
  701. roleId := insertTestRoleWithLevel(t, svcCtx, productCode, consts.StatusEnabled, callerLevel)
  702. now := time.Now().Unix()
  703. _, err := svcCtx.SysUserRoleModel.Insert(superCtx, &userroleModel.SysUserRole{
  704. UserId: callerUserId,
  705. RoleId: roleId,
  706. CreateTime: now,
  707. UpdateTime: now,
  708. })
  709. require.NoError(t, err)
  710. cleanup := func() {
  711. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", callerUserId)
  712. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  713. testutil.CleanTable(superCtx, conn, "`sys_user`", callerUserId)
  714. testutil.CleanTable(superCtx, conn, "`sys_role`", roleId)
  715. }
  716. return callerUserId, cleanup
  717. }
  718. // ---------------------------------------------------------------------------
  719. // 覆盖目标:"不能分配与自己同级(或更高)的角色"。
  720. // 修复前代码仅拦 `>` 严格高于,允许 MEMBER 调用者把同级角色分配给别人,继而下一次 BindRoles 时
  721. // 由于同级权限集相同,可用后续 upgrade 路径放大;修复后变为 `<=`(含同级)拦截。
  722. // 本文件作为"同级也必须 403"的契约锚点。
  723. // ---------------------------------------------------------------------------
  724. // TC-0813: MEMBER 调用者不能分配与自己同 permsLevel 的角色。
  725. func TestBindRoles_EqualPermsLevel_Rejected(t *testing.T) {
  726. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  727. conn := testutil.GetTestSqlConn()
  728. superCtx := ctxhelper.SuperAdminCtx()
  729. deptId, deptPath, cleanupDept := setupDeptForCaller(t, svcCtx)
  730. t.Cleanup(cleanupDept)
  731. productCode := "test_product"
  732. username := testutil.UniqueId()
  733. targetUserId := insertTestUserFull(t, superCtx, &userModel.SysUser{
  734. Username: username, Password: testutil.HashPassword("pass"),
  735. Nickname: "tgt_eq", DeptId: deptId,
  736. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: 2, Status: consts.StatusEnabled,
  737. })
  738. mId := insertTestMember(t, svcCtx, productCode, targetUserId)
  739. const callerLevel int64 = 50
  740. sameLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, consts.StatusEnabled, callerLevel)
  741. // 修复后 GuardRoleLevelAssignable 走 DB 强一致读取 caller 的 MinPermsLevel,
  742. // 因此需要在 DB 里为调用者落地真实的 user + role + user_role 关系链。
  743. callerUserId, callerCleanup := seedCallerWithRoleLevel(t, svcCtx, productCode, callerLevel)
  744. t.Cleanup(callerCleanup)
  745. t.Cleanup(func() {
  746. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", targetUserId)
  747. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  748. testutil.CleanTable(superCtx, conn, "`sys_user`", targetUserId)
  749. testutil.CleanTable(superCtx, conn, "`sys_role`", sameLevelRole)
  750. })
  751. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  752. UserId: callerUserId,
  753. Username: "member_eq_level",
  754. IsSuperAdmin: false,
  755. MemberType: consts.MemberTypeMember,
  756. Status: consts.StatusEnabled,
  757. ProductCode: productCode,
  758. DeptId: deptId,
  759. DeptPath: deptPath,
  760. MinPermsLevel: callerLevel,
  761. })
  762. err := NewBindRolesLogic(ctx, svcCtx).BindRoles(&types.BindRolesReq{
  763. UserId: targetUserId,
  764. RoleIds: []int64{sameLevelRole},
  765. ProductCode: productCode,
  766. })
  767. require.Error(t, err, "同级角色分配必须被拒绝(含同级)")
  768. var ce *response.CodeError
  769. require.True(t, errors.As(err, &ce))
  770. assert.Equal(t, 403, ce.Code())
  771. assert.Contains(t, ce.Error(), "不能分配权限级别高于自身的角色",
  772. "错误消息应当明确点出'含同级'的拦截语义")
  773. // 同时验证 DB 未产生任何 user-role 关系。
  774. rids, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserIdForProduct(ctx, targetUserId, productCode)
  775. require.NoError(t, err)
  776. assert.Empty(t, rids, "被拒绝的 BindRoles 不得落地任何行")
  777. }
  778. // TC-1102: 非超管 caller + 空 MemberType + 不存在的 userId —— 必须 403 "缺少产品成员上下文",
  779. // 而不是 404 "用户不存在"。L-R13-1 闸的目的就是消除通过 BindRoles 返回差异(403 vs 404)
  780. // 枚举 sys_user 行是否存在的 oracle。
  781. func TestBindRoles_L_R13_1_EmptyMemberTypeForbidsBeforeUserLookup(t *testing.T) {
  782. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  783. // caller:未登录到任何产品(MemberType="" 且非超管),对任意不存在的 userId 都必须 403。
  784. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  785. UserId: 10000000001,
  786. Username: "ghost_member",
  787. IsSuperAdmin: false,
  788. MemberType: "",
  789. Status: consts.StatusEnabled,
  790. ProductCode: "test_product",
  791. })
  792. err := NewBindRolesLogic(ctx, svcCtx).BindRoles(&types.BindRolesReq{
  793. UserId: 999999999,
  794. RoleIds: []int64{1},
  795. })
  796. require.Error(t, err)
  797. var ce *response.CodeError
  798. require.True(t, errors.As(err, &ce))
  799. assert.Equal(t, 403, ce.Code(),
  800. "L-R13-1:非超管且无产品成员上下文时必须 403,不得返回 404 泄漏 userId 存在性")
  801. assert.Equal(t, "缺少产品成员上下文", ce.Error())
  802. }
  803. // TC-1103: 超管不传 productCode → 400 "必须指定产品编码"(新增前置校验)。
  804. // 超管 JWT 中 productCode 为空,必须通过 req.ProductCode 显式传入;不传则提前 400,不再穿透到 FindOne。
  805. func TestBindRoles_L_R13_1_SuperAdminWithEmptyMemberTypeStillProceeds(t *testing.T) {
  806. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  807. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  808. UserId: 1,
  809. Username: "super_no_member_ctx",
  810. IsSuperAdmin: true,
  811. MemberType: "",
  812. Status: consts.StatusEnabled,
  813. ProductCode: "test_product",
  814. })
  815. err := NewBindRolesLogic(ctx, svcCtx).BindRoles(&types.BindRolesReq{
  816. UserId: 999999999,
  817. RoleIds: []int64{1},
  818. // ProductCode 不传
  819. })
  820. require.Error(t, err)
  821. var ce *response.CodeError
  822. require.True(t, errors.As(err, &ce))
  823. assert.Equal(t, 400, ce.Code(),
  824. "超管不传 productCode 应被前置校验拦截返回 400,不再穿透到 FindOne")
  825. assert.Equal(t, "必须指定产品编码", ce.Error())
  826. }
  827. // TC-1265: 非超管(ADMIN)传入 req.ProductCode 指向其他产品时,该字段必须被忽略,
  828. // 始终使用 JWT context 中的 productCode,不允许跨产品操作。
  829. // 安全约束:若非超管能通过 req.ProductCode 切换产品,则可绕过 CheckManageAccess 的产品隔离。
  830. func TestBindRoles_NonSuperAdmin_ReqProductCodeIgnored(t *testing.T) {
  831. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  832. conn := testutil.GetTestSqlConn()
  833. superCtx := ctxhelper.SuperAdminCtx()
  834. productCode := "test_product"
  835. username := testutil.UniqueId()
  836. userId := insertTestUser(t, superCtx, username, testutil.HashPassword("pass"))
  837. mId := insertTestMember(t, svcCtx, productCode, userId)
  838. role := insertTestRole(t, svcCtx, productCode, consts.StatusEnabled)
  839. t.Cleanup(func() {
  840. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  841. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  842. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  843. testutil.CleanTable(superCtx, conn, "`sys_role`", role)
  844. })
  845. // ADMIN caller 的 JWT context productCode = "test_product",
  846. // 请求体传入 "other_product"——非超管时该字段必须被忽略。
  847. ctx := ctxhelper.AdminCtx(productCode)
  848. err := NewBindRolesLogic(ctx, svcCtx).BindRoles(&types.BindRolesReq{
  849. UserId: userId,
  850. RoleIds: []int64{role},
  851. ProductCode: "other_product", // 非超管时此字段应被忽略
  852. })
  853. require.NoError(t, err, "非超管传入其他产品的 productCode 应被忽略,仍按 JWT context 产品操作")
  854. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  855. require.NoError(t, err)
  856. assert.Contains(t, roleIds, role, "角色应成功绑定到 JWT context 对应的产品下")
  857. }
  858. // TC-1266: 非超管(ADMIN)不传 req.ProductCode,使用 JWT context 中的 productCode 正常绑定。
  859. func TestBindRoles_NonSuperAdmin_UsesCtxProductCode(t *testing.T) {
  860. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  861. conn := testutil.GetTestSqlConn()
  862. superCtx := ctxhelper.SuperAdminCtx()
  863. productCode := "test_product"
  864. username := testutil.UniqueId()
  865. userId := insertTestUser(t, superCtx, username, testutil.HashPassword("pass"))
  866. mId := insertTestMember(t, svcCtx, productCode, userId)
  867. role := insertTestRole(t, svcCtx, productCode, consts.StatusEnabled)
  868. t.Cleanup(func() {
  869. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  870. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  871. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  872. testutil.CleanTable(superCtx, conn, "`sys_role`", role)
  873. })
  874. ctx := ctxhelper.AdminCtx(productCode)
  875. err := NewBindRolesLogic(ctx, svcCtx).BindRoles(&types.BindRolesReq{
  876. UserId: userId,
  877. RoleIds: []int64{role},
  878. // ProductCode 不传,应自动使用 JWT context 中的 "test_product"
  879. })
  880. require.NoError(t, err)
  881. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  882. require.NoError(t, err)
  883. assert.Contains(t, roleIds, role)
  884. }