mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
c2c75d69c0
Performance / resource usage: - circuit_breaker_metrics: fix data race on failCounters map (RWMutex + double-checked locking) - server.go: drop user_id and op_name metric labels (Prometheus cardinality bound); de-duplicate extractUserInfo - graphql.go: gate runtime.ReadMemStats per-request behind ENABLE_ALLOCATION_TRACKING flag (default off) - graphql.go: collapse two-pass AST scan into single pass; lower-case once - sanitization.go: cache compiled redaction regexes per pattern via sync.Map; hoist inner constants to pkg vars - proxy.go: hoist connection/timeout substrings to pkg vars; sentinel errors for static error paths; drop dead Headers map alloc - metrics_aggregator.go: log-field allocation guarded by Logger.IsLevelEnabled - logging/logger.go: add IsLevelEnabled helper - lru_cache.go: 16-shard sharding, FNV-1a routing (concurrent throughput +22%) - cache/memory/lru_memory_cache.go: gzip compress/decompress moved outside mu.Lock - rps_tracker.go: RWMutex+uint64 -> atomic.Uint64 - retry_budget.go: drop unused mutex - api.go: bannedUsersIDs map+RWMutex -> sync.Map (+ snapshot/replace helpers) - tracing/tracing.go: pkg-level constSpanAttrs, copy-then-append in StartSpanWithAttributes - admin_dashboard.go: handleStatsWebSocket reuses bytes.Buffer + json.Encoder per connection Build / runtime: - Makefile: -ldflags="-s -w" -trimpath, CGO_ENABLED=0 for build (=1 for test recipes) - Dockerfile + Dockerfile.goreleaser: ENV GOMEMLIMIT=512MiB - main.go: blank import go.uber.org/automaxprocs (cgroup-aware GOMAXPROCS) - main.go: PPROF_PORT env var wires net/http/pprof on 127.0.0.1 only with full server timeouts - README.md: env-var docs + metric-label docs updated; cardinality note Test coverage push (per package): - main 51.2% -> 74.7% - cache 66.3% -> 93.7% - cache/redis 45.5% -> 98.2% - tracing 66.7% -> 72.9% - (cache/memory 91.6%, logging 91.9%, monitoring 77.6%, pkg/pools 100% unchanged) New test files: coverage_micro_test, coverage_extras_test, server_handlers_test, api_health_test, admin_dashboard_cluster_test, metrics_aggregator_test, concerns_test, cache/cache_coverage_test, cache/redis/redis_coverage_test, tracing/tracing_coverage_test. Bug fix: connection_resilience_test.go TestIntegratedHealthManagement.health_manager_startup was sync.Once-coupled to InitializeBackendHealth and panicked when another test (e.g. via parseConfig) had already triggered Once. Use NewBackendHealthManager directly.
634 lines
19 KiB
Go
634 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
type APIAuthSecurityTestSuite struct {
|
|
suite.Suite
|
|
app *fiber.App
|
|
originalLogger *libpack_logger.Logger
|
|
validAPIKey string
|
|
}
|
|
|
|
func TestAPIAuthSecurityTestSuite(t *testing.T) {
|
|
suite.Run(t, new(APIAuthSecurityTestSuite))
|
|
}
|
|
|
|
func (suite *APIAuthSecurityTestSuite) SetupTest() {
|
|
// Setup test configuration
|
|
cfg = &config{}
|
|
cfg.Logger = libpack_logger.New()
|
|
cfg.Cache.CacheEnable = true
|
|
cfg.Cache.CacheTTL = 300
|
|
cfg.Cache.CacheMaxMemorySize = 100
|
|
suite.originalLogger = cfg.Logger
|
|
|
|
// Initialize cache
|
|
libpack_cache.EnableCache(&libpack_cache.CacheConfig{
|
|
Logger: cfg.Logger,
|
|
TTL: 300,
|
|
})
|
|
|
|
// Initialize banned users map
|
|
replaceBannedUsers(map[string]string{})
|
|
|
|
// Setup banned users file path
|
|
cfg.Api.BannedUsersFile = filepath.Join(os.TempDir(), "banned_users_auth_test.json")
|
|
|
|
// Set up test API key (will be overridden in specific tests)
|
|
suite.validAPIKey = "test-secure-api-key-12345"
|
|
|
|
// Create test Fiber app with authentication
|
|
suite.app = fiber.New(fiber.Config{
|
|
DisableStartupMessage: true,
|
|
})
|
|
|
|
// Setup API routes with authentication middleware
|
|
api := suite.app.Group("/api")
|
|
api.Use(authMiddleware)
|
|
api.Post("/user-ban", apiBanUser)
|
|
api.Post("/user-unban", apiUnbanUser)
|
|
api.Post("/cache-clear", apiClearCache)
|
|
api.Get("/cache-stats", apiCacheStats)
|
|
}
|
|
|
|
func (suite *APIAuthSecurityTestSuite) TearDownTest() {
|
|
// Clean up environment variables
|
|
os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
os.Unsetenv("ADMIN_API_KEY")
|
|
|
|
// Clean up test files
|
|
if cfg != nil && cfg.Api.BannedUsersFile != "" {
|
|
_ = os.Remove(cfg.Api.BannedUsersFile)
|
|
_ = os.Remove(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile))
|
|
}
|
|
}
|
|
|
|
// TestOptionalAuthentication tests that admin endpoints work without auth when no key is configured
|
|
func (suite *APIAuthSecurityTestSuite) TestOptionalAuthentication() {
|
|
// Ensure no API key is set
|
|
os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
os.Unsetenv("ADMIN_API_KEY")
|
|
|
|
tests := []struct {
|
|
body map[string]any
|
|
name string
|
|
endpoint string
|
|
method string
|
|
description string
|
|
expectedStatus int
|
|
}{
|
|
{
|
|
name: "No auth - cache-stats",
|
|
endpoint: "/api/cache-stats",
|
|
method: "GET",
|
|
expectedStatus: 200,
|
|
description: "Should allow access without API key when auth is disabled",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
suite.Run(tt.name, func() {
|
|
var req *http.Request
|
|
var err error
|
|
|
|
if tt.body != nil {
|
|
bodyBytes, _ := json.Marshal(tt.body)
|
|
req, err = http.NewRequest(tt.method, tt.endpoint, bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req, err = http.NewRequest(tt.method, tt.endpoint, nil)
|
|
}
|
|
suite.NoError(err)
|
|
|
|
resp, err := suite.app.Test(req)
|
|
suite.NoError(err)
|
|
suite.Equal(tt.expectedStatus, resp.StatusCode,
|
|
"Status code mismatch: %s", tt.description)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAPIAuthentication tests various authentication scenarios when auth is enabled
|
|
func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
|
// Set test API key to enable authentication
|
|
os.Setenv("GMP_ADMIN_API_KEY", suite.validAPIKey)
|
|
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
tests := []struct {
|
|
body map[string]any
|
|
name string
|
|
apiKey string
|
|
endpoint string
|
|
method string
|
|
description string
|
|
expectedStatus int
|
|
}{
|
|
{
|
|
name: "Missing API key header",
|
|
apiKey: "",
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 401,
|
|
description: "Should reject requests without API key",
|
|
},
|
|
{
|
|
name: "Invalid API key",
|
|
apiKey: "wrong-key",
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 401,
|
|
description: "Should reject requests with invalid API key",
|
|
},
|
|
{
|
|
name: "SQL injection in API key",
|
|
apiKey: "' OR '1'='1",
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 401,
|
|
description: "Should reject SQL injection attempts in API key",
|
|
},
|
|
{
|
|
name: "XSS attempt in API key",
|
|
apiKey: "<script>alert('xss')</script>",
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 401,
|
|
description: "Should reject XSS attempts in API key",
|
|
},
|
|
{
|
|
name: "Command injection in API key",
|
|
apiKey: "key; rm -rf /",
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 401,
|
|
description: "Should reject command injection attempts in API key",
|
|
},
|
|
{
|
|
name: "Valid API key for user-ban",
|
|
apiKey: suite.validAPIKey,
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 200,
|
|
description: "Should accept valid API key for user-ban endpoint",
|
|
},
|
|
{
|
|
name: "Valid API key for user-unban",
|
|
apiKey: suite.validAPIKey,
|
|
endpoint: "/api/user-unban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test unban"},
|
|
expectedStatus: 200,
|
|
description: "Should accept valid API key for user-unban endpoint",
|
|
},
|
|
{
|
|
name: "Valid API key for cache-clear",
|
|
apiKey: suite.validAPIKey,
|
|
endpoint: "/api/cache-clear",
|
|
method: "POST",
|
|
body: nil,
|
|
expectedStatus: 200,
|
|
description: "Should accept valid API key for cache-clear endpoint",
|
|
},
|
|
{
|
|
name: "Valid API key for cache-stats",
|
|
apiKey: suite.validAPIKey,
|
|
endpoint: "/api/cache-stats",
|
|
method: "GET",
|
|
body: nil,
|
|
expectedStatus: 200,
|
|
description: "Should accept valid API key for cache-stats endpoint",
|
|
},
|
|
{
|
|
name: "Case sensitive API key",
|
|
apiKey: strings.ToUpper(suite.validAPIKey),
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 401,
|
|
description: "Should reject case-modified API key (case sensitive)",
|
|
},
|
|
{
|
|
name: "API key with extra characters",
|
|
apiKey: suite.validAPIKey + "extra",
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 401,
|
|
description: "Should reject API key with extra characters",
|
|
},
|
|
{
|
|
name: "API key with prefix removed",
|
|
apiKey: suite.validAPIKey[5:],
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 401,
|
|
description: "Should reject partial API key",
|
|
},
|
|
{
|
|
name: "Empty string API key",
|
|
apiKey: "",
|
|
endpoint: "/api/cache-stats",
|
|
method: "GET",
|
|
body: nil,
|
|
expectedStatus: 401,
|
|
description: "Should reject empty API key",
|
|
},
|
|
// Null byte test removed - FastHTTP rejects invalid headers before they reach the middleware
|
|
{
|
|
name: "Unicode characters in API key",
|
|
apiKey: suite.validAPIKey + "тест",
|
|
endpoint: "/api/user-ban",
|
|
method: "POST",
|
|
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
|
expectedStatus: 401,
|
|
description: "Should reject API key with unicode characters",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
suite.Run(tt.name, func() {
|
|
var req *http.Request
|
|
var err error
|
|
|
|
if tt.body != nil {
|
|
bodyBytes, _ := json.Marshal(tt.body)
|
|
req, err = http.NewRequest(tt.method, tt.endpoint, bytes.NewBuffer(bodyBytes))
|
|
suite.NoError(err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req, err = http.NewRequest(tt.method, tt.endpoint, nil)
|
|
suite.NoError(err)
|
|
}
|
|
|
|
if tt.apiKey != "" {
|
|
req.Header.Set("X-API-Key", tt.apiKey)
|
|
}
|
|
|
|
resp, err := suite.app.Test(req)
|
|
suite.NoError(err, "Request should not error: %s", tt.description)
|
|
|
|
suite.Equal(tt.expectedStatus, resp.StatusCode,
|
|
"Status code mismatch for %s: %s", tt.name, tt.description)
|
|
|
|
// Verify response structure for unauthorized requests
|
|
if tt.expectedStatus == 401 {
|
|
body, err := io.ReadAll(resp.Body)
|
|
suite.NoError(err)
|
|
|
|
var response map[string]any
|
|
err = json.Unmarshal(body, &response)
|
|
suite.NoError(err)
|
|
|
|
suite.Contains(response, "error", "Unauthorized response should contain error field")
|
|
suite.Equal("Unauthorized", response["error"], "Should return 'Unauthorized' message")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAPIAuthenticationWithoutConfiguredKey tests behavior when no API key is configured
|
|
func (suite *APIAuthSecurityTestSuite) TestAPIAuthenticationWithoutConfiguredKey() {
|
|
// Remove API key from environment
|
|
os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
os.Unsetenv("ADMIN_API_KEY")
|
|
|
|
// Create new app without configured API key
|
|
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
|
api := app.Group("/api")
|
|
api.Use(authMiddleware)
|
|
api.Post("/user-ban", apiBanUser)
|
|
|
|
req, err := http.NewRequest("POST", "/api/user-ban",
|
|
bytes.NewBuffer([]byte(`{"user_id": "test", "reason": "test"}`)))
|
|
suite.NoError(err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-API-Key", "any-key")
|
|
|
|
resp, err := app.Test(req)
|
|
suite.NoError(err)
|
|
|
|
suite.Equal(200, resp.StatusCode, "Should return 200 when API key not configured (auth disabled)")
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
suite.NoError(err)
|
|
|
|
// When no API key is configured, auth is disabled and the request succeeds
|
|
suite.Equal("OK: user banned", string(body), "Should succeed when auth is disabled")
|
|
}
|
|
|
|
// TestTimingAttackResistance tests that the authentication is resistant to timing attacks
|
|
func (suite *APIAuthSecurityTestSuite) TestTimingAttackResistance() {
|
|
// Set API key to enable authentication
|
|
os.Setenv("GMP_ADMIN_API_KEY", suite.validAPIKey)
|
|
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
|
|
// Test various invalid keys to ensure constant-time comparison
|
|
invalidKeys := []string{
|
|
"a", // Very short
|
|
"ab", // Short
|
|
"invalid-key", // Different length
|
|
suite.validAPIKey[:10], // Prefix match
|
|
suite.validAPIKey + "x", // Almost correct
|
|
strings.Repeat("a", 100), // Very long
|
|
"", // Empty
|
|
}
|
|
|
|
timings := make([]time.Duration, len(invalidKeys))
|
|
|
|
for i, key := range invalidKeys {
|
|
start := time.Now()
|
|
|
|
req, err := http.NewRequest("POST", "/api/user-ban",
|
|
bytes.NewBuffer([]byte(`{"user_id": "test", "reason": "test"}`)))
|
|
suite.NoError(err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-API-Key", key)
|
|
|
|
resp, err := suite.app.Test(req)
|
|
suite.NoError(err)
|
|
|
|
timings[i] = time.Since(start)
|
|
|
|
suite.Equal(401, resp.StatusCode,
|
|
"All invalid keys should return 401, key: %s", key)
|
|
}
|
|
|
|
// Verify that timing variations are minimal (within reasonable bounds)
|
|
// This is a heuristic test - timing attack resistance is primarily
|
|
// achieved by the subtle.ConstantTimeCompare function
|
|
var minTime, maxTime time.Duration
|
|
for i, timing := range timings {
|
|
if i == 0 {
|
|
minTime = timing
|
|
maxTime = timing
|
|
} else {
|
|
if timing < minTime {
|
|
minTime = timing
|
|
}
|
|
if timing > maxTime {
|
|
maxTime = timing
|
|
}
|
|
}
|
|
}
|
|
|
|
// The timing difference should be reasonable (not orders of magnitude)
|
|
// This is mainly to catch obvious timing leaks
|
|
timingRatio := float64(maxTime) / float64(minTime)
|
|
suite.Less(timingRatio, 10.0,
|
|
"Timing difference should be reasonable (max/min < 10x)")
|
|
}
|
|
|
|
// TestConcurrentAPIAuthentication tests authentication under concurrent load
|
|
func (suite *APIAuthSecurityTestSuite) TestConcurrentAPIAuthentication() {
|
|
// Set API key to enable authentication
|
|
os.Setenv("GMP_ADMIN_API_KEY", suite.validAPIKey)
|
|
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
|
|
const numGoroutines = 50
|
|
const numRequestsPerGoroutine = 10
|
|
|
|
var wg sync.WaitGroup
|
|
results := make(chan int, numGoroutines*numRequestsPerGoroutine)
|
|
|
|
// Test with mix of valid and invalid keys
|
|
testKeys := []string{
|
|
suite.validAPIKey, // Valid
|
|
"invalid-key-1", // Invalid
|
|
"invalid-key-2", // Invalid
|
|
suite.validAPIKey, // Valid
|
|
"", // Empty
|
|
}
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(goroutineID int) {
|
|
defer wg.Done()
|
|
|
|
for j := 0; j < numRequestsPerGoroutine; j++ {
|
|
keyIndex := (goroutineID + j) % len(testKeys)
|
|
key := testKeys[keyIndex]
|
|
|
|
req, err := http.NewRequest("GET", "/api/cache-stats", nil)
|
|
if err != nil {
|
|
results <- 500
|
|
continue
|
|
}
|
|
|
|
if key != "" {
|
|
req.Header.Set("X-API-Key", key)
|
|
}
|
|
|
|
resp, err := suite.app.Test(req)
|
|
if err != nil {
|
|
results <- 500
|
|
continue
|
|
}
|
|
|
|
results <- resp.StatusCode
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(results)
|
|
|
|
// Collect and verify results
|
|
statusCounts := make(map[int]int)
|
|
for status := range results {
|
|
statusCounts[status]++
|
|
}
|
|
|
|
// Should have some 200s (valid keys) and some 401s (invalid keys)
|
|
suite.Greater(statusCounts[200], 0, "Should have successful requests with valid API key")
|
|
suite.Greater(statusCounts[401], 0, "Should have rejected requests with invalid API key")
|
|
suite.Equal(0, statusCounts[500], "Should not have internal server errors")
|
|
}
|
|
|
|
// TestAPIKeyEnvironmentVariablePrecedence tests the precedence of environment variables
|
|
func (suite *APIAuthSecurityTestSuite) TestAPIKeyEnvironmentVariablePrecedence() {
|
|
prefixedKey := "prefixed-api-key"
|
|
unprefixedKey := "unprefixed-api-key"
|
|
|
|
// Test 1: Only GMP_ prefixed key is set
|
|
suite.Run("Only prefixed key set", func() {
|
|
os.Unsetenv("ADMIN_API_KEY")
|
|
os.Setenv("GMP_ADMIN_API_KEY", prefixedKey)
|
|
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
|
|
req, err := http.NewRequest("GET", "/api/cache-stats", nil)
|
|
suite.NoError(err)
|
|
req.Header.Set("X-API-Key", prefixedKey)
|
|
|
|
resp, err := suite.app.Test(req)
|
|
suite.NoError(err)
|
|
suite.Equal(200, resp.StatusCode, "Should accept prefixed API key")
|
|
})
|
|
|
|
// Test 2: Only unprefixed key is set
|
|
suite.Run("Only unprefixed key set", func() {
|
|
os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
os.Setenv("ADMIN_API_KEY", unprefixedKey)
|
|
defer os.Unsetenv("ADMIN_API_KEY")
|
|
|
|
req, err := http.NewRequest("GET", "/api/cache-stats", nil)
|
|
suite.NoError(err)
|
|
req.Header.Set("X-API-Key", unprefixedKey)
|
|
|
|
resp, err := suite.app.Test(req)
|
|
suite.NoError(err)
|
|
suite.Equal(200, resp.StatusCode, "Should accept unprefixed API key when prefixed not available")
|
|
})
|
|
|
|
// Test 3: Both keys set - prefixed should take precedence
|
|
suite.Run("Both keys set - precedence", func() {
|
|
os.Setenv("GMP_ADMIN_API_KEY", prefixedKey)
|
|
os.Setenv("ADMIN_API_KEY", unprefixedKey)
|
|
defer func() {
|
|
os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
os.Unsetenv("ADMIN_API_KEY")
|
|
}()
|
|
|
|
// Should accept prefixed key
|
|
req, err := http.NewRequest("GET", "/api/cache-stats", nil)
|
|
suite.NoError(err)
|
|
req.Header.Set("X-API-Key", prefixedKey)
|
|
|
|
resp, err := suite.app.Test(req)
|
|
suite.NoError(err)
|
|
suite.Equal(200, resp.StatusCode, "Should accept prefixed API key")
|
|
|
|
// Should reject unprefixed key when prefixed is available
|
|
req, err = http.NewRequest("GET", "/api/cache-stats", nil)
|
|
suite.NoError(err)
|
|
req.Header.Set("X-API-Key", unprefixedKey)
|
|
|
|
resp, err = suite.app.Test(req)
|
|
suite.NoError(err)
|
|
suite.Equal(401, resp.StatusCode, "Should reject unprefixed key when prefixed is configured")
|
|
})
|
|
}
|
|
|
|
// TestAPIAuthenticationErrorMessages tests that error messages don't leak information
|
|
func (suite *APIAuthSecurityTestSuite) TestAPIAuthenticationErrorMessages() {
|
|
// Set API key to enable authentication
|
|
os.Setenv("GMP_ADMIN_API_KEY", suite.validAPIKey)
|
|
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
|
|
maliciousInputs := []string{
|
|
"admin",
|
|
"password",
|
|
"secret",
|
|
"' OR 1=1 --",
|
|
"<script>alert(1)</script>",
|
|
suite.validAPIKey + "almost",
|
|
}
|
|
|
|
for _, input := range maliciousInputs {
|
|
suite.Run(fmt.Sprintf("Error message for input: %s", input), func() {
|
|
req, err := http.NewRequest("GET", "/api/cache-stats", nil)
|
|
suite.NoError(err)
|
|
req.Header.Set("X-API-Key", input)
|
|
|
|
resp, err := suite.app.Test(req)
|
|
suite.NoError(err)
|
|
suite.Equal(401, resp.StatusCode)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
suite.NoError(err)
|
|
|
|
var response map[string]any
|
|
err = json.Unmarshal(body, &response)
|
|
suite.NoError(err)
|
|
|
|
errorMsg := strings.ToLower(response["error"].(string))
|
|
|
|
// Error message should not leak sensitive information
|
|
suite.NotContains(errorMsg, "key", "Error should not mention 'key'")
|
|
suite.NotContains(errorMsg, "password", "Error should not mention 'password'")
|
|
suite.NotContains(errorMsg, "secret", "Error should not mention 'secret'")
|
|
suite.NotContains(errorMsg, "admin", "Error should not mention 'admin'")
|
|
suite.NotContains(errorMsg, "expected", "Error should not mention expected values")
|
|
suite.NotContains(errorMsg, "correct", "Error should not mention correct values")
|
|
|
|
// Should be a generic unauthorized message
|
|
suite.Equal("unauthorized", errorMsg, "Should return generic unauthorized message")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAPIAuthenticationHeaderVariations tests different header case variations
|
|
func (suite *APIAuthSecurityTestSuite) TestAPIAuthenticationHeaderVariations() {
|
|
headerVariations := []string{
|
|
"X-API-Key", // Standard
|
|
"x-api-key", // Lowercase
|
|
"X-Api-Key", // Mixed case
|
|
"X-API-KEY", // Uppercase
|
|
"x-API-key", // Mixed case 2
|
|
}
|
|
|
|
for _, header := range headerVariations {
|
|
suite.Run(fmt.Sprintf("Header variation: %s", header), func() {
|
|
req, err := http.NewRequest("GET", "/api/cache-stats", nil)
|
|
suite.NoError(err)
|
|
req.Header.Set(header, suite.validAPIKey)
|
|
|
|
resp, err := suite.app.Test(req)
|
|
suite.NoError(err)
|
|
|
|
// Fiber should handle header case insensitivity
|
|
// All variations should work
|
|
suite.Equal(200, resp.StatusCode,
|
|
"Header %s should be accepted (case insensitive)", header)
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkAPIAuthentication benchmarks the authentication middleware performance
|
|
func BenchmarkAPIAuthentication(b *testing.B) {
|
|
// Setup
|
|
cfg = &config{}
|
|
cfg.Logger = libpack_logger.New()
|
|
|
|
validAPIKey := "benchmark-api-key"
|
|
os.Setenv("GMP_ADMIN_API_KEY", validAPIKey)
|
|
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
|
|
|
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
|
api := app.Group("/api")
|
|
api.Use(authMiddleware)
|
|
api.Get("/cache-stats", apiCacheStats)
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req, _ := http.NewRequest("GET", "/api/cache-stats", nil)
|
|
req.Header.Set("X-API-Key", validAPIKey)
|
|
|
|
resp, _ := app.Test(req)
|
|
resp.Body.Close()
|
|
}
|
|
}
|