package auth import ( "context" "errors" "testing" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" "perms-system-server/internal/response" "perms-system-server/internal/testutil/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeromicro/go-zero/core/stores/sqlx" "go.uber.org/mock/gomock" ) // --------------------------------------------------------------------------- // 覆盖目标:审计 M-3 修复 —— GuardRoleLevelAssignable 必须每次走 DB 强一致查询, // 绝不能信任 caller(loaders.UserDetails)里可能已经 stale 的 MinPermsLevel 缓存。 // // TOCTOU 场景: // 1. caller 原先是 permsLevel=5 的高阶成员。 // 2. 超管把 caller 的高阶角色摘掉,现在 DB 里 MinPermsLevel=100(低阶)。 // 3. UD 缓存还没被 Clean(Redis 抖动 / TTL 窗口内),caller.MinPermsLevel=5 是旧值。 // 4. caller 此刻尝试分配 permsLevel=50 的角色 —— 若信缓存(5 vs 50)会**误放行**; // 修复后走 DB(100 vs 50),必须 403 拦截。 // --------------------------------------------------------------------------- // TC-0930: M-3 —— stale caller.MinPermsLevel 不得影响判定,rolePermsLevel <= freshLevel 必须 403。 func TestGuardRoleLevelAssignable_StaleCallerCache_FreshDBRejects(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) const productCode = "m3_pc_stale" const callerId = int64(1001) mockRole := mocks.NewMockSysRoleModel(ctrl) // 关键:DB 强一致返回 100(被降级后的真实等级)。 mockRole.EXPECT(). FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode). Return(int64(100), nil). Times(1) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole}) caller := &loaders.UserDetails{ UserId: callerId, Username: "m3_stale_caller", MemberType: consts.MemberTypeMember, ProductCode: productCode, Status: consts.StatusEnabled, MinPermsLevel: 5, } err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 50) require.Error(t, err, "stale 缓存(5) 下试图分配 permsLevel=50,信缓存会放行;走 DB(100) 必须 403") var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code(), "M-3:拒绝分配高于自身 fresh 等级的角色 → 403") } // TC-0931: M-3 —— 同级(rolePermsLevel == freshLevel)也要拦截,保持与 checkPermLevel 的 ">=" 对齐。 func TestGuardRoleLevelAssignable_SameLevel_Rejected(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) const productCode = "m3_pc_same" const callerId = int64(1002) mockRole := mocks.NewMockSysRoleModel(ctrl) mockRole.EXPECT(). FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode). Return(int64(50), nil). Times(1) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole}) caller := &loaders.UserDetails{ UserId: callerId, Username: "m3_same_caller", MemberType: consts.MemberTypeMember, ProductCode: productCode, Status: consts.StatusEnabled, } err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 50) 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-0932: M-3 —— rolePermsLevel 严格低于 freshLevel(数值更大)时放行。 func TestGuardRoleLevelAssignable_StrictlyLower_Allowed(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) const productCode = "m3_pc_ok" const callerId = int64(1003) mockRole := mocks.NewMockSysRoleModel(ctrl) mockRole.EXPECT(). FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode). Return(int64(50), nil). Times(1) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole}) caller := &loaders.UserDetails{ UserId: callerId, Username: "m3_ok_caller", MemberType: consts.MemberTypeMember, ProductCode: productCode, Status: consts.StatusEnabled, } err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 80) require.NoError(t, err, "permsLevel=80 严格低于 freshLevel=50(数值更大 = 更低权限)应放行") } // TC-0933: M-3 —— SuperAdmin 完全豁免,不触发任何 DB 查询。 func TestGuardRoleLevelAssignable_SuperAdmin_BypassNoDBCall(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) mockRole := mocks.NewMockSysRoleModel(ctrl) // 预期 0 次调用:SuperAdmin 必须短路返回,不能浪费 DB RTT。 mockRole.EXPECT(). FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), gomock.Any(), gomock.Any()). Times(0) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole}) caller := &loaders.UserDetails{ UserId: 1, Username: "root", IsSuperAdmin: true, Status: consts.StatusEnabled, } err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 1) require.NoError(t, err, "SuperAdmin 必须放行任何 permsLevel") } // TC-0934: M-3 —— 产品 ADMIN 拥有全权,豁免 DB 查询。 func TestGuardRoleLevelAssignable_ProductAdmin_BypassNoDBCall(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) mockRole := mocks.NewMockSysRoleModel(ctrl) mockRole.EXPECT(). FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), gomock.Any(), gomock.Any()). Times(0) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole}) caller := &loaders.UserDetails{ UserId: 2, Username: "pa", MemberType: consts.MemberTypeAdmin, ProductCode: "p1", Status: consts.StatusEnabled, } err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 1) require.NoError(t, err, "产品 ADMIN 属于全权角色,必须豁免等级校验") } // TC-0935: M-3 —— DEVELOPER 同样享有全权豁免。 func TestGuardRoleLevelAssignable_Developer_BypassNoDBCall(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) mockRole := mocks.NewMockSysRoleModel(ctrl) mockRole.EXPECT(). FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), gomock.Any(), gomock.Any()). Times(0) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole}) caller := &loaders.UserDetails{ UserId: 3, Username: "dev", MemberType: consts.MemberTypeDeveloper, ProductCode: "p1", Status: consts.StatusEnabled, } err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 1) require.NoError(t, err) } // TC-0936: M-3 —— caller 在 DB 里**无任何角色**(ErrNotFound),必须 403,不能默认为 MaxInt64 放行。 // 这里的语义是"没有可分配的角色等级":一个 MEMBER 连自己都没角色,自然不能分配角色给别人。 func TestGuardRoleLevelAssignable_CallerHasNoRole_Rejected(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) const productCode = "m3_pc_noRole" const callerId = int64(1004) mockRole := mocks.NewMockSysRoleModel(ctrl) mockRole.EXPECT(). FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode). Return(int64(0), sqlx.ErrNotFound). Times(1) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole}) caller := &loaders.UserDetails{ UserId: callerId, Username: "m3_no_role", MemberType: consts.MemberTypeMember, ProductCode: productCode, Status: consts.StatusEnabled, } err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 99) require.Error(t, err, "caller 无任何角色时必须拒绝,否则会被误判为 MaxInt64 最低级从而放行任何 permsLevel") var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Contains(t, ce.Error(), "没有可分配的角色等级") } // TC-0937: M-3 —— DB 抖动(非 ErrNotFound)必须 fail-close 返回 500,不得降级为"无角色 → 放行"。 func TestGuardRoleLevelAssignable_DBError_FailCloseWith500(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) const productCode = "m3_pc_dbErr" const callerId = int64(1005) mockRole := mocks.NewMockSysRoleModel(ctrl) mockRole.EXPECT(). FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode). Return(int64(0), errors.New("driver: bad connection")). Times(1) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole}) caller := &loaders.UserDetails{ UserId: callerId, Username: "m3_db_err", MemberType: consts.MemberTypeMember, ProductCode: productCode, Status: consts.StatusEnabled, } err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 10) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 500, ce.Code(), "M-3:DB 非 ErrNotFound 错误必须 fail-close 500,不能被伪装成 ErrNotFound → 放行超权分配") } // TC-0938: M-3 —— nil caller 防御:理论上无登录上下文绝不该进入此函数,防御性路径必须 403 而非 panic。 func TestGuardRoleLevelAssignable_NilCaller_Rejected(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) mockRole := mocks.NewMockSysRoleModel(ctrl) mockRole.EXPECT(). FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), gomock.Any(), gomock.Any()). Times(0) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole}) err := GuardRoleLevelAssignable(context.Background(), svcCtx, nil, 10) require.Error(t, err, "nil caller 必须被拦截,杜绝隐式放行") var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) }