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) }