| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- package auth
- import (
- "context"
- "math"
- "testing"
- "perms-system-server/internal/consts"
- "perms-system-server/internal/loaders"
- "perms-system-server/internal/middleware"
- deptModel "perms-system-server/internal/model/dept"
- productmemberModel "perms-system-server/internal/model/productmember"
- userModel "perms-system-server/internal/model/user"
- "perms-system-server/internal/testutil/mocks"
- "github.com/stretchr/testify/assert"
- "go.uber.org/mock/gomock"
- )
- // ---------------------------------------------------------------------------
- // 覆盖目标:审计第 6 轮 M-5 修复回归 —— CheckManageAccess(WithPrefetchedTarget(...))
- // 允许调用方透传已经 FindOne 到的 target,避免单次请求内重复 FindOne(targetUserId)。
- //
- // 修复前:UpdateUserStatus / UpdateUser 一次请求会先做 ValidateStatusChange 里的 FindOne,
- // 紧接着 checkDeptHierarchy 里又 FindOne 一次,DB/缓存都白打一次 RTT。
- //
- // 修复后的契约:
- // * Option 与参数一致(target.Id == targetUserId)时,FindOne 必须被跳过;
- // * 不一致时 option 失效(defensive ignore),checkDeptHierarchy 回退到原有 FindOne 路径。
- // ---------------------------------------------------------------------------
- func buildMemberCallerCtx() context.Context {
- caller := &loaders.UserDetails{
- UserId: 1,
- Username: "op",
- IsSuperAdmin: false,
- MemberType: consts.MemberTypeMember,
- Status: consts.StatusEnabled,
- ProductCode: "pc_m5",
- DeptId: 100,
- DeptPath: "/100/",
- MinPermsLevel: 50,
- }
- return middleware.WithUserDetails(context.Background(), caller)
- }
- // TC-0860: 透传的 prefetched.Id 与 targetUserId 一致 → SysUserModel.FindOne 必须一次都不被调用。
- func TestCheckManageAccess_PrefetchedTarget_SkipsFindOne(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- userMock := mocks.NewMockSysUserModel(ctrl)
- // 关键断言:FindOne 次数为 0。gomock 默认不允许未声明的调用;省略 EXPECT 即相当于 0 次。
- deptMock := mocks.NewMockSysDeptModel(ctrl)
- pmMock := mocks.NewMockSysProductMemberModel(ctrl)
- roleMock := mocks.NewMockSysRoleModel(ctrl)
- // 目标用户所在部门的 Path 需满足 HasPrefix caller.DeptPath="/100/"
- deptMock.EXPECT().FindOne(gomock.Any(), int64(101)).
- Return(&deptModel.SysDept{Id: 101, Path: "/100/101/"}, nil)
- // 目标产品成员存在,MemberType=MEMBER 与 caller 同级 → 走 permsLevel 比较分支。
- pmMock.EXPECT().FindOneByProductCodeUserId(gomock.Any(), "pc_m5", int64(42)).
- Return(&productmemberModel.SysProductMember{MemberType: consts.MemberTypeMember}, nil)
- // 目标的 permsLevel 高于 caller(数值更大 → 权限更低),校验放行。
- roleMock.EXPECT().FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(42), "pc_m5").
- Return(int64(100), nil)
- // 审计 H-2:checkPermLevel 现在会对 caller 也做一次 DB fresh read。
- // caller.UserId=1,permsLevel=50(比 target=100 严格高权)→ 放行。
- roleMock.EXPECT().FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(1), "pc_m5").
- Return(int64(50), nil)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
- User: userMock, Dept: deptMock, ProductMember: pmMock, Role: roleMock,
- })
- prefetched := &userModel.SysUser{Id: 42, DeptId: 101}
- err := CheckManageAccess(buildMemberCallerCtx(), svcCtx, 42, "pc_m5", WithPrefetchedTarget(prefetched))
- assert.NoError(t, err,
- "M-5:prefetched 与 targetUserId 一致且业务级校验全部通过时应放行")
- // ctrl.Finish() 里会自动校验 userMock.FindOne 调用次数为 0(未显式 EXPECT),
- // 若源码回退到 FindOne 路径测试会抛 "unexpected call to FindOne" 直接 FAIL。
- }
- // TC-0861: 透传的 prefetched.Id 与 targetUserId 不一致 → option 被 defensive 忽略,
- // 必须真实调用 SysUserModel.FindOne(ctx, targetUserId) 一次。
- // 这是一条 "调用方把错 id 传进来时不能被当做合法 prefetched" 的安全断言:
- // 如果源码直接信任 prefetched 而不校验 Id,就会出现 "用 A 的 userDetails 去放行对 B 的管理"。
- func TestCheckManageAccess_PrefetchedIdMismatch_IgnoredAndFallsBackToFindOne(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- userMock := mocks.NewMockSysUserModel(ctrl)
- deptMock := mocks.NewMockSysDeptModel(ctrl)
- pmMock := mocks.NewMockSysProductMemberModel(ctrl)
- roleMock := mocks.NewMockSysRoleModel(ctrl)
- // 关键断言:FindOne(targetUserId=42) 必须真实被调用一次,说明 prefetched 没被盲信。
- // 我们返回的真实对象 DeptId=101(与乱传的 prefetched 一致),好让流程继续走通。
- userMock.EXPECT().FindOne(gomock.Any(), int64(42)).
- Return(&userModel.SysUser{Id: 42, DeptId: 101}, nil).Times(1)
- deptMock.EXPECT().FindOne(gomock.Any(), int64(101)).
- Return(&deptModel.SysDept{Id: 101, Path: "/100/101/"}, nil)
- pmMock.EXPECT().FindOneByProductCodeUserId(gomock.Any(), "pc_m5", int64(42)).
- Return(&productmemberModel.SysProductMember{MemberType: consts.MemberTypeMember}, nil)
- roleMock.EXPECT().FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(42), "pc_m5").
- Return(int64(100), nil)
- // 审计 H-2:caller 侧 fresh read 仍需要。
- roleMock.EXPECT().FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(1), "pc_m5").
- Return(int64(50), nil)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
- User: userMock, Dept: deptMock, ProductMember: pmMock, Role: roleMock,
- })
- // 故意传 Id=999,与 targetUserId=42 不一致。
- wrong := &userModel.SysUser{Id: 999, DeptId: 101}
- err := CheckManageAccess(buildMemberCallerCtx(), svcCtx, 42, "pc_m5", WithPrefetchedTarget(wrong))
- assert.NoError(t, err,
- "M-5:prefetched.Id 不匹配时回退 FindOne 后,本场景仍应通过业务级校验")
- }
- // 正向防御:prefetched 为 nil 时也不应 panic,且必须走 FindOne 一次(不传 option 的等价路径)。
- func TestCheckManageAccess_NilPrefetched_FallsBackToFindOne(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- userMock := mocks.NewMockSysUserModel(ctrl)
- deptMock := mocks.NewMockSysDeptModel(ctrl)
- pmMock := mocks.NewMockSysProductMemberModel(ctrl)
- roleMock := mocks.NewMockSysRoleModel(ctrl)
- userMock.EXPECT().FindOne(gomock.Any(), int64(42)).
- Return(&userModel.SysUser{Id: 42, DeptId: 101}, nil).Times(1)
- deptMock.EXPECT().FindOne(gomock.Any(), int64(101)).
- Return(&deptModel.SysDept{Id: 101, Path: "/100/101/"}, nil)
- pmMock.EXPECT().FindOneByProductCodeUserId(gomock.Any(), "pc_m5", int64(42)).
- Return(&productmemberModel.SysProductMember{MemberType: consts.MemberTypeMember}, nil)
- roleMock.EXPECT().FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(42), "pc_m5").
- Return(int64(math.MaxInt64), nil)
- // 审计 H-2:caller 侧 fresh read。
- roleMock.EXPECT().FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(1), "pc_m5").
- Return(int64(50), nil)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
- User: userMock, Dept: deptMock, ProductMember: pmMock, Role: roleMock,
- })
- err := CheckManageAccess(buildMemberCallerCtx(), svcCtx, 42, "pc_m5", WithPrefetchedTarget(nil))
- assert.NoError(t, err)
- }
|