invalid_auth_abuse_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. //go:build unit
  2. package middleware
  3. import (
  4. "context"
  5. "errors"
  6. "net/http"
  7. "net/http/httptest"
  8. "testing"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. "github.com/stretchr/testify/require"
  13. )
  14. func invalidAuthAbuseTestConfig(threshold int) *config.Config {
  15. return &config.Config{
  16. RunMode: config.RunModeSimple,
  17. APIKeyAuth: config.APIKeyAuthCacheConfig{InvalidAbuse: config.InvalidAuthAbuseConfig{
  18. Enabled: true, Threshold: threshold, WindowSeconds: 60, BlockSeconds: 60, Capacity: 256,
  19. }},
  20. }
  21. }
  22. func TestAPIKeyAuthInvalidAbuseReturns429BeforeRepository(t *testing.T) {
  23. gin.SetMode(gin.TestMode)
  24. repoCalls := 0
  25. repo := &stubApiKeyRepo{getByKey: func(context.Context, string) (*service.APIKey, error) {
  26. repoCalls++
  27. return nil, service.ErrAPIKeyNotFound
  28. }}
  29. cfg := invalidAuthAbuseTestConfig(3)
  30. svc := service.NewAPIKeyService(repo, nil, nil, nil, nil, nil, cfg)
  31. r := gin.New()
  32. var reason IngressRejectReason
  33. r.Use(func(c *gin.Context) { c.Next(); reason, _ = GetIngressRejectReason(c) })
  34. r.Use(gin.HandlerFunc(NewAPIKeyAuthMiddleware(svc, nil, cfg)))
  35. r.POST("/v1/messages", func(c *gin.Context) { c.Status(http.StatusOK) })
  36. requests := []*http.Request{
  37. httpRequest(t, "/v1/messages", "", ""),
  38. httpRequest(t, "/v1/messages", "Basic malformed", ""),
  39. httpRequest(t, "/v1/messages", "", "random-invalid-key"),
  40. }
  41. for _, req := range requests {
  42. w := httptest.NewRecorder()
  43. r.ServeHTTP(w, req)
  44. require.NotEqual(t, http.StatusTooManyRequests, w.Code)
  45. }
  46. w := httptest.NewRecorder()
  47. r.ServeHTTP(w, httpRequest(t, "/v1/messages", "", "another-random-key"))
  48. require.Equal(t, http.StatusTooManyRequests, w.Code)
  49. require.Equal(t, "60", w.Header().Get("Retry-After"))
  50. require.Contains(t, w.Body.String(), "INVALID_AUTH_RATE_LIMITED")
  51. require.Equal(t, IngressRejectInvalidAuthRateLimited, reason)
  52. require.Equal(t, 1, repoCalls, "rate-limited request must not reach the repository")
  53. }
  54. func TestGoogleAPIKeyAuthInvalidAbuseReturnsProtocol429(t *testing.T) {
  55. gin.SetMode(gin.TestMode)
  56. repoCalls := 0
  57. repo := fakeAPIKeyRepo{getByKey: func(context.Context, string) (*service.APIKey, error) {
  58. repoCalls++
  59. return nil, service.ErrAPIKeyNotFound
  60. }}
  61. cfg := invalidAuthAbuseTestConfig(2)
  62. svc := service.NewAPIKeyService(repo, nil, nil, nil, nil, nil, cfg)
  63. r := gin.New()
  64. var reason IngressRejectReason
  65. r.Use(func(c *gin.Context) { c.Next(); reason, _ = GetIngressRejectReason(c) })
  66. r.Use(APIKeyAuthGoogle(svc, cfg))
  67. r.POST("/v1beta/models/test:generateContent", func(c *gin.Context) { c.Status(http.StatusOK) })
  68. for _, key := range []string{"random-1", "random-2"} {
  69. w := httptest.NewRecorder()
  70. req := httpRequest(t, "/v1beta/models/test:generateContent", "", key)
  71. req.Header.Del("x-api-key")
  72. req.Header.Set("x-goog-api-key", key)
  73. r.ServeHTTP(w, req)
  74. require.Equal(t, http.StatusUnauthorized, w.Code)
  75. }
  76. w := httptest.NewRecorder()
  77. req := httpRequest(t, "/v1beta/models/test:generateContent", "", "random-3")
  78. req.Header.Del("x-api-key")
  79. req.Header.Set("x-goog-api-key", "random-3")
  80. r.ServeHTTP(w, req)
  81. require.Equal(t, http.StatusTooManyRequests, w.Code)
  82. require.Equal(t, "60", w.Header().Get("Retry-After"))
  83. require.Contains(t, w.Body.String(), "RESOURCE_EXHAUSTED")
  84. require.Equal(t, IngressRejectInvalidAuthRateLimited, reason)
  85. require.Equal(t, 2, repoCalls)
  86. }
  87. func TestInvalidAuthAbuseDoesNotCountValidOrOperationalFailures(t *testing.T) {
  88. gin.SetMode(gin.TestMode)
  89. user := &service.User{ID: 1, Status: service.StatusActive, Role: service.RoleUser, Balance: 1}
  90. repo := &stubApiKeyRepo{getByKey: func(_ context.Context, key string) (*service.APIKey, error) {
  91. switch key {
  92. case "valid-key":
  93. return &service.APIKey{ID: 1, UserID: 1, Key: key, Status: service.StatusActive, User: user}, nil
  94. case "db-error":
  95. return nil, errors.New("database unavailable")
  96. default:
  97. return nil, service.ErrAPIKeyNotFound
  98. }
  99. }}
  100. cfg := invalidAuthAbuseTestConfig(10)
  101. svc := service.NewAPIKeyService(repo, nil, nil, nil, nil, nil, cfg)
  102. r := gin.New()
  103. r.Use(gin.HandlerFunc(NewAPIKeyAuthMiddleware(svc, nil, cfg)))
  104. r.POST("/t", func(c *gin.Context) { c.Status(http.StatusOK) })
  105. for _, tc := range []struct {
  106. key string
  107. want int
  108. }{{"invalid", 401}, {"valid-key", 200}, {"db-error", 500}, {"db-error", 500}} {
  109. w := httptest.NewRecorder()
  110. r.ServeHTTP(w, httpRequest(t, "/t", "", tc.key))
  111. require.Equal(t, tc.want, w.Code)
  112. }
  113. w := httptest.NewRecorder()
  114. req := httpRequest(t, "/t", "", "")
  115. req.Header.Set("x-goog-api-key", "valid-key")
  116. r.ServeHTTP(w, req)
  117. require.Equal(t, http.StatusOK, w.Code)
  118. require.Equal(t, uint64(1), svc.InvalidAuthAbuseHealth().Recorded)
  119. }
  120. func TestNormalizeIngressRejectIPGroupsIPv6By64(t *testing.T) {
  121. require.Equal(t, "2001:db8:abcd:1234::", normalizeIngressRejectIP("2001:db8:abcd:1234:1111::1"))
  122. require.Equal(t, normalizeIngressRejectIP("2001:db8:abcd:1234:1111::1"), normalizeIngressRejectIP("2001:db8:abcd:1234:ffff::2"))
  123. }
  124. func httpRequest(t *testing.T, path, authorization, apiKey string) *http.Request {
  125. t.Helper()
  126. req := httptest.NewRequest(http.MethodPost, path, nil)
  127. req.RemoteAddr = "203.0.113.10:12345"
  128. if authorization != "" {
  129. req.Header.Set("Authorization", authorization)
  130. }
  131. if apiKey != "" {
  132. req.Header.Set("x-api-key", apiKey)
  133. }
  134. return req
  135. }