bindRolesLogic_test.go 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020
  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: 超管不传 productCode → 400 "必须指定产品编码"(新增前置校验)。
  803. // 超管 JWT 中 productCode 为空,必须通过 req.ProductCode 显式传入;不传则提前 400,不再穿透到 FindOne。
  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. // ProductCode 不传
  818. })
  819. require.Error(t, err)
  820. var ce *response.CodeError
  821. require.True(t, errors.As(err, &ce))
  822. assert.Equal(t, 400, ce.Code(),
  823. "超管不传 productCode 应被前置校验拦截返回 400,不再穿透到 FindOne")
  824. assert.Equal(t, "必须指定产品编码", ce.Error())
  825. }
  826. // TC-1265: 非超管(ADMIN)传入 req.ProductCode 指向其他产品时,该字段必须被忽略,
  827. // 始终使用 JWT context 中的 productCode,不允许跨产品操作。
  828. // 安全约束:若非超管能通过 req.ProductCode 切换产品,则可绕过 CheckManageAccess 的产品隔离。
  829. func TestBindRoles_NonSuperAdmin_ReqProductCodeIgnored(t *testing.T) {
  830. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  831. conn := testutil.GetTestSqlConn()
  832. superCtx := ctxhelper.SuperAdminCtx()
  833. productCode := "test_product"
  834. username := testutil.UniqueId()
  835. userId := insertTestUser(t, superCtx, username, testutil.HashPassword("pass"))
  836. mId := insertTestMember(t, svcCtx, productCode, userId)
  837. role := insertTestRole(t, svcCtx, productCode, consts.StatusEnabled)
  838. t.Cleanup(func() {
  839. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  840. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  841. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  842. testutil.CleanTable(superCtx, conn, "`sys_role`", role)
  843. })
  844. // ADMIN caller 的 JWT context productCode = "test_product",
  845. // 请求体传入 "other_product"——非超管时该字段必须被忽略。
  846. ctx := ctxhelper.AdminCtx(productCode)
  847. err := NewBindRolesLogic(ctx, svcCtx).BindRoles(&types.BindRolesReq{
  848. UserId: userId,
  849. RoleIds: []int64{role},
  850. ProductCode: "other_product", // 非超管时此字段应被忽略
  851. })
  852. require.NoError(t, err, "非超管传入其他产品的 productCode 应被忽略,仍按 JWT context 产品操作")
  853. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  854. require.NoError(t, err)
  855. assert.Contains(t, roleIds, role, "角色应成功绑定到 JWT context 对应的产品下")
  856. }
  857. // TC-1266: 非超管(ADMIN)不传 req.ProductCode,使用 JWT context 中的 productCode 正常绑定。
  858. func TestBindRoles_NonSuperAdmin_UsesCtxProductCode(t *testing.T) {
  859. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  860. conn := testutil.GetTestSqlConn()
  861. superCtx := ctxhelper.SuperAdminCtx()
  862. productCode := "test_product"
  863. username := testutil.UniqueId()
  864. userId := insertTestUser(t, superCtx, username, testutil.HashPassword("pass"))
  865. mId := insertTestMember(t, svcCtx, productCode, userId)
  866. role := insertTestRole(t, svcCtx, productCode, consts.StatusEnabled)
  867. t.Cleanup(func() {
  868. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  869. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  870. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  871. testutil.CleanTable(superCtx, conn, "`sys_role`", role)
  872. })
  873. ctx := ctxhelper.AdminCtx(productCode)
  874. err := NewBindRolesLogic(ctx, svcCtx).BindRoles(&types.BindRolesReq{
  875. UserId: userId,
  876. RoleIds: []int64{role},
  877. // ProductCode 不传,应自动使用 JWT context 中的 "test_product"
  878. })
  879. require.NoError(t, err)
  880. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  881. require.NoError(t, err)
  882. assert.Contains(t, roleIds, role)
  883. }