bindRolesLogic_test.go 33 KB

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