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