setUserPermsLogic_test.go 30 KB

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