setUserPermsLogic_test.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. package user
  2. import (
  3. "context"
  4. "errors"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. "github.com/zeromicro/go-zero/core/stores/sqlx"
  8. "math"
  9. "perms-system-server/internal/consts"
  10. "perms-system-server/internal/loaders"
  11. "perms-system-server/internal/middleware"
  12. memberModel "perms-system-server/internal/model/productmember"
  13. permModel "perms-system-server/internal/model/perm"
  14. productModel "perms-system-server/internal/model/product"
  15. "perms-system-server/internal/response"
  16. "perms-system-server/internal/svc"
  17. "perms-system-server/internal/testutil"
  18. "perms-system-server/internal/testutil/ctxhelper"
  19. "perms-system-server/internal/types"
  20. "testing"
  21. "time"
  22. )
  23. type userPermRow struct {
  24. Id int64 `db:"id"`
  25. UserId int64 `db:"userId"`
  26. PermId int64 `db:"permId"`
  27. Effect string `db:"effect"`
  28. }
  29. func findUserPerms(t *testing.T, ctx context.Context, userId int64) []userPermRow {
  30. t.Helper()
  31. conn := testutil.GetTestSqlConn()
  32. var rows []userPermRow
  33. require.NoError(t, conn.QueryRowsCtx(ctx, &rows,
  34. "SELECT `id`,`userId`,`permId`,`effect` FROM `sys_user_perm` WHERE `userId`=?", userId))
  35. return rows
  36. }
  37. func insertTestPerm(t *testing.T, svcCtx *svc.ServiceContext, productCode string) int64 {
  38. t.Helper()
  39. now := time.Now().Unix()
  40. res, err := svcCtx.SysPermModel.Insert(ctxhelper.SuperAdminCtx(), &permModel.SysPerm{
  41. ProductCode: productCode,
  42. Name: "perm_" + testutil.UniqueId(),
  43. Code: "code_" + testutil.UniqueId(),
  44. Status: 1,
  45. CreateTime: now,
  46. UpdateTime: now,
  47. })
  48. require.NoError(t, err)
  49. id, _ := res.LastInsertId()
  50. return id
  51. }
  52. // TC-0192: 正常ALLOW
  53. func TestSetUserPerms_Allow(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. p1 := insertTestPerm(t, svcCtx, "test_product")
  61. p2 := insertTestPerm(t, svcCtx, "test_product")
  62. t.Cleanup(func() {
  63. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "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_perm`", p1, p2)
  67. })
  68. logic := NewSetUserPermsLogic(ctx, svcCtx)
  69. err := logic.SetUserPerms(&types.SetPermsReq{
  70. UserId: userId,
  71. ProductCode: "test_product",
  72. Perms: []types.UserPermItem{
  73. {PermId: p1, Effect: "ALLOW"},
  74. {PermId: p2, Effect: "ALLOW"},
  75. },
  76. })
  77. require.NoError(t, err)
  78. perms := findUserPerms(t, ctx, userId)
  79. assert.Len(t, perms, 2)
  80. for _, p := range perms {
  81. assert.Equal(t, "ALLOW", p.Effect)
  82. }
  83. }
  84. // TC-0194: DENY权限
  85. func TestSetUserPerms_Deny(t *testing.T) {
  86. ctx := ctxhelper.SuperAdminCtx()
  87. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  88. conn := testutil.GetTestSqlConn()
  89. username := testutil.UniqueId()
  90. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  91. mId := insertTestMember(t, svcCtx, "test_product", userId)
  92. p1 := insertTestPerm(t, svcCtx, "test_product")
  93. t.Cleanup(func() {
  94. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  95. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  96. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  97. testutil.CleanTable(ctx, conn, "`sys_perm`", p1)
  98. })
  99. logic := NewSetUserPermsLogic(ctx, svcCtx)
  100. err := logic.SetUserPerms(&types.SetPermsReq{
  101. UserId: userId,
  102. ProductCode: "test_product",
  103. Perms: []types.UserPermItem{
  104. {PermId: p1, Effect: "DENY"},
  105. },
  106. })
  107. require.NoError(t, err)
  108. perms := findUserPerms(t, ctx, userId)
  109. require.Len(t, perms, 1)
  110. assert.Equal(t, "DENY", perms[0].Effect)
  111. assert.Equal(t, p1, perms[0].PermId)
  112. }
  113. // TC-0193: 用户不存在
  114. func TestSetUserPerms_UserNotFound(t *testing.T) {
  115. ctx := ctxhelper.SuperAdminCtx()
  116. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  117. logic := NewSetUserPermsLogic(ctx, svcCtx)
  118. err := logic.SetUserPerms(&types.SetPermsReq{
  119. UserId: 999999999,
  120. ProductCode: "test_product",
  121. Perms: []types.UserPermItem{
  122. {PermId: 1, Effect: "ALLOW"},
  123. },
  124. })
  125. require.Error(t, err)
  126. var codeErr *response.CodeError
  127. require.True(t, errors.As(err, &codeErr))
  128. assert.Equal(t, 404, codeErr.Code())
  129. assert.Equal(t, "用户不存在", codeErr.Error())
  130. }
  131. // TC-0195: 清空权限
  132. func TestSetUserPerms_EmptyPerms_ClearsAll(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. p1 := insertTestPerm(t, svcCtx, "test_product")
  140. t.Cleanup(func() {
  141. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  142. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  143. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  144. testutil.CleanTable(ctx, conn, "`sys_perm`", p1)
  145. })
  146. logic := NewSetUserPermsLogic(ctx, svcCtx)
  147. err := logic.SetUserPerms(&types.SetPermsReq{
  148. UserId: userId,
  149. ProductCode: "test_product",
  150. Perms: []types.UserPermItem{
  151. {PermId: p1, Effect: "ALLOW"},
  152. },
  153. })
  154. require.NoError(t, err)
  155. err = logic.SetUserPerms(&types.SetPermsReq{
  156. UserId: userId,
  157. ProductCode: "test_product",
  158. Perms: []types.UserPermItem{},
  159. })
  160. require.NoError(t, err)
  161. perms := findUserPerms(t, ctx, userId)
  162. assert.Empty(t, perms)
  163. }
  164. // TC-0196: 无效Effect值
  165. func TestSetUserPerms_InvalidEffect(t *testing.T) {
  166. ctx := ctxhelper.SuperAdminCtx()
  167. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  168. conn := testutil.GetTestSqlConn()
  169. username := testutil.UniqueId()
  170. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  171. mId := insertTestMember(t, svcCtx, "test_product", userId)
  172. t.Cleanup(func() {
  173. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  174. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  175. })
  176. logic := NewSetUserPermsLogic(ctx, svcCtx)
  177. err := logic.SetUserPerms(&types.SetPermsReq{
  178. UserId: userId,
  179. ProductCode: "test_product",
  180. Perms: []types.UserPermItem{
  181. {PermId: 1, Effect: "INVALID"},
  182. },
  183. })
  184. require.Error(t, err)
  185. var codeErr *response.CodeError
  186. require.True(t, errors.As(err, &codeErr))
  187. assert.Equal(t, 400, codeErr.Code())
  188. assert.Contains(t, codeErr.Error(), "effect值无效")
  189. }
  190. // TC-0197: PermId不存在
  191. func TestSetUserPerms_PermNotExists(t *testing.T) {
  192. ctx := ctxhelper.SuperAdminCtx()
  193. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  194. conn := testutil.GetTestSqlConn()
  195. username := testutil.UniqueId()
  196. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  197. mId := insertTestMember(t, svcCtx, "test_product", userId)
  198. t.Cleanup(func() {
  199. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  200. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  201. })
  202. logic := NewSetUserPermsLogic(ctx, svcCtx)
  203. err := logic.SetUserPerms(&types.SetPermsReq{
  204. UserId: userId,
  205. ProductCode: "test_product",
  206. Perms: []types.UserPermItem{
  207. {PermId: 999999999, Effect: "ALLOW"},
  208. },
  209. })
  210. require.Error(t, err)
  211. var codeErr *response.CodeError
  212. require.True(t, errors.As(err, &codeErr))
  213. assert.Equal(t, 400, codeErr.Code())
  214. assert.Contains(t, codeErr.Error(), "无效的权限ID")
  215. }
  216. // TC-0198: 权限不属于当前产品
  217. func TestSetUserPerms_PermBelongsToOtherProduct(t *testing.T) {
  218. ctx := ctxhelper.SuperAdminCtx()
  219. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  220. conn := testutil.GetTestSqlConn()
  221. username := testutil.UniqueId()
  222. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  223. mId := insertTestMember(t, svcCtx, "test_product", userId)
  224. otherPerm := insertTestPerm(t, svcCtx, "other_product")
  225. t.Cleanup(func() {
  226. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  227. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  228. testutil.CleanTable(ctx, conn, "`sys_perm`", otherPerm)
  229. })
  230. logic := NewSetUserPermsLogic(ctx, svcCtx)
  231. err := logic.SetUserPerms(&types.SetPermsReq{
  232. UserId: userId,
  233. ProductCode: "test_product",
  234. Perms: []types.UserPermItem{
  235. {PermId: otherPerm, Effect: "ALLOW"},
  236. },
  237. })
  238. require.Error(t, err)
  239. var codeErr *response.CodeError
  240. require.True(t, errors.As(err, &codeErr))
  241. assert.Equal(t, 400, codeErr.Code())
  242. assert.Contains(t, codeErr.Error(), "其他产品的权限")
  243. }
  244. // TC-0210: 同一权限ID同时为ALLOW和DENY被拒绝
  245. func TestSetUserPerms_ConflictingEffects(t *testing.T) {
  246. ctx := ctxhelper.SuperAdminCtx()
  247. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  248. conn := testutil.GetTestSqlConn()
  249. username := testutil.UniqueId()
  250. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  251. mId := insertTestMember(t, svcCtx, "test_product", userId)
  252. p1 := insertTestPerm(t, svcCtx, "test_product")
  253. t.Cleanup(func() {
  254. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  255. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  256. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  257. testutil.CleanTable(ctx, conn, "`sys_perm`", p1)
  258. })
  259. logic := NewSetUserPermsLogic(ctx, svcCtx)
  260. err := logic.SetUserPerms(&types.SetPermsReq{
  261. UserId: userId,
  262. ProductCode: "test_product",
  263. Perms: []types.UserPermItem{
  264. {PermId: p1, Effect: "ALLOW"},
  265. {PermId: p1, Effect: "DENY"},
  266. },
  267. })
  268. require.Error(t, err)
  269. var codeErr *response.CodeError
  270. require.True(t, errors.As(err, &codeErr))
  271. assert.Equal(t, 400, codeErr.Code())
  272. assert.Contains(t, codeErr.Error(), "同一权限ID不能同时为 ALLOW 和 DENY")
  273. }
  274. // TC-0211: 重复的权限ID相同Effect被去重
  275. func TestSetUserPerms_DuplicatePermDedup(t *testing.T) {
  276. ctx := ctxhelper.SuperAdminCtx()
  277. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  278. conn := testutil.GetTestSqlConn()
  279. username := testutil.UniqueId()
  280. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  281. mId := insertTestMember(t, svcCtx, "test_product", userId)
  282. p1 := insertTestPerm(t, svcCtx, "test_product")
  283. t.Cleanup(func() {
  284. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  285. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  286. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  287. testutil.CleanTable(ctx, conn, "`sys_perm`", p1)
  288. })
  289. logic := NewSetUserPermsLogic(ctx, svcCtx)
  290. err := logic.SetUserPerms(&types.SetPermsReq{
  291. UserId: userId,
  292. ProductCode: "test_product",
  293. Perms: []types.UserPermItem{
  294. {PermId: p1, Effect: "ALLOW"},
  295. {PermId: p1, Effect: "ALLOW"},
  296. },
  297. })
  298. require.NoError(t, err)
  299. perms := findUserPerms(t, ctx, userId)
  300. assert.Len(t, perms, 1, "重复的权限ID应被去重,只插入一条")
  301. assert.Equal(t, "ALLOW", perms[0].Effect)
  302. }
  303. // TC-0212: 已禁用的权限不能被设置
  304. func TestSetUserPerms_DisabledPermRejected(t *testing.T) {
  305. ctx := ctxhelper.SuperAdminCtx()
  306. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  307. conn := testutil.GetTestSqlConn()
  308. username := testutil.UniqueId()
  309. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  310. mId := insertTestMember(t, svcCtx, "test_product", userId)
  311. now := time.Now().Unix()
  312. res, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  313. ProductCode: "test_product",
  314. Name: "disabled_perm_" + testutil.UniqueId(),
  315. Code: "disabled_" + testutil.UniqueId(),
  316. Status: 2,
  317. CreateTime: now,
  318. UpdateTime: now,
  319. })
  320. require.NoError(t, err)
  321. disabledPermId, _ := res.LastInsertId()
  322. t.Cleanup(func() {
  323. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  324. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  325. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  326. testutil.CleanTable(ctx, conn, "`sys_perm`", disabledPermId)
  327. })
  328. logic := NewSetUserPermsLogic(ctx, svcCtx)
  329. err = logic.SetUserPerms(&types.SetPermsReq{
  330. UserId: userId,
  331. ProductCode: "test_product",
  332. Perms: []types.UserPermItem{
  333. {PermId: disabledPermId, Effect: "ALLOW"},
  334. },
  335. })
  336. require.Error(t, err)
  337. var codeErr *response.CodeError
  338. require.True(t, errors.As(err, &codeErr))
  339. assert.Equal(t, 400, codeErr.Code())
  340. assert.Contains(t, codeErr.Error(), "已被禁用")
  341. }
  342. // TC-0199: 目标用户不是当前产品成员时拒绝设置权限(修复验证)
  343. func TestSetUserPerms_NonMemberRejected(t *testing.T) {
  344. ctx := ctxhelper.SuperAdminCtx()
  345. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  346. conn := testutil.GetTestSqlConn()
  347. username := testutil.UniqueId()
  348. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  349. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
  350. logic := NewSetUserPermsLogic(ctx, svcCtx)
  351. err := logic.SetUserPerms(&types.SetPermsReq{
  352. UserId: userId,
  353. ProductCode: "test_product",
  354. Perms: []types.UserPermItem{},
  355. })
  356. require.Error(t, err)
  357. var codeErr2 *response.CodeError
  358. require.True(t, errors.As(err, &codeErr2))
  359. assert.Equal(t, 400, codeErr2.Code())
  360. assert.Contains(t, codeErr2.Error(), "不是当前产品的成员")
  361. }
  362. type lyingSysPermModel struct {
  363. permModel.SysPermModel
  364. lyingProductCode string
  365. }
  366. func (m *lyingSysPermModel) FindByIds(ctx context.Context, ids []int64) ([]*permModel.SysPerm, error) {
  367. real, err := m.SysPermModel.FindByIds(ctx, ids)
  368. if err != nil {
  369. return nil, err
  370. }
  371. for _, p := range real {
  372. p.ProductCode = m.lyingProductCode
  373. p.Status = 1
  374. }
  375. return real, nil
  376. }
  377. // TC-0988: TOCTOU 复核 —— 前置检查通过但实际 Disabled,事务末 COUNT 必须触发 409 回滚。
  378. func TestSetUserPerms_L4_TOCTOU_CountMismatch_RollsBackWith409(t *testing.T) {
  379. ctx := ctxhelper.SuperAdminCtx()
  380. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  381. conn := testutil.GetTestSqlConn()
  382. username := testutil.UniqueId()
  383. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  384. mId := insertTestMember(t, svcCtx, "test_product", userId)
  385. // 直接在 DB 里塞一个 status=Disabled 的 perm,模拟 SyncPermissions 已经提交
  386. // 把这个 perm 落盘为 Disabled 的状态。
  387. now := time.Now().Unix()
  388. res, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  389. ProductCode: "test_product",
  390. Name: "l4_disabled_" + testutil.UniqueId(),
  391. Code: "l4_dis_" + testutil.UniqueId(),
  392. Status: 2, // Disabled
  393. CreateTime: now, UpdateTime: now,
  394. })
  395. require.NoError(t, err)
  396. disabledPermId, _ := res.LastInsertId()
  397. t.Cleanup(func() {
  398. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  399. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  400. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  401. testutil.CleanTable(ctx, conn, "`sys_perm`", disabledPermId)
  402. })
  403. // 装饰 SysPermModel:让 FindByIds 撒谎(Status=1, productCode=test_product)。
  404. svcCtx.SysPermModel = &lyingSysPermModel{
  405. SysPermModel: svcCtx.SysPermModel,
  406. lyingProductCode: "test_product",
  407. }
  408. err = NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
  409. UserId: userId,
  410. ProductCode: "test_product",
  411. Perms: []types.UserPermItem{{PermId: disabledPermId, Effect: "ALLOW"}},
  412. })
  413. require.Error(t, err, "前置通过但 DB 实际 Disabled 时,事务末 COUNT 必须触发 409")
  414. var ce *response.CodeError
  415. require.True(t, errors.As(err, &ce))
  416. assert.Equal(t, 409, ce.Code(),
  417. "TOCTOU 复核必须返回 409 Conflict;若仍是 200/4xx 说明复核 COUNT 被移除,"+
  418. "脏 user_perm 会被真实落盘")
  419. assert.Contains(t, ce.Error(), "已被禁用",
  420. "错误文案必须明示'部分权限在提交时已被禁用',供前端判定是否重试")
  421. // 最关键的断言:脏行必须不可能落盘。
  422. leftover := findUserPerms(t, ctx, userId)
  423. assert.Empty(t, leftover,
  424. "事务必须回滚;如果发现 sys_user_perm 有脏行,说明 COUNT 复核失效或"+
  425. "事务隔离性被破坏,loadPerms 的 status=1 过滤能兜底但会绕开链")
  426. }
  427. // TC-0989: 正向基线 —— 所有 perm 真实 Enabled 时,不得被 复核误杀。
  428. // 这条显式"不回滚"的断言防止未来有人把 COUNT 改成 "!=" 逻辑或把阈值改错。
  429. func TestSetUserPerms_L4_AllEnabled_CountPasses(t *testing.T) {
  430. ctx := ctxhelper.SuperAdminCtx()
  431. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  432. conn := testutil.GetTestSqlConn()
  433. username := testutil.UniqueId()
  434. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  435. mId := insertTestMember(t, svcCtx, "test_product", userId)
  436. p1 := insertTestPerm(t, svcCtx, "test_product")
  437. p2 := insertTestPerm(t, svcCtx, "test_product")
  438. t.Cleanup(func() {
  439. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  440. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  441. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  442. testutil.CleanTable(ctx, conn, "`sys_perm`", p1, p2)
  443. })
  444. err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
  445. UserId: userId,
  446. ProductCode: "test_product",
  447. Perms: []types.UserPermItem{
  448. {PermId: p1, Effect: "ALLOW"},
  449. {PermId: p2, Effect: "DENY"},
  450. },
  451. })
  452. require.NoError(t, err, "不得误杀正常写入;一旦误报会把正常管理操作变 409")
  453. rows := findUserPerms(t, ctx, userId)
  454. assert.Len(t, rows, 2, "两条 user_perm 必须落盘")
  455. }
  456. func TestSetUserPerms_MemberCannotSelfEscalate(t *testing.T) {
  457. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  458. conn := testutil.GetTestSqlConn()
  459. now := time.Now().Unix()
  460. bootstrap := context.Background()
  461. code := testutil.UniqueId()
  462. pRes, err := svcCtx.SysProductModel.Insert(bootstrap, &productModel.SysProduct{
  463. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  464. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  465. })
  466. require.NoError(t, err)
  467. pId, _ := pRes.LastInsertId()
  468. username := testutil.UniqueId()
  469. userId := insertTestUser(t, bootstrap, username, testutil.HashPassword("pw"))
  470. mId := insertTestMember(t, svcCtx, code, userId)
  471. permRes, err := svcCtx.SysPermModel.Insert(bootstrap, &permModel.SysPerm{
  472. ProductCode: code, Name: "escalate_p", Code: "esc_" + testutil.UniqueId(),
  473. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  474. })
  475. require.NoError(t, err)
  476. permId, _ := permRes.LastInsertId()
  477. t.Cleanup(func() {
  478. testutil.CleanTableByField(bootstrap, conn, "`sys_user_perm`", "userId", userId)
  479. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  480. testutil.CleanTable(bootstrap, conn, "`sys_perm`", permId)
  481. testutil.CleanTable(bootstrap, conn, "`sys_user`", userId)
  482. testutil.CleanTable(bootstrap, conn, "`sys_product`", pId)
  483. })
  484. // caller = 目标用户本人,MemberType=MEMBER(非 ADMIN)
  485. callerCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  486. UserId: userId, Username: username,
  487. IsSuperAdmin: false,
  488. MemberType: consts.MemberTypeMember,
  489. Status: consts.StatusEnabled,
  490. ProductCode: code,
  491. DeptId: 1,
  492. DeptPath: "/1/",
  493. MinPermsLevel: math.MaxInt64,
  494. })
  495. err = NewSetUserPermsLogic(callerCtx, svcCtx).SetUserPerms(&types.SetPermsReq{
  496. UserId: userId, // 给自己
  497. Perms: []types.UserPermItem{{PermId: permId, Effect: consts.PermEffectAllow}},
  498. })
  499. require.Error(t, err, "MEMBER 不得自我授权")
  500. var ce *response.CodeError
  501. require.True(t, errors.As(err, &ce))
  502. assert.Equal(t, 403, ce.Code())
  503. assert.Contains(t, ce.Error(), "仅超级管理员或该产品的管理员可执行此操作")
  504. // 二次确认:没有任何 user_perm 记录被写入
  505. rows := findUserPerms(t, bootstrap, userId)
  506. assert.Len(t, rows, 0, "被拒绝的 SetUserPerms 不得在 DB 残留任何个性化权限")
  507. }
  508. // TC-0744: -A 修复回归 —— DEVELOPER 调用者(非 ADMIN)同样被拦截,即便目标不是自己。
  509. func TestSetUserPerms_DeveloperCallerRejected(t *testing.T) {
  510. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  511. conn := testutil.GetTestSqlConn()
  512. bootstrap := context.Background()
  513. now := time.Now().Unix()
  514. code := testutil.UniqueId()
  515. pRes, err := svcCtx.SysProductModel.Insert(bootstrap, &productModel.SysProduct{
  516. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  517. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  518. })
  519. require.NoError(t, err)
  520. pId, _ := pRes.LastInsertId()
  521. targetUsername := "target_" + testutil.UniqueId()
  522. targetId := insertTestUser(t, bootstrap, targetUsername, testutil.HashPassword("pw"))
  523. mId := insertTestMember(t, svcCtx, code, targetId)
  524. t.Cleanup(func() {
  525. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  526. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  527. testutil.CleanTable(bootstrap, conn, "`sys_product`", pId)
  528. })
  529. devCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  530. UserId: 777777, Username: "dev_caller",
  531. MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled,
  532. ProductCode: code, DeptId: 1, DeptPath: "/1/", MinPermsLevel: math.MaxInt64,
  533. })
  534. err = NewSetUserPermsLogic(devCtx, svcCtx).SetUserPerms(&types.SetPermsReq{
  535. UserId: targetId, Perms: []types.UserPermItem{},
  536. })
  537. require.Error(t, err)
  538. var ce *response.CodeError
  539. require.True(t, errors.As(err, &ce))
  540. assert.Equal(t, 403, ce.Code())
  541. assert.Contains(t, ce.Error(), "仅超级管理员或该产品的管理员可执行此操作")
  542. }
  543. // TC-0745: -A 正向回归 —— 同产品 ADMIN 操作合法 MEMBER 目标(非自己)依旧放行。
  544. func TestSetUserPerms_ProductAdminStillWorks(t *testing.T) {
  545. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  546. conn := testutil.GetTestSqlConn()
  547. bootstrap := context.Background()
  548. now := time.Now().Unix()
  549. code := testutil.UniqueId()
  550. pRes, err := svcCtx.SysProductModel.Insert(bootstrap, &productModel.SysProduct{
  551. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  552. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  553. })
  554. require.NoError(t, err)
  555. pId, _ := pRes.LastInsertId()
  556. targetId := insertTestUser(t, bootstrap, "tgt_"+testutil.UniqueId(), testutil.HashPassword("pw"))
  557. mId := insertTestMember(t, svcCtx, code, targetId)
  558. permRes, err := svcCtx.SysPermModel.Insert(bootstrap, &permModel.SysPerm{
  559. ProductCode: code, Name: "ok_p", Code: "ok_" + testutil.UniqueId(),
  560. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  561. })
  562. require.NoError(t, err)
  563. permId, _ := permRes.LastInsertId()
  564. t.Cleanup(func() {
  565. testutil.CleanTableByField(bootstrap, conn, "`sys_user_perm`", "userId", targetId)
  566. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
  567. testutil.CleanTable(bootstrap, conn, "`sys_perm`", permId)
  568. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  569. testutil.CleanTable(bootstrap, conn, "`sys_product`", pId)
  570. })
  571. adminCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  572. UserId: 999999, Username: "admin_caller",
  573. MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
  574. ProductCode: code, DeptId: 1, DeptPath: "/1/", MinPermsLevel: math.MaxInt64,
  575. })
  576. err = NewSetUserPermsLogic(adminCtx, svcCtx).SetUserPerms(&types.SetPermsReq{
  577. UserId: targetId,
  578. Perms: []types.UserPermItem{{PermId: permId, Effect: consts.PermEffectAllow}},
  579. })
  580. require.NoError(t, err, "产品 ADMIN 正常路径必须放行")
  581. rows := findUserPerms(t, bootstrap, targetId)
  582. assert.Len(t, rows, 1, "ADMIN 授权后 DB 应有 1 条 user_perm")
  583. }
  584. // TC-1104: 非 ADMIN caller + 不存在的 userId —— 必须 403(不是 404),
  585. // L-R13-1:`RequireProductAdminFor` 先行于 `SysUserModel.FindOne(userId)`,
  586. // 消除通过 setUserPerms 做 userId 枚举的 oracle。
  587. func TestSetUserPerms_L_R13_1_AuthBeforeUserLookup(t *testing.T) {
  588. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  589. // caller 持有 test_product 的 MEMBER 上下文(非 ADMIN / 非超管)
  590. ctx := ctxhelper.MemberCtx("test_product")
  591. err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
  592. UserId: 999999999,
  593. Perms: []types.UserPermItem{{PermId: 1, Effect: consts.PermEffectAllow}},
  594. })
  595. require.Error(t, err)
  596. var ce *response.CodeError
  597. require.True(t, errors.As(err, &ce))
  598. assert.Equal(t, 403, ce.Code(),
  599. "L-R13-1:MEMBER caller 对任意 userId 都必须 403,不得返 404 泄漏用户存在性")
  600. assert.Contains(t, ce.Error(), "仅超级管理员或该产品的管理员可执行此操作")
  601. }
  602. // typeFlippingMemberModel 是 L-R13-2 TOCTOU 测试的窗口装饰器:
  603. // - FindOneByProductCodeUserId:把目标成员的 MemberType 改写为 outsideTxType(通常是 MEMBER),
  604. // 让前置校验通过;
  605. // - FindOneForShareTx:
  606. // insideTxForceError=true 时直接返回 error(用来断言 ALLOW-only 路径必须**跳过** S 锁);
  607. // 否则把 MemberType 替换为 insideTxTypeHook(通常是 ADMIN),模拟"事务内 S 锁读到
  608. // 并发 UpdateMember 写入的新值"。
  609. // - 其它方法通过匿名内嵌的 memberModel.SysProductMemberModel 接口自动透传。
  610. type typeFlippingMemberModel struct {
  611. memberModel.SysProductMemberModel
  612. targetMemberId int64
  613. outsideTxType string
  614. insideTxTypeHook string
  615. insideTxForceError bool
  616. insideTxForceErrorMsg string
  617. }
  618. func (m *typeFlippingMemberModel) FindOneByProductCodeUserId(ctx context.Context, productCode string, userId int64) (*memberModel.SysProductMember, error) {
  619. real, err := m.SysProductMemberModel.FindOneByProductCodeUserId(ctx, productCode, userId)
  620. if err != nil {
  621. return nil, err
  622. }
  623. if m.outsideTxType != "" && real.Id == m.targetMemberId {
  624. clone := *real
  625. clone.MemberType = m.outsideTxType
  626. return &clone, nil
  627. }
  628. return real, nil
  629. }
  630. func (m *typeFlippingMemberModel) FindOneForShareTx(ctx context.Context, session sqlx.Session, id int64) (*memberModel.SysProductMember, error) {
  631. if m.insideTxForceError && id == m.targetMemberId {
  632. return nil, errors.New(m.insideTxForceErrorMsg)
  633. }
  634. real, err := m.SysProductMemberModel.FindOneForShareTx(ctx, session, id)
  635. if err != nil {
  636. return nil, err
  637. }
  638. if m.insideTxTypeHook != "" && real.Id == m.targetMemberId {
  639. clone := *real
  640. clone.MemberType = m.insideTxTypeHook
  641. return &clone, nil
  642. }
  643. return real, nil
  644. }
  645. // TC-1105: L-R13-2 DENY TOCTOU 闭环——事务外读到 MEMBER,事务内 FindOneForShareTx
  646. // 返回 ADMIN,必须触发 400 "目标用户是产品管理员或开发者...",并且事务回滚(无 DENY 脏行)。
  647. //
  648. // 实现思路:直接在事务外保持 member=MEMBER,让前置校验通过;在 setUserPerms 进入事务的
  649. // 瞬间,用 sqlx 直接 UPDATE 把 memberType 改为 ADMIN(在另一连接上绕过 go-zero 缓存层)。
  650. // 但这种时序非常脆。更稳妥的做法:用 svcCtx.SysProductMemberModel 的装饰器,让
  651. // FindOneForShareTx 直接返回 MemberType="ADMIN",模拟"事务内读到并发更新后的真值"。
  652. func TestSetUserPerms_L_R13_2_DenyTypeFlipRollsBack(t *testing.T) {
  653. ctx := ctxhelper.SuperAdminCtx()
  654. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  655. conn := testutil.GetTestSqlConn()
  656. username := testutil.UniqueId()
  657. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  658. mId := insertTestMember(t, svcCtx, "test_product", userId) // 落盘为 MEMBER
  659. permId := insertTestPerm(t, svcCtx, "test_product")
  660. t.Cleanup(func() {
  661. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  662. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  663. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  664. testutil.CleanTable(ctx, conn, "`sys_perm`", permId)
  665. })
  666. // 装饰器:事务外读(FindOneByProductCodeUserId)返 MEMBER;
  667. // 事务内 FindOneForShareTx 返 ADMIN,模拟并发 UpdateMember 在窗口期把目标升为 ADMIN
  668. // 后被 S 锁正确读到最新值。
  669. svcCtx.SysProductMemberModel = &typeFlippingMemberModel{
  670. SysProductMemberModel: svcCtx.SysProductMemberModel,
  671. targetMemberId: mId,
  672. outsideTxType: consts.MemberTypeMember,
  673. insideTxTypeHook: consts.MemberTypeAdmin,
  674. }
  675. err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
  676. UserId: userId,
  677. ProductCode: "test_product",
  678. Perms: []types.UserPermItem{{PermId: permId, Effect: consts.PermEffectDeny}},
  679. })
  680. require.Error(t, err, "事务内读到 ADMIN 必须拒绝写 DENY")
  681. var ce *response.CodeError
  682. require.True(t, errors.As(err, &ce))
  683. assert.Equal(t, 400, ce.Code(),
  684. "L-R13-2:事务内 member.MemberType=ADMIN 时必须 400,不得沉默写 DENY 脏行")
  685. assert.Contains(t, ce.Error(), "DENY 设置不会生效")
  686. // 不变式:被拒绝的事务必须回滚,DB 里绝不能出现 DENY 脏行。
  687. rows := findUserPerms(t, ctx, userId)
  688. assert.Empty(t, rows,
  689. "L-R13-2 的核心断言:事务必须原子回滚,sys_user_perm 里不得有任何行;"+
  690. "若此处出现 DENY 行说明 FindOneForShareTx 没有阻塞写或事务未正确 abort")
  691. }
  692. // TC-1106: 纯 ALLOW 请求不应触发 FindOneForShareTx 的 S 锁路径(hasDeny==false 时短路)。
  693. // 装饰器让 FindOneForShareTx 直接返回 error —— 如果逻辑还是调了,ALLOW 请求就会失败。
  694. func TestSetUserPerms_L_R13_2_AllowOnlySkipsShareLock(t *testing.T) {
  695. ctx := ctxhelper.SuperAdminCtx()
  696. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  697. conn := testutil.GetTestSqlConn()
  698. username := testutil.UniqueId()
  699. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  700. mId := insertTestMember(t, svcCtx, "test_product", userId)
  701. permId := insertTestPerm(t, svcCtx, "test_product")
  702. t.Cleanup(func() {
  703. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  704. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  705. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  706. testutil.CleanTable(ctx, conn, "`sys_perm`", permId)
  707. })
  708. // 故意让 FindOneForShareTx 爆炸:只要被调就把错误带出。
  709. svcCtx.SysProductMemberModel = &typeFlippingMemberModel{
  710. SysProductMemberModel: svcCtx.SysProductMemberModel,
  711. targetMemberId: mId,
  712. outsideTxType: consts.MemberTypeMember,
  713. insideTxForceError: true,
  714. insideTxForceErrorMsg: "FindOneForShareTx must NOT be called for ALLOW-only requests",
  715. }
  716. err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
  717. UserId: userId,
  718. ProductCode: "test_product",
  719. Perms: []types.UserPermItem{{PermId: permId, Effect: consts.PermEffectAllow}},
  720. })
  721. require.NoError(t, err,
  722. "纯 ALLOW 请求 hasDeny==false,必须短路、不调 FindOneForShareTx;"+
  723. "否则 ALLOW 也要承担一次 S 锁开销且被错误阻塞")
  724. rows := findUserPerms(t, ctx, userId)
  725. require.Len(t, rows, 1)
  726. assert.Equal(t, "ALLOW", rows[0].Effect)
  727. }
  728. // TC-1302: 超级管理员不传 ProductCode 时必须返回 400。
  729. func TestSetUserPerms_SuperAdmin_MissingProductCode(t *testing.T) {
  730. ctx := ctxhelper.SuperAdminCtx()
  731. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  732. err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
  733. UserId: 999999999,
  734. Perms: []types.UserPermItem{},
  735. })
  736. require.Error(t, err)
  737. var ce *response.CodeError
  738. require.True(t, errors.As(err, &ce))
  739. assert.Equal(t, 400, ce.Code())
  740. assert.Contains(t, ce.Error(), "必须指定产品编码")
  741. }
  742. // TC-1303: 超级管理员传入 ProductCode 时正常工作。
  743. func TestSetUserPerms_SuperAdmin_WithProductCode(t *testing.T) {
  744. ctx := ctxhelper.SuperAdminCtx()
  745. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  746. conn := testutil.GetTestSqlConn()
  747. username := testutil.UniqueId()
  748. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  749. mId := insertTestMember(t, svcCtx, "test_product", userId)
  750. permId := insertTestPerm(t, svcCtx, "test_product")
  751. t.Cleanup(func() {
  752. testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
  753. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  754. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  755. testutil.CleanTable(ctx, conn, "`sys_perm`", permId)
  756. })
  757. err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
  758. UserId: userId,
  759. ProductCode: "test_product",
  760. Perms: []types.UserPermItem{{PermId: permId, Effect: consts.PermEffectAllow}},
  761. })
  762. require.NoError(t, err, "超级管理员传入 ProductCode 时应正常设置权限")
  763. rows := findUserPerms(t, ctx, userId)
  764. require.Len(t, rows, 1)
  765. assert.Equal(t, consts.PermEffectAllow, rows[0].Effect)
  766. }