grpc_rate_limit_mr11_1_audit_test.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. package server
  2. import (
  3. "context"
  4. "testing"
  5. "perms-system-server/internal/svc"
  6. "perms-system-server/internal/testutil"
  7. "perms-system-server/pb"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. "github.com/zeromicro/go-zero/core/limit"
  11. "github.com/zeromicro/go-zero/core/stores/redis"
  12. "google.golang.org/grpc/codes"
  13. "google.golang.org/grpc/status"
  14. )
  15. // ---------------------------------------------------------------------------
  16. // 覆盖目标:审计 M-R11-1 —— gRPC SyncPermissions / GetUserPerms 入口缺少限流。
  17. // 修复后:
  18. // SyncPermissions:按 appKey 维度做 GrpcSyncLimiter 入口限流,超限 ResourceExhausted;
  19. // 限流在 bcrypt.Compare(appSecret) / LockByCodeTx 之前执行,避免
  20. // 恶意重放以高 CPU / 事务级 X 锁打满。
  21. // GetUserPerms :按 appKey + 源 IP 双维度做 GrpcGetUserPermsLimiter 限流,任一桶超限
  22. // 都拒绝;防止合法 appKey 泄漏后遍历 userId 或多实例 DDoS 放大。
  23. //
  24. // 这里每个契约都做两件事:
  25. // 1) 把限流上限调到 quota=1 并观察第 2 次请求必是 ResourceExhausted;
  26. // 2) 提供"第 3 次换一个完全不相关的桶键"必须放行,证明限流口径是**按 key**隔离的,
  27. // 不是简单全局计数。
  28. // ---------------------------------------------------------------------------
  29. // TC-1052: M-R11-1 —— SyncPermissions 的 appKey 维度限流
  30. func TestGrpcSyncPermissions_AppKeyRateLimit(t *testing.T) {
  31. ctx := context.Background()
  32. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  33. cfg := testutil.GetTestConfig()
  34. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  35. svcCtx.GrpcSyncLimiter = limit.NewPeriodLimit(
  36. 60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:grpc:sync:ut:"+testutil.UniqueId())
  37. srv := NewPermServer(svcCtx)
  38. // 同一 appKey 的第 1 次:limiter 放行,业务层因 appKey 非法走 Unauthenticated。
  39. appKey := "unknown_" + testutil.UniqueId()
  40. _, err1 := srv.SyncPermissions(ctx, &pb.SyncPermissionsReq{
  41. AppKey: appKey, AppSecret: "anything",
  42. Perms: []*pb.PermItem{{Code: "p.a", Name: "A"}},
  43. })
  44. require.Error(t, err1)
  45. st1, _ := status.FromError(err1)
  46. assert.Equal(t, codes.Unauthenticated, st1.Code(),
  47. "首次 limiter 放行,业务应因 appKey 不存在 Unauthenticated,非 ResourceExhausted")
  48. // 同一 appKey 的第 2 次:必是 ResourceExhausted。
  49. _, err2 := srv.SyncPermissions(ctx, &pb.SyncPermissionsReq{
  50. AppKey: appKey, AppSecret: "whatever",
  51. Perms: []*pb.PermItem{{Code: "p.b", Name: "B"}},
  52. })
  53. require.Error(t, err2)
  54. st2, _ := status.FromError(err2)
  55. assert.Equal(t, codes.ResourceExhausted, st2.Code(),
  56. "M-R11-1:同 appKey 达到配额必须 ResourceExhausted;严禁恶意方反复重放触发 bcrypt / X 锁")
  57. assert.Contains(t, st2.Message(), "过于频繁")
  58. // 另一 appKey 放行:证明 limiter 按 appKey 隔离,不是全局计数器。
  59. otherKey := "unknown_other_" + testutil.UniqueId()
  60. _, err3 := srv.SyncPermissions(ctx, &pb.SyncPermissionsReq{
  61. AppKey: otherKey, AppSecret: "whatever",
  62. Perms: []*pb.PermItem{{Code: "p.c", Name: "C"}},
  63. })
  64. require.Error(t, err3)
  65. st3, _ := status.FromError(err3)
  66. assert.Equal(t, codes.Unauthenticated, st3.Code(),
  67. "M-R11-1:limiter 桶键形如 'grpc:sync:<appKey>',不同 appKey 互不串扰")
  68. }
  69. // TC-1053: M-R11-1 —— SyncPermissions 空 AppKey 不消耗 limiter 配额
  70. // 代码里 `if req.AppKey != "" { Take(...) }` 的两层防护:
  71. // 1) 恶意方用空串连续打,不会把 limiter key space 膨胀为一个永不过期的"空串大桶";
  72. // 2) 业务层统一由 FindOneByAppKey("") 命中 ErrNotFound 返回 Unauthenticated。
  73. // 契约:空 AppKey 连打 3 次后,quota=1 的 limiter 仍然是**全新**状态;任意新 appKey 的第一次
  74. // 请求必须走业务层(Unauthenticated),绝不允许被 ResourceExhausted 截断。
  75. func TestGrpcSyncPermissions_EmptyAppKeyDoesNotConsumeQuota(t *testing.T) {
  76. ctx := context.Background()
  77. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  78. cfg := testutil.GetTestConfig()
  79. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  80. svcCtx.GrpcSyncLimiter = limit.NewPeriodLimit(
  81. 60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:grpc:sync:empty:"+testutil.UniqueId())
  82. srv := NewPermServer(svcCtx)
  83. for i := 0; i < 3; i++ {
  84. _, err := srv.SyncPermissions(ctx, &pb.SyncPermissionsReq{
  85. AppKey: "", AppSecret: "x",
  86. Perms: []*pb.PermItem{{Code: "p", Name: "n"}},
  87. })
  88. require.Error(t, err)
  89. st, _ := status.FromError(err)
  90. assert.Equal(t, codes.Unauthenticated, st.Code(),
  91. "空 AppKey 走 FindOneByAppKey('') → Unauthenticated;此路径不得触达 limiter")
  92. }
  93. // 真实新 AppKey 的第 1 次请求必须得到业务层的 Unauthenticated,
  94. // 而不是因"空串占用配额"退化出的 ResourceExhausted。
  95. realKey := "sync_empty_probe_" + testutil.UniqueId()
  96. _, err := srv.SyncPermissions(ctx, &pb.SyncPermissionsReq{
  97. AppKey: realKey, AppSecret: "x",
  98. Perms: []*pb.PermItem{{Code: "p", Name: "n"}},
  99. })
  100. require.Error(t, err)
  101. st, _ := status.FromError(err)
  102. assert.Equal(t, codes.Unauthenticated, st.Code(),
  103. "M-R11-1:空 AppKey 不消耗 limiter 配额;若这里返回 ResourceExhausted 则说明"+
  104. "空串也被计数,`req.AppKey != \"\"` 前置分支缺失或被回退")
  105. }
  106. // TC-1054: M-R11-1 —— GetUserPerms 的 appKey 维度限流
  107. func TestGrpcGetUserPerms_AppKeyRateLimit(t *testing.T) {
  108. ctx := context.Background()
  109. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  110. cfg := testutil.GetTestConfig()
  111. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  112. svcCtx.GrpcGetUserPermsLimiter = limit.NewPeriodLimit(
  113. 60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:grpc:perms:ut:"+testutil.UniqueId())
  114. srv := NewPermServer(svcCtx)
  115. appKey := "perms_ak_" + testutil.UniqueId()
  116. ctx1 := withPeerIP(ctx, "172.31.0.10:40001")
  117. _, err1 := srv.GetUserPerms(ctx1, &pb.GetUserPermsReq{
  118. AppKey: appKey, AppSecret: "x", ProductCode: "test_product", UserId: 1,
  119. })
  120. require.Error(t, err1)
  121. st1, _ := status.FromError(err1)
  122. assert.Equal(t, codes.Unauthenticated, st1.Code(),
  123. "首次放行,业务层应因 appKey 不存在 Unauthenticated")
  124. // 同 appKey 第二次:appKey 桶即告罄。
  125. ctx2 := withPeerIP(ctx, "172.31.0.11:40002") // 换 IP,证明拦的是 appKey 桶而不是 IP 桶
  126. _, err2 := srv.GetUserPerms(ctx2, &pb.GetUserPermsReq{
  127. AppKey: appKey, AppSecret: "x", ProductCode: "test_product", UserId: 2,
  128. })
  129. require.Error(t, err2)
  130. st2, _ := status.FromError(err2)
  131. assert.Equal(t, codes.ResourceExhausted, st2.Code(),
  132. "M-R11-1:同 appKey 达到 appKey 维度配额,必须 ResourceExhausted")
  133. }
  134. // TC-1055: M-R11-1 —— GetUserPerms 的 IP 维度限流
  135. // 双维度叠加意味着:若 appKey 维度没爆但 IP 维度爆了,同样必须拒绝。
  136. // 这里用两个不同 appKey(消耗两份 appKey 配额,各占 1 个)但共用同一源 IP,
  137. // 第 2 次因为 IP 桶也只剩 1 个配额而必定 ResourceExhausted。
  138. func TestGrpcGetUserPerms_IPRateLimit_OrthogonalToAppKey(t *testing.T) {
  139. ctx := context.Background()
  140. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  141. cfg := testutil.GetTestConfig()
  142. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  143. // quota=1:一次调用会消耗 appKey 桶 + IP 桶 各 1 个;
  144. // 第 2 次用"新 appKey"但"同 IP",appKey 桶还够,IP 桶已见底 → IP 桶拒绝。
  145. svcCtx.GrpcGetUserPermsLimiter = limit.NewPeriodLimit(
  146. 60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:grpc:perms:ip:"+testutil.UniqueId())
  147. srv := NewPermServer(svcCtx)
  148. appKeyA := "perms_ak_a_" + testutil.UniqueId()
  149. appKeyB := "perms_ak_b_" + testutil.UniqueId()
  150. ctxSameIP1 := withPeerIP(ctx, "198.51.100.7:50001")
  151. ctxSameIP2 := withPeerIP(ctx, "198.51.100.7:50002") // 同 IP 不同端口
  152. _, err1 := srv.GetUserPerms(ctxSameIP1, &pb.GetUserPermsReq{
  153. AppKey: appKeyA, AppSecret: "x", ProductCode: "test_product", UserId: 1,
  154. })
  155. require.Error(t, err1)
  156. st1, _ := status.FromError(err1)
  157. assert.Equal(t, codes.Unauthenticated, st1.Code(),
  158. "首次放行(appKey 桶 + IP 桶各耗 1 个)")
  159. // 第 2 次:appKey 不同(appKey 桶还有配额),但同 IP 的 IP 桶已耗尽。
  160. _, err2 := srv.GetUserPerms(ctxSameIP2, &pb.GetUserPermsReq{
  161. AppKey: appKeyB, AppSecret: "x", ProductCode: "test_product", UserId: 2,
  162. })
  163. require.Error(t, err2)
  164. st2, _ := status.FromError(err2)
  165. assert.Equal(t, codes.ResourceExhausted, st2.Code(),
  166. "M-R11-1:appKey 桶有余但 IP 桶已爆,必须 ResourceExhausted;双维度是'谁先爆谁拒'")
  167. }