bindRolesLogic_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. package user
  2. import (
  3. "errors"
  4. "fmt"
  5. "math"
  6. "testing"
  7. "time"
  8. "perms-system-server/internal/consts"
  9. "perms-system-server/internal/loaders"
  10. deptModel "perms-system-server/internal/model/dept"
  11. memberModel "perms-system-server/internal/model/productmember"
  12. roleModel "perms-system-server/internal/model/role"
  13. userModel "perms-system-server/internal/model/user"
  14. "perms-system-server/internal/response"
  15. "perms-system-server/internal/svc"
  16. "perms-system-server/internal/testutil"
  17. "perms-system-server/internal/testutil/ctxhelper"
  18. "perms-system-server/internal/types"
  19. "github.com/stretchr/testify/assert"
  20. "github.com/stretchr/testify/require"
  21. )
  22. func insertTestMember(t *testing.T, svcCtx *svc.ServiceContext, productCode string, userId int64) int64 {
  23. t.Helper()
  24. now := time.Now().Unix()
  25. res, err := svcCtx.SysProductMemberModel.Insert(ctxhelper.SuperAdminCtx(), &memberModel.SysProductMember{
  26. ProductCode: productCode,
  27. UserId: userId,
  28. MemberType: "MEMBER",
  29. Status: 1,
  30. CreateTime: now,
  31. UpdateTime: now,
  32. })
  33. require.NoError(t, err)
  34. id, _ := res.LastInsertId()
  35. return id
  36. }
  37. func insertTestRole(t *testing.T, svcCtx *svc.ServiceContext, productCode string, status int64) int64 {
  38. t.Helper()
  39. now := time.Now().Unix()
  40. res, err := svcCtx.SysRoleModel.Insert(ctxhelper.SuperAdminCtx(), &roleModel.SysRole{
  41. ProductCode: productCode,
  42. Name: "role_" + testutil.UniqueId(),
  43. Status: status,
  44. PermsLevel: 1,
  45. CreateTime: now,
  46. UpdateTime: now,
  47. })
  48. require.NoError(t, err)
  49. id, _ := res.LastInsertId()
  50. return id
  51. }
  52. // TC-0184: 正常绑定
  53. func TestBindRoles_Success(t *testing.T) {
  54. ctx := ctxhelper.SuperAdminCtx()
  55. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  56. conn := testutil.GetTestSqlConn()
  57. username := testutil.UniqueId()
  58. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  59. mId := insertTestMember(t, svcCtx, "test_product", userId)
  60. r1 := insertTestRole(t, svcCtx, "test_product", 1)
  61. r2 := insertTestRole(t, svcCtx, "test_product", 1)
  62. t.Cleanup(func() {
  63. testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId)
  64. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  65. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  66. testutil.CleanTable(ctx, conn, "`sys_role`", r1, r2)
  67. })
  68. logic := NewBindRolesLogic(ctx, svcCtx)
  69. err := logic.BindRoles(&types.BindRolesReq{
  70. UserId: userId,
  71. RoleIds: []int64{r1, r2},
  72. })
  73. require.NoError(t, err)
  74. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  75. require.NoError(t, err)
  76. assert.ElementsMatch(t, []int64{r1, r2}, roleIds)
  77. }
  78. // TC-0185: 用户不存在
  79. func TestBindRoles_UserNotFound(t *testing.T) {
  80. ctx := ctxhelper.SuperAdminCtx()
  81. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  82. logic := NewBindRolesLogic(ctx, svcCtx)
  83. err := logic.BindRoles(&types.BindRolesReq{
  84. UserId: 999999999,
  85. RoleIds: []int64{1},
  86. })
  87. require.Error(t, err)
  88. var codeErr *response.CodeError
  89. require.True(t, errors.As(err, &codeErr))
  90. assert.Equal(t, 404, codeErr.Code())
  91. assert.Equal(t, "用户不存在", codeErr.Error())
  92. }
  93. // TC-0186: 清空角色
  94. func TestBindRoles_EmptyRoleIds_ClearsAll(t *testing.T) {
  95. ctx := ctxhelper.SuperAdminCtx()
  96. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  97. conn := testutil.GetTestSqlConn()
  98. username := testutil.UniqueId()
  99. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  100. mId := insertTestMember(t, svcCtx, "test_product", userId)
  101. r1 := insertTestRole(t, svcCtx, "test_product", 1)
  102. t.Cleanup(func() {
  103. testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId)
  104. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  105. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  106. testutil.CleanTable(ctx, conn, "`sys_role`", r1)
  107. })
  108. logic := NewBindRolesLogic(ctx, svcCtx)
  109. err := logic.BindRoles(&types.BindRolesReq{
  110. UserId: userId,
  111. RoleIds: []int64{r1},
  112. })
  113. require.NoError(t, err)
  114. err = logic.BindRoles(&types.BindRolesReq{
  115. UserId: userId,
  116. RoleIds: []int64{},
  117. })
  118. require.NoError(t, err)
  119. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  120. require.NoError(t, err)
  121. assert.Empty(t, roleIds)
  122. }
  123. // TC-0184: 正常重新绑定
  124. func TestBindRoles_Rebind(t *testing.T) {
  125. ctx := ctxhelper.SuperAdminCtx()
  126. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  127. conn := testutil.GetTestSqlConn()
  128. username := testutil.UniqueId()
  129. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  130. mId := insertTestMember(t, svcCtx, "test_product", userId)
  131. r1 := insertTestRole(t, svcCtx, "test_product", 1)
  132. r2 := insertTestRole(t, svcCtx, "test_product", 1)
  133. r3 := insertTestRole(t, svcCtx, "test_product", 1)
  134. t.Cleanup(func() {
  135. testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId)
  136. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  137. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  138. testutil.CleanTable(ctx, conn, "`sys_role`", r1, r2, r3)
  139. })
  140. logic := NewBindRolesLogic(ctx, svcCtx)
  141. err := logic.BindRoles(&types.BindRolesReq{
  142. UserId: userId,
  143. RoleIds: []int64{r1, r2},
  144. })
  145. require.NoError(t, err)
  146. err = logic.BindRoles(&types.BindRolesReq{
  147. UserId: userId,
  148. RoleIds: []int64{r2, r3},
  149. })
  150. require.NoError(t, err)
  151. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  152. require.NoError(t, err)
  153. assert.ElementsMatch(t, []int64{r2, r3}, roleIds)
  154. }
  155. // TC-0188: 角色不属于当前产品
  156. func TestBindRoles_RoleBelongsToOtherProduct(t *testing.T) {
  157. ctx := ctxhelper.SuperAdminCtx()
  158. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  159. conn := testutil.GetTestSqlConn()
  160. username := testutil.UniqueId()
  161. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  162. mId := insertTestMember(t, svcCtx, "test_product", userId)
  163. otherRole := insertTestRole(t, svcCtx, "other_product", 1)
  164. t.Cleanup(func() {
  165. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  166. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  167. testutil.CleanTable(ctx, conn, "`sys_role`", otherRole)
  168. })
  169. logic := NewBindRolesLogic(ctx, svcCtx)
  170. err := logic.BindRoles(&types.BindRolesReq{
  171. UserId: userId,
  172. RoleIds: []int64{otherRole},
  173. })
  174. require.Error(t, err)
  175. var codeErr *response.CodeError
  176. require.True(t, errors.As(err, &codeErr))
  177. assert.Equal(t, 400, codeErr.Code())
  178. assert.Contains(t, codeErr.Error(), "其他产品的角色")
  179. }
  180. // TC-0189: 角色已禁用
  181. func TestBindRoles_RoleDisabled(t *testing.T) {
  182. ctx := ctxhelper.SuperAdminCtx()
  183. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  184. conn := testutil.GetTestSqlConn()
  185. username := testutil.UniqueId()
  186. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  187. mId := insertTestMember(t, svcCtx, "test_product", userId)
  188. disabledRole := insertTestRole(t, svcCtx, "test_product", 2)
  189. t.Cleanup(func() {
  190. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  191. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  192. testutil.CleanTable(ctx, conn, "`sys_role`", disabledRole)
  193. })
  194. logic := NewBindRolesLogic(ctx, svcCtx)
  195. err := logic.BindRoles(&types.BindRolesReq{
  196. UserId: userId,
  197. RoleIds: []int64{disabledRole},
  198. })
  199. require.Error(t, err)
  200. var codeErr *response.CodeError
  201. require.True(t, errors.As(err, &codeErr))
  202. assert.Equal(t, 400, codeErr.Code())
  203. assert.Contains(t, codeErr.Error(), "已禁用的角色")
  204. }
  205. // TC-0190: 角色不存在
  206. func TestBindRoles_RoleNotExists(t *testing.T) {
  207. ctx := ctxhelper.SuperAdminCtx()
  208. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  209. conn := testutil.GetTestSqlConn()
  210. username := testutil.UniqueId()
  211. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  212. mId := insertTestMember(t, svcCtx, "test_product", userId)
  213. t.Cleanup(func() {
  214. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  215. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  216. })
  217. logic := NewBindRolesLogic(ctx, svcCtx)
  218. err := logic.BindRoles(&types.BindRolesReq{
  219. UserId: userId,
  220. RoleIds: []int64{999999999},
  221. })
  222. require.Error(t, err)
  223. var codeErr *response.CodeError
  224. require.True(t, errors.As(err, &codeErr))
  225. assert.Equal(t, 400, codeErr.Code())
  226. assert.Contains(t, codeErr.Error(), "无效的角色ID")
  227. }
  228. func insertTestRoleWithLevel(t *testing.T, svcCtx *svc.ServiceContext, productCode string, status int64, permsLevel int64) int64 {
  229. t.Helper()
  230. now := time.Now().Unix()
  231. res, err := svcCtx.SysRoleModel.Insert(ctxhelper.SuperAdminCtx(), &roleModel.SysRole{
  232. ProductCode: productCode,
  233. Name: "role_" + testutil.UniqueId(),
  234. Status: status,
  235. PermsLevel: permsLevel,
  236. CreateTime: now,
  237. UpdateTime: now,
  238. })
  239. require.NoError(t, err)
  240. id, _ := res.LastInsertId()
  241. return id
  242. }
  243. // setupDeptForCaller 插入一个 dept,同时构造 caller(使用该 dept)与 target(同 dept 下)的环境
  244. // 返回 deptId、deptPath(caller 使用)、cleanup function
  245. func setupDeptForCaller(t *testing.T, svcCtx *svc.ServiceContext) (int64, string, func()) {
  246. t.Helper()
  247. now := time.Now().Unix()
  248. superCtx := ctxhelper.SuperAdminCtx()
  249. res, err := svcCtx.SysDeptModel.Insert(superCtx, &deptModel.SysDept{
  250. Name: "dept_" + testutil.UniqueId(),
  251. ParentId: 0,
  252. Path: "/",
  253. DeptType: consts.DeptTypeNormal,
  254. Status: consts.StatusEnabled,
  255. CreateTime: now,
  256. UpdateTime: now,
  257. })
  258. require.NoError(t, err)
  259. deptId, _ := res.LastInsertId()
  260. // 先占位再用真实 deptId 构造 path:"/{deptId}/"
  261. path := fmt.Sprintf("/%d/", deptId)
  262. _, err = svcCtx.SysDeptModel.Insert(superCtx, &deptModel.SysDept{}) // noop — keep linter happy
  263. _ = err
  264. // 更新 path
  265. dept, _ := svcCtx.SysDeptModel.FindOne(superCtx, deptId)
  266. dept.Path = path
  267. dept.UpdateTime = time.Now().Unix()
  268. require.NoError(t, svcCtx.SysDeptModel.Update(superCtx, dept))
  269. conn := testutil.GetTestSqlConn()
  270. cleanup := func() {
  271. testutil.CleanTable(superCtx, conn, "`sys_dept`", deptId)
  272. }
  273. return deptId, path, cleanup
  274. }
  275. // TC-0208: MEMBER 调用者不能分配权限级别高于自身的角色 (audit H-1 修复后 permsLevel 仅对 MEMBER 生效)
  276. func TestBindRoles_PermsLevelEscalation_Rejected(t *testing.T) {
  277. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  278. conn := testutil.GetTestSqlConn()
  279. superCtx := ctxhelper.SuperAdminCtx()
  280. deptId, deptPath, cleanupDept := setupDeptForCaller(t, svcCtx)
  281. t.Cleanup(cleanupDept)
  282. productCode := "test_product"
  283. // 目标用户:放进 dept 下,MEMBER 产品成员
  284. username := testutil.UniqueId()
  285. targetUserId := insertTestUserFull(t, superCtx, &userModel.SysUser{
  286. Username: username, Password: testutil.HashPassword("pass"),
  287. Nickname: "tgt", DeptId: deptId,
  288. IsSuperAdmin: 2, MustChangePassword: 2, Status: 1,
  289. })
  290. mId := insertTestMember(t, svcCtx, productCode, targetUserId)
  291. highLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1)
  292. // M-3 修复后 GuardRoleLevelAssignable 走 DB 强一致读取 caller 的 MinPermsLevel,
  293. // 因此需要在 DB 里为调用者落地真实的 user + role + user_role 关系链(permsLevel=50)。
  294. callerUserId, callerCleanup := seedCallerWithRoleLevel(t, svcCtx, productCode, 50)
  295. t.Cleanup(callerCleanup)
  296. t.Cleanup(func() {
  297. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", targetUserId)
  298. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  299. testutil.CleanTable(superCtx, conn, "`sys_user`", targetUserId)
  300. testutil.CleanTable(superCtx, conn, "`sys_role`", highLevelRole)
  301. })
  302. // MEMBER 调用者与 target 同 dept,DB 中 MinPermsLevel=50,目标角色 permsLevel=1 → 越级
  303. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  304. UserId: callerUserId,
  305. Username: "member_caller",
  306. IsSuperAdmin: false,
  307. MemberType: consts.MemberTypeMember,
  308. Status: consts.StatusEnabled,
  309. ProductCode: productCode,
  310. DeptId: deptId,
  311. DeptPath: deptPath,
  312. MinPermsLevel: 50,
  313. })
  314. logic := NewBindRolesLogic(ctx, svcCtx)
  315. err := logic.BindRoles(&types.BindRolesReq{
  316. UserId: targetUserId,
  317. RoleIds: []int64{highLevelRole},
  318. })
  319. require.Error(t, err)
  320. var ce *response.CodeError
  321. require.True(t, errors.As(err, &ce))
  322. assert.Equal(t, 403, ce.Code())
  323. assert.Contains(t, ce.Error(), "不能分配权限级别高于自身的角色")
  324. }
  325. // TC-0711: ADMIN 调用者(MinPermsLevel=math.MaxInt64)不受 permsLevel 校验约束 (audit H-1 回归)
  326. // 修复前:ADMIN 通过 member_type 获得权限,MinPermsLevel 保持 math.MaxInt64,
  327. // r.PermsLevel < math.MaxInt64 必然成立 → ADMIN 无法绑定任何角色。
  328. // 修复后:代码显式豁免 ADMIN/DEVELOPER 的 permsLevel 校验。
  329. func TestBindRoles_AdminBypassesPermsLevelCheck(t *testing.T) {
  330. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  331. conn := testutil.GetTestSqlConn()
  332. superCtx := ctxhelper.SuperAdminCtx()
  333. productCode := "test_product"
  334. username := testutil.UniqueId()
  335. userId := insertTestUser(t, superCtx, username, testutil.HashPassword("pass"))
  336. mId := insertTestMember(t, svcCtx, productCode, userId)
  337. lowLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1)
  338. t.Cleanup(func() {
  339. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  340. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  341. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  342. testutil.CleanTable(superCtx, conn, "`sys_role`", lowLevelRole)
  343. })
  344. // 关键:模拟 loader 真实产出——ADMIN 没有自定义角色,MinPermsLevel=math.MaxInt64
  345. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  346. UserId: 999997,
  347. Username: "admin_caller",
  348. IsSuperAdmin: false,
  349. MemberType: consts.MemberTypeAdmin,
  350. Status: consts.StatusEnabled,
  351. ProductCode: productCode,
  352. DeptId: 1,
  353. DeptPath: "/1/",
  354. MinPermsLevel: math.MaxInt64, // 默认 sentinel
  355. })
  356. logic := NewBindRolesLogic(ctx, svcCtx)
  357. err := logic.BindRoles(&types.BindRolesReq{
  358. UserId: userId,
  359. RoleIds: []int64{lowLevelRole},
  360. })
  361. require.NoError(t, err, "ADMIN 调用者应当能绑定任意级别的角色 (audit H-1)")
  362. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  363. require.NoError(t, err)
  364. assert.Contains(t, roleIds, lowLevelRole)
  365. }
  366. // TC-0712: DEVELOPER 调用者同样不受 permsLevel 校验约束 (audit H-1 回归)
  367. func TestBindRoles_DeveloperBypassesPermsLevelCheck(t *testing.T) {
  368. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  369. conn := testutil.GetTestSqlConn()
  370. superCtx := ctxhelper.SuperAdminCtx()
  371. deptId, deptPath, cleanupDept := setupDeptForCaller(t, svcCtx)
  372. t.Cleanup(cleanupDept)
  373. productCode := "test_product"
  374. username := testutil.UniqueId()
  375. userId := insertTestUserFull(t, superCtx, &userModel.SysUser{
  376. Username: username, Password: testutil.HashPassword("pass"),
  377. Nickname: "tgt_dev", DeptId: deptId,
  378. IsSuperAdmin: 2, MustChangePassword: 2, Status: 1,
  379. })
  380. mId := insertTestMember(t, svcCtx, productCode, userId)
  381. lowLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1)
  382. t.Cleanup(func() {
  383. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  384. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  385. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  386. testutil.CleanTable(superCtx, conn, "`sys_role`", lowLevelRole)
  387. })
  388. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  389. UserId: 999996,
  390. Username: "developer_caller",
  391. IsSuperAdmin: false,
  392. MemberType: consts.MemberTypeDeveloper,
  393. Status: consts.StatusEnabled,
  394. ProductCode: productCode,
  395. DeptId: deptId,
  396. DeptPath: deptPath,
  397. MinPermsLevel: math.MaxInt64,
  398. })
  399. logic := NewBindRolesLogic(ctx, svcCtx)
  400. err := logic.BindRoles(&types.BindRolesReq{
  401. UserId: userId,
  402. RoleIds: []int64{lowLevelRole},
  403. })
  404. require.NoError(t, err, "DEVELOPER 调用者应当能绑定任意级别的角色 (audit H-1)")
  405. }
  406. // TC-0713: MinPermsLevel == math.MaxInt64 的 MEMBER 调用者也必须被豁免
  407. // (sentinel 判定路径:既不是 ADMIN/DEVELOPER,也没有角色,此时 r.PermsLevel<MaxInt64 的逐字面比较
  408. // 曾经误伤此类 MEMBER;修复后代码用 MinPermsLevel==MaxInt64 做短路)
  409. func TestBindRoles_MemberWithSentinelMinLevel_NotBlocked(t *testing.T) {
  410. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  411. conn := testutil.GetTestSqlConn()
  412. superCtx := ctxhelper.SuperAdminCtx()
  413. productCode := "test_product"
  414. username := testutil.UniqueId()
  415. userId := insertTestUser(t, superCtx, username, testutil.HashPassword("pass"))
  416. mId := insertTestMember(t, svcCtx, productCode, userId)
  417. role := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 100)
  418. t.Cleanup(func() {
  419. testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId)
  420. testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId)
  421. testutil.CleanTable(superCtx, conn, "`sys_user`", userId)
  422. testutil.CleanTable(superCtx, conn, "`sys_role`", role)
  423. })
  424. // MEMBER 调用者没有绑定任何启用角色,MinPermsLevel=MaxInt64(sentinel)
  425. // 正式语义:"我自己无级别" → 不应触发越级校验(否则所有无角色 MEMBER 都永远无法分配角色)
  426. ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
  427. UserId: 999995,
  428. Username: "member_no_role",
  429. IsSuperAdmin: false,
  430. MemberType: consts.MemberTypeMember,
  431. Status: consts.StatusEnabled,
  432. ProductCode: productCode,
  433. DeptId: 1,
  434. DeptPath: "/1/",
  435. MinPermsLevel: math.MaxInt64,
  436. })
  437. logic := NewBindRolesLogic(ctx, svcCtx)
  438. // 注意:业务层早期就会用 `RequireProductAdminFor` 拦住非 ADMIN/SUPER 的调用;此处是为了单独验证
  439. // bindRoles 内部的 permsLevel 分支。实际发生于 ADMIN 通过上层校验但 MemberType 上下文异常时的防御。
  440. // 这里只断言:"sentinel 路径不应报 403 '不能分配权限级别高于自身的角色'"。
  441. err := logic.BindRoles(&types.BindRolesReq{
  442. UserId: userId,
  443. RoleIds: []int64{role},
  444. })
  445. // 调用者非 ADMIN,且是 MEMBER,上游会拦 403 "仅ADMIN/超管可绑定角色";
  446. // 此处我们只校验"即使走到 permsLevel 分支,sentinel MinPermsLevel 不应命中"
  447. if err != nil {
  448. var ce *response.CodeError
  449. require.True(t, errors.As(err, &ce))
  450. assert.NotContains(t, ce.Error(), "不能分配权限级别高于自身的角色",
  451. "sentinel MinPermsLevel=math.MaxInt64 不应触发越级错误 (audit H-1)")
  452. }
  453. }
  454. // TC-0209: 超管可以分配任意权限级别的角色
  455. func TestBindRoles_SuperAdminCanAssignAnyLevel(t *testing.T) {
  456. ctx := ctxhelper.SuperAdminCtx()
  457. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  458. conn := testutil.GetTestSqlConn()
  459. productCode := "test_product"
  460. username := testutil.UniqueId()
  461. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  462. mId := insertTestMember(t, svcCtx, productCode, userId)
  463. highLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1)
  464. t.Cleanup(func() {
  465. testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId)
  466. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  467. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  468. testutil.CleanTable(ctx, conn, "`sys_role`", highLevelRole)
  469. })
  470. logic := NewBindRolesLogic(ctx, svcCtx)
  471. err := logic.BindRoles(&types.BindRolesReq{
  472. UserId: userId,
  473. RoleIds: []int64{highLevelRole},
  474. })
  475. require.NoError(t, err)
  476. roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId)
  477. require.NoError(t, err)
  478. assert.Contains(t, roleIds, highLevelRole)
  479. }
  480. // TC-0191: 目标用户不是当前产品成员时拒绝绑定角色(L-4修复验证)
  481. func TestBindRoles_NonMemberRejected(t *testing.T) {
  482. ctx := ctxhelper.SuperAdminCtx()
  483. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  484. conn := testutil.GetTestSqlConn()
  485. username := testutil.UniqueId()
  486. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  487. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
  488. logic := NewBindRolesLogic(ctx, svcCtx)
  489. err := logic.BindRoles(&types.BindRolesReq{
  490. UserId: userId,
  491. RoleIds: []int64{},
  492. })
  493. require.Error(t, err)
  494. var codeErr2 *response.CodeError
  495. require.True(t, errors.As(err, &codeErr2))
  496. assert.Equal(t, 400, codeErr2.Code())
  497. assert.Contains(t, codeErr2.Error(), "不是当前产品的成员")
  498. }