bindRolesLogic_test.go 37 KB

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