| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829 |
- package user
- import (
- "context"
- "errors"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- "math"
- "perms-system-server/internal/consts"
- "perms-system-server/internal/loaders"
- "perms-system-server/internal/middleware"
- memberModel "perms-system-server/internal/model/productmember"
- permModel "perms-system-server/internal/model/perm"
- productModel "perms-system-server/internal/model/product"
- "perms-system-server/internal/response"
- "perms-system-server/internal/svc"
- "perms-system-server/internal/testutil"
- "perms-system-server/internal/testutil/ctxhelper"
- "perms-system-server/internal/types"
- "testing"
- "time"
- )
- type userPermRow struct {
- Id int64 `db:"id"`
- UserId int64 `db:"userId"`
- PermId int64 `db:"permId"`
- Effect string `db:"effect"`
- }
- func findUserPerms(t *testing.T, ctx context.Context, userId int64) []userPermRow {
- t.Helper()
- conn := testutil.GetTestSqlConn()
- var rows []userPermRow
- require.NoError(t, conn.QueryRowsCtx(ctx, &rows,
- "SELECT `id`,`userId`,`permId`,`effect` FROM `sys_user_perm` WHERE `userId`=?", userId))
- return rows
- }
- func insertTestPerm(t *testing.T, svcCtx *svc.ServiceContext, productCode string) int64 {
- t.Helper()
- now := time.Now().Unix()
- res, err := svcCtx.SysPermModel.Insert(ctxhelper.SuperAdminCtx(), &permModel.SysPerm{
- ProductCode: productCode,
- Name: "perm_" + testutil.UniqueId(),
- Code: "code_" + testutil.UniqueId(),
- Status: 1,
- CreateTime: now,
- UpdateTime: now,
- })
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- return id
- }
- // TC-0192: 正常ALLOW
- func TestSetUserPerms_Allow(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- p1 := insertTestPerm(t, svcCtx, "test_product")
- p2 := insertTestPerm(t, svcCtx, "test_product")
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", p1, p2)
- })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: p1, Effect: "ALLOW"},
- {PermId: p2, Effect: "ALLOW"},
- },
- })
- require.NoError(t, err)
- perms := findUserPerms(t, ctx, userId)
- assert.Len(t, perms, 2)
- for _, p := range perms {
- assert.Equal(t, "ALLOW", p.Effect)
- }
- }
- // TC-0194: DENY权限
- func TestSetUserPerms_Deny(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- p1 := insertTestPerm(t, svcCtx, "test_product")
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", p1)
- })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: p1, Effect: "DENY"},
- },
- })
- require.NoError(t, err)
- perms := findUserPerms(t, ctx, userId)
- require.Len(t, perms, 1)
- assert.Equal(t, "DENY", perms[0].Effect)
- assert.Equal(t, p1, perms[0].PermId)
- }
- // TC-0193: 用户不存在
- func TestSetUserPerms_UserNotFound(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: 999999999,
- Perms: []types.UserPermItem{
- {PermId: 1, Effect: "ALLOW"},
- },
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 404, codeErr.Code())
- assert.Equal(t, "用户不存在", codeErr.Error())
- }
- // TC-0195: 清空权限
- func TestSetUserPerms_EmptyPerms_ClearsAll(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- p1 := insertTestPerm(t, svcCtx, "test_product")
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", p1)
- })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: p1, Effect: "ALLOW"},
- },
- })
- require.NoError(t, err)
- err = logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{},
- })
- require.NoError(t, err)
- perms := findUserPerms(t, ctx, userId)
- assert.Empty(t, perms)
- }
- // TC-0196: 无效Effect值
- func TestSetUserPerms_InvalidEffect(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- t.Cleanup(func() {
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: 1, Effect: "INVALID"},
- },
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Contains(t, codeErr.Error(), "effect值无效")
- }
- // TC-0197: PermId不存在
- func TestSetUserPerms_PermNotExists(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- t.Cleanup(func() {
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: 999999999, Effect: "ALLOW"},
- },
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Contains(t, codeErr.Error(), "无效的权限ID")
- }
- // TC-0198: 权限不属于当前产品
- func TestSetUserPerms_PermBelongsToOtherProduct(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- otherPerm := insertTestPerm(t, svcCtx, "other_product")
- t.Cleanup(func() {
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", otherPerm)
- })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: otherPerm, Effect: "ALLOW"},
- },
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Contains(t, codeErr.Error(), "其他产品的权限")
- }
- // TC-0210: 同一权限ID同时为ALLOW和DENY被拒绝
- func TestSetUserPerms_ConflictingEffects(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- p1 := insertTestPerm(t, svcCtx, "test_product")
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", p1)
- })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: p1, Effect: "ALLOW"},
- {PermId: p1, Effect: "DENY"},
- },
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Contains(t, codeErr.Error(), "同一权限ID不能同时为 ALLOW 和 DENY")
- }
- // TC-0211: 重复的权限ID相同Effect被去重
- func TestSetUserPerms_DuplicatePermDedup(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- p1 := insertTestPerm(t, svcCtx, "test_product")
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", p1)
- })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: p1, Effect: "ALLOW"},
- {PermId: p1, Effect: "ALLOW"},
- },
- })
- require.NoError(t, err)
- perms := findUserPerms(t, ctx, userId)
- assert.Len(t, perms, 1, "重复的权限ID应被去重,只插入一条")
- assert.Equal(t, "ALLOW", perms[0].Effect)
- }
- // TC-0212: 已禁用的权限不能被设置
- func TestSetUserPerms_DisabledPermRejected(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- now := time.Now().Unix()
- res, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
- ProductCode: "test_product",
- Name: "disabled_perm_" + testutil.UniqueId(),
- Code: "disabled_" + testutil.UniqueId(),
- Status: 2,
- CreateTime: now,
- UpdateTime: now,
- })
- require.NoError(t, err)
- disabledPermId, _ := res.LastInsertId()
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", disabledPermId)
- })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err = logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: disabledPermId, Effect: "ALLOW"},
- },
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Contains(t, codeErr.Error(), "已被禁用")
- }
- // TC-0199: 目标用户不是当前产品成员时拒绝设置权限(修复验证)
- func TestSetUserPerms_NonMemberRejected(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
- logic := NewSetUserPermsLogic(ctx, svcCtx)
- err := logic.SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{},
- })
- require.Error(t, err)
- var codeErr2 *response.CodeError
- require.True(t, errors.As(err, &codeErr2))
- assert.Equal(t, 400, codeErr2.Code())
- assert.Contains(t, codeErr2.Error(), "不是当前产品的成员")
- }
- type lyingSysPermModel struct {
- permModel.SysPermModel
- lyingProductCode string
- }
- func (m *lyingSysPermModel) FindByIds(ctx context.Context, ids []int64) ([]*permModel.SysPerm, error) {
- real, err := m.SysPermModel.FindByIds(ctx, ids)
- if err != nil {
- return nil, err
- }
- for _, p := range real {
- p.ProductCode = m.lyingProductCode
- p.Status = 1
- }
- return real, nil
- }
- // TC-0988: TOCTOU 复核 —— 前置检查通过但实际 Disabled,事务末 COUNT 必须触发 409 回滚。
- func TestSetUserPerms_L4_TOCTOU_CountMismatch_RollsBackWith409(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- // 直接在 DB 里塞一个 status=Disabled 的 perm,模拟 SyncPermissions 已经提交
- // 把这个 perm 落盘为 Disabled 的状态。
- now := time.Now().Unix()
- res, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
- ProductCode: "test_product",
- Name: "l4_disabled_" + testutil.UniqueId(),
- Code: "l4_dis_" + testutil.UniqueId(),
- Status: 2, // Disabled
- CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- disabledPermId, _ := res.LastInsertId()
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", disabledPermId)
- })
- // 装饰 SysPermModel:让 FindByIds 撒谎(Status=1, productCode=test_product)。
- svcCtx.SysPermModel = &lyingSysPermModel{
- SysPermModel: svcCtx.SysPermModel,
- lyingProductCode: "test_product",
- }
- err = NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{{PermId: disabledPermId, Effect: "ALLOW"}},
- })
- require.Error(t, err, "前置通过但 DB 实际 Disabled 时,事务末 COUNT 必须触发 409")
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 409, ce.Code(),
- "TOCTOU 复核必须返回 409 Conflict;若仍是 200/4xx 说明复核 COUNT 被移除,"+
- "脏 user_perm 会被真实落盘")
- assert.Contains(t, ce.Error(), "已被禁用",
- "错误文案必须明示'部分权限在提交时已被禁用',供前端判定是否重试")
- // 最关键的断言:脏行必须不可能落盘。
- leftover := findUserPerms(t, ctx, userId)
- assert.Empty(t, leftover,
- "事务必须回滚;如果发现 sys_user_perm 有脏行,说明 COUNT 复核失效或"+
- "事务隔离性被破坏,loadPerms 的 status=1 过滤能兜底但会绕开链")
- }
- // TC-0989: 正向基线 —— 所有 perm 真实 Enabled 时,不得被 复核误杀。
- // 这条显式"不回滚"的断言防止未来有人把 COUNT 改成 "!=" 逻辑或把阈值改错。
- func TestSetUserPerms_L4_AllEnabled_CountPasses(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- p1 := insertTestPerm(t, svcCtx, "test_product")
- p2 := insertTestPerm(t, svcCtx, "test_product")
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", p1, p2)
- })
- err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: p1, Effect: "ALLOW"},
- {PermId: p2, Effect: "DENY"},
- },
- })
- require.NoError(t, err, "不得误杀正常写入;一旦误报会把正常管理操作变 409")
- rows := findUserPerms(t, ctx, userId)
- assert.Len(t, rows, 2, "两条 user_perm 必须落盘")
- }
- func TestSetUserPerms_MemberCannotSelfEscalate(t *testing.T) {
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- now := time.Now().Unix()
- bootstrap := context.Background()
- code := testutil.UniqueId()
- pRes, err := svcCtx.SysProductModel.Insert(bootstrap, &productModel.SysProduct{
- Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
- Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- pId, _ := pRes.LastInsertId()
- username := testutil.UniqueId()
- userId := insertTestUser(t, bootstrap, username, testutil.HashPassword("pw"))
- mId := insertTestMember(t, svcCtx, code, userId)
- permRes, err := svcCtx.SysPermModel.Insert(bootstrap, &permModel.SysPerm{
- ProductCode: code, Name: "escalate_p", Code: "esc_" + testutil.UniqueId(),
- Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- permId, _ := permRes.LastInsertId()
- t.Cleanup(func() {
- testutil.CleanTableByField(bootstrap, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
- testutil.CleanTable(bootstrap, conn, "`sys_perm`", permId)
- testutil.CleanTable(bootstrap, conn, "`sys_user`", userId)
- testutil.CleanTable(bootstrap, conn, "`sys_product`", pId)
- })
- // caller = 目标用户本人,MemberType=MEMBER(非 ADMIN)
- callerCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
- UserId: userId, Username: username,
- IsSuperAdmin: false,
- MemberType: consts.MemberTypeMember,
- Status: consts.StatusEnabled,
- ProductCode: code,
- DeptId: 1,
- DeptPath: "/1/",
- MinPermsLevel: math.MaxInt64,
- })
- err = NewSetUserPermsLogic(callerCtx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: userId, // 给自己
- Perms: []types.UserPermItem{{PermId: permId, Effect: consts.PermEffectAllow}},
- })
- require.Error(t, err, "MEMBER 不得自我授权")
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code())
- assert.Contains(t, ce.Error(), "仅超级管理员或该产品的管理员可执行此操作")
- // 二次确认:没有任何 user_perm 记录被写入
- rows := findUserPerms(t, bootstrap, userId)
- assert.Len(t, rows, 0, "被拒绝的 SetUserPerms 不得在 DB 残留任何个性化权限")
- }
- // TC-0744: -A 修复回归 —— DEVELOPER 调用者(非 ADMIN)同样被拦截,即便目标不是自己。
- func TestSetUserPerms_DeveloperCallerRejected(t *testing.T) {
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- bootstrap := context.Background()
- now := time.Now().Unix()
- code := testutil.UniqueId()
- pRes, err := svcCtx.SysProductModel.Insert(bootstrap, &productModel.SysProduct{
- Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
- Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- pId, _ := pRes.LastInsertId()
- targetUsername := "target_" + testutil.UniqueId()
- targetId := insertTestUser(t, bootstrap, targetUsername, testutil.HashPassword("pw"))
- mId := insertTestMember(t, svcCtx, code, targetId)
- t.Cleanup(func() {
- testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
- testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
- testutil.CleanTable(bootstrap, conn, "`sys_product`", pId)
- })
- devCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
- UserId: 777777, Username: "dev_caller",
- MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled,
- ProductCode: code, DeptId: 1, DeptPath: "/1/", MinPermsLevel: math.MaxInt64,
- })
- err = NewSetUserPermsLogic(devCtx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: targetId, Perms: []types.UserPermItem{},
- })
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code())
- assert.Contains(t, ce.Error(), "仅超级管理员或该产品的管理员可执行此操作")
- }
- // TC-0745: -A 正向回归 —— 同产品 ADMIN 操作合法 MEMBER 目标(非自己)依旧放行。
- func TestSetUserPerms_ProductAdminStillWorks(t *testing.T) {
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- bootstrap := context.Background()
- now := time.Now().Unix()
- code := testutil.UniqueId()
- pRes, err := svcCtx.SysProductModel.Insert(bootstrap, &productModel.SysProduct{
- Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
- Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- pId, _ := pRes.LastInsertId()
- targetId := insertTestUser(t, bootstrap, "tgt_"+testutil.UniqueId(), testutil.HashPassword("pw"))
- mId := insertTestMember(t, svcCtx, code, targetId)
- permRes, err := svcCtx.SysPermModel.Insert(bootstrap, &permModel.SysPerm{
- ProductCode: code, Name: "ok_p", Code: "ok_" + testutil.UniqueId(),
- Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- permId, _ := permRes.LastInsertId()
- t.Cleanup(func() {
- testutil.CleanTableByField(bootstrap, conn, "`sys_user_perm`", "userId", targetId)
- testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId)
- testutil.CleanTable(bootstrap, conn, "`sys_perm`", permId)
- testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
- testutil.CleanTable(bootstrap, conn, "`sys_product`", pId)
- })
- adminCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
- UserId: 999999, Username: "admin_caller",
- MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
- ProductCode: code, DeptId: 1, DeptPath: "/1/", MinPermsLevel: math.MaxInt64,
- })
- err = NewSetUserPermsLogic(adminCtx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: targetId,
- Perms: []types.UserPermItem{{PermId: permId, Effect: consts.PermEffectAllow}},
- })
- require.NoError(t, err, "产品 ADMIN 正常路径必须放行")
- rows := findUserPerms(t, bootstrap, targetId)
- assert.Len(t, rows, 1, "ADMIN 授权后 DB 应有 1 条 user_perm")
- }
- // TC-1104: 非 ADMIN caller + 不存在的 userId —— 必须 403(不是 404),
- // L-R13-1:`RequireProductAdminFor` 先行于 `SysUserModel.FindOne(userId)`,
- // 消除通过 setUserPerms 做 userId 枚举的 oracle。
- func TestSetUserPerms_L_R13_1_AuthBeforeUserLookup(t *testing.T) {
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- // caller 持有 test_product 的 MEMBER 上下文(非 ADMIN / 非超管)
- ctx := ctxhelper.MemberCtx("test_product")
- err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: 999999999,
- Perms: []types.UserPermItem{{PermId: 1, Effect: consts.PermEffectAllow}},
- })
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code(),
- "L-R13-1:MEMBER caller 对任意 userId 都必须 403,不得返 404 泄漏用户存在性")
- assert.Contains(t, ce.Error(), "仅超级管理员或该产品的管理员可执行此操作")
- }
- // typeFlippingMemberModel 是 L-R13-2 TOCTOU 测试的窗口装饰器:
- // - FindOneByProductCodeUserId:把目标成员的 MemberType 改写为 outsideTxType(通常是 MEMBER),
- // 让前置校验通过;
- // - FindOneForShareTx:
- // insideTxForceError=true 时直接返回 error(用来断言 ALLOW-only 路径必须**跳过** S 锁);
- // 否则把 MemberType 替换为 insideTxTypeHook(通常是 ADMIN),模拟"事务内 S 锁读到
- // 并发 UpdateMember 写入的新值"。
- // - 其它方法通过匿名内嵌的 memberModel.SysProductMemberModel 接口自动透传。
- type typeFlippingMemberModel struct {
- memberModel.SysProductMemberModel
- targetMemberId int64
- outsideTxType string
- insideTxTypeHook string
- insideTxForceError bool
- insideTxForceErrorMsg string
- }
- func (m *typeFlippingMemberModel) FindOneByProductCodeUserId(ctx context.Context, productCode string, userId int64) (*memberModel.SysProductMember, error) {
- real, err := m.SysProductMemberModel.FindOneByProductCodeUserId(ctx, productCode, userId)
- if err != nil {
- return nil, err
- }
- if m.outsideTxType != "" && real.Id == m.targetMemberId {
- clone := *real
- clone.MemberType = m.outsideTxType
- return &clone, nil
- }
- return real, nil
- }
- func (m *typeFlippingMemberModel) FindOneForShareTx(ctx context.Context, session sqlx.Session, id int64) (*memberModel.SysProductMember, error) {
- if m.insideTxForceError && id == m.targetMemberId {
- return nil, errors.New(m.insideTxForceErrorMsg)
- }
- real, err := m.SysProductMemberModel.FindOneForShareTx(ctx, session, id)
- if err != nil {
- return nil, err
- }
- if m.insideTxTypeHook != "" && real.Id == m.targetMemberId {
- clone := *real
- clone.MemberType = m.insideTxTypeHook
- return &clone, nil
- }
- return real, nil
- }
- // TC-1105: L-R13-2 DENY TOCTOU 闭环——事务外读到 MEMBER,事务内 FindOneForShareTx
- // 返回 ADMIN,必须触发 400 "目标用户是产品管理员或开发者...",并且事务回滚(无 DENY 脏行)。
- //
- // 实现思路:直接在事务外保持 member=MEMBER,让前置校验通过;在 setUserPerms 进入事务的
- // 瞬间,用 sqlx 直接 UPDATE 把 memberType 改为 ADMIN(在另一连接上绕过 go-zero 缓存层)。
- // 但这种时序非常脆。更稳妥的做法:用 svcCtx.SysProductMemberModel 的装饰器,让
- // FindOneForShareTx 直接返回 MemberType="ADMIN",模拟"事务内读到并发更新后的真值"。
- func TestSetUserPerms_L_R13_2_DenyTypeFlipRollsBack(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId) // 落盘为 MEMBER
- permId := insertTestPerm(t, svcCtx, "test_product")
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", permId)
- })
- // 装饰器:事务外读(FindOneByProductCodeUserId)返 MEMBER;
- // 事务内 FindOneForShareTx 返 ADMIN,模拟并发 UpdateMember 在窗口期把目标升为 ADMIN
- // 后被 S 锁正确读到最新值。
- svcCtx.SysProductMemberModel = &typeFlippingMemberModel{
- SysProductMemberModel: svcCtx.SysProductMemberModel,
- targetMemberId: mId,
- outsideTxType: consts.MemberTypeMember,
- insideTxTypeHook: consts.MemberTypeAdmin,
- }
- err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{{PermId: permId, Effect: consts.PermEffectDeny}},
- })
- require.Error(t, err, "事务内读到 ADMIN 必须拒绝写 DENY")
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 400, ce.Code(),
- "L-R13-2:事务内 member.MemberType=ADMIN 时必须 400,不得沉默写 DENY 脏行")
- assert.Contains(t, ce.Error(), "DENY 设置不会生效")
- // 不变式:被拒绝的事务必须回滚,DB 里绝不能出现 DENY 脏行。
- rows := findUserPerms(t, ctx, userId)
- assert.Empty(t, rows,
- "L-R13-2 的核心断言:事务必须原子回滚,sys_user_perm 里不得有任何行;"+
- "若此处出现 DENY 行说明 FindOneForShareTx 没有阻塞写或事务未正确 abort")
- }
- // TC-1106: 纯 ALLOW 请求不应触发 FindOneForShareTx 的 S 锁路径(hasDeny==false 时短路)。
- // 装饰器让 FindOneForShareTx 直接返回 error —— 如果逻辑还是调了,ALLOW 请求就会失败。
- func TestSetUserPerms_L_R13_2_AllowOnlySkipsShareLock(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- permId := insertTestPerm(t, svcCtx, "test_product")
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", permId)
- })
- // 故意让 FindOneForShareTx 爆炸:只要被调就把错误带出。
- svcCtx.SysProductMemberModel = &typeFlippingMemberModel{
- SysProductMemberModel: svcCtx.SysProductMemberModel,
- targetMemberId: mId,
- outsideTxType: consts.MemberTypeMember,
- insideTxForceError: true,
- insideTxForceErrorMsg: "FindOneForShareTx must NOT be called for ALLOW-only requests",
- }
- err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{{PermId: permId, Effect: consts.PermEffectAllow}},
- })
- require.NoError(t, err,
- "纯 ALLOW 请求 hasDeny==false,必须短路、不调 FindOneForShareTx;"+
- "否则 ALLOW 也要承担一次 S 锁开销且被错误阻塞")
- rows := findUserPerms(t, ctx, userId)
- require.Len(t, rows, 1)
- assert.Equal(t, "ALLOW", rows[0].Effect)
- }
|