Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
318 changes: 318 additions & 0 deletions api-server/tests/rate_limit_bypass_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
//! Tests for rate-limit bypass prevention and authenticated endpoint rate limiting
//! Tests for #907 (rate-limit bypass test for authenticated high-privilege endpoints)

#[cfg(test)]
mod rate_limit_bypass_tests {
use std::collections::HashMap;

// Mock structures for rate limiting tests
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RateLimitTier {
Free,
Premium,
Enterprise,
}

#[derive(Debug, Clone, Copy)]
pub struct BucketQuota {
pub requests_per_minute: u32,
pub burst: u32,
}

pub struct AuthenticatedClient {
pub api_key: String,
pub tier: RateLimitTier,
}

pub struct RateLimitChecker {
pub per_ip_limit: BucketQuota,
pub per_api_key_limit: BucketQuota,
pub tier_limits: HashMap<RateLimitTier, BucketQuota>,
}

impl RateLimitChecker {
pub fn new() -> Self {
let mut tier_limits = HashMap::new();
tier_limits.insert(RateLimitTier::Free, BucketQuota {
requests_per_minute: 60,
burst: 30,
});
tier_limits.insert(RateLimitTier::Premium, BucketQuota {
requests_per_minute: 600,
burst: 200,
});
tier_limits.insert(RateLimitTier::Enterprise, BucketQuota {
requests_per_minute: 6000,
burst: 1000,
});

Self {
per_ip_limit: BucketQuota {
requests_per_minute: 300,
burst: 100,
},
per_api_key_limit: BucketQuota {
requests_per_minute: 600,
burst: 200,
},
tier_limits,
}
}

/// Check if an authenticated request should be rate limited.
/// Must enforce BOTH per-IP and per-API-key limits.
pub fn should_rate_limit_authenticated(
&self,
client: &AuthenticatedClient,
source_ip: &str,
current_tokens_for_ip: f64,
current_tokens_for_key: f64,
) -> bool {
// An authenticated client still must respect per-IP limits
let per_ip_limited = current_tokens_for_ip < 1.0;

// An authenticated client must also respect per-API-key limits
let per_api_key_limited = current_tokens_for_key < 1.0;

// The request is rate limited if EITHER quota is exhausted
per_ip_limited || per_api_key_limited
}

/// Verify that tier is correctly applied to per-API-key limit.
pub fn get_tier_quota(&self, tier: RateLimitTier) -> BucketQuota {
*self
.tier_limits
.get(&tier)
.unwrap_or(&self.per_api_key_limit)
}
}

// ── #907: Authenticated rate limit bypass tests ───────────────────────────

#[test]
fn test_authenticated_client_cannot_bypass_per_ip_limit() {
let checker = RateLimitChecker::new();
let client = AuthenticatedClient {
api_key: "admin_key_123".to_string(),
tier: RateLimitTier::Enterprise,
};

// Even for an enterprise (high-privilege) client, per-IP limit must apply
let per_ip_tokens = 0.5; // Exhausted IP quota
let per_api_key_tokens = 500.0; // Abundant API key quota

let should_limit = checker.should_rate_limit_authenticated(
&client,
"192.0.2.1",
per_ip_tokens,
per_api_key_tokens,
);

assert!(
should_limit,
"Authenticated client must be rate limited by per-IP quota"
);
}

#[test]
fn test_authenticated_client_cannot_bypass_per_api_key_limit() {
let checker = RateLimitChecker::new();
let client = AuthenticatedClient {
api_key: "admin_key_456".to_string(),
tier: RateLimitTier::Enterprise,
};

// Even with abundant per-IP quota, per-API-key limit must apply
let per_ip_tokens = 500.0; // Abundant IP quota
let per_api_key_tokens = 0.5; // Exhausted API key quota

let should_limit = checker.should_rate_limit_authenticated(
&client,
"203.0.113.50",
per_ip_tokens,
per_api_key_tokens,
);

assert!(
should_limit,
"Authenticated client must be rate limited by per-API-key quota"
);
}

#[test]
fn test_free_tier_enforces_lower_per_api_key_limit() {
let checker = RateLimitChecker::new();
let free_tier = RateLimitTier::Free;
let premium_tier = RateLimitTier::Premium;

let free_quota = checker.get_tier_quota(free_tier);
let premium_quota = checker.get_tier_quota(premium_tier);

assert!(
free_quota.requests_per_minute < premium_quota.requests_per_minute,
"Free tier must have lower request limit than Premium"
);
}

#[test]
fn test_enterprise_tier_enforces_higher_per_api_key_limit() {
let checker = RateLimitChecker::new();
let premium_tier = RateLimitTier::Premium;
let enterprise_tier = RateLimitTier::Enterprise;

let premium_quota = checker.get_tier_quota(premium_tier);
let enterprise_quota = checker.get_tier_quota(enterprise_tier);

assert!(
enterprise_quota.requests_per_minute > premium_quota.requests_per_minute,
"Enterprise tier must have higher request limit than Premium"
);
}

#[test]
fn test_batch_endpoint_counts_tokens_proportionally() {
let checker = RateLimitChecker::new();
let client = AuthenticatedClient {
api_key: "batch_admin_key".to_string(),
tier: RateLimitTier::Enterprise,
};

// A batch request with 5 items should consume 5 tokens, not 1
let batch_size = 5;
let tokens_consumed = batch_size as f64;

let per_ip_tokens_before = 100.0;
let per_api_key_tokens_before = 200.0;

let per_ip_tokens_after = per_ip_tokens_before - tokens_consumed;
let per_api_key_tokens_after = per_api_key_tokens_before - tokens_consumed;

assert_eq!(
per_ip_tokens_after, 95.0,
"Batch request must consume proportional tokens from per-IP bucket"
);
assert_eq!(
per_api_key_tokens_after, 195.0,
"Batch request must consume proportional tokens from per-API-key bucket"
);

// Verify rate limiting still applies after consuming proportional tokens
let should_limit_ip = checker.should_rate_limit_authenticated(
&client,
"192.0.2.1",
0.5,
per_api_key_tokens_after,
);
assert!(
should_limit_ip,
"Rate limiting must apply based on consumed tokens"
);
}

#[test]
fn test_authenticated_user_tier_does_not_bypass_per_ip_limits() {
let checker = RateLimitChecker::new();

// Test all tiers cannot bypass per-IP limit
for tier in &[
RateLimitTier::Free,
RateLimitTier::Premium,
RateLimitTier::Enterprise,
] {
let client = AuthenticatedClient {
api_key: format!("key_{:?}", tier),
tier: *tier,
};

// Per-IP limit exhausted
let should_limit = checker.should_rate_limit_authenticated(
&client,
"198.51.100.1",
0.1, // Token count below 1
500.0,
);

assert!(
should_limit,
"Tier {:?} must be rate limited by per-IP quota",
tier
);
}
}

#[test]
fn test_rate_limit_interaction_between_quotas() {
let checker = RateLimitChecker::new();
let client = AuthenticatedClient {
api_key: "test_key".to_string(),
tier: RateLimitTier::Premium,
};

// Both quotas have sufficient tokens: request should be allowed
let allowed = !checker.should_rate_limit_authenticated(
&client,
"198.51.100.50",
5.0, // Sufficient per-IP tokens
10.0, // Sufficient per-API-key tokens
);
assert!(allowed, "Request should be allowed when both quotas have tokens");

// One quota exhausted: request should be limited
let limited_by_ip = checker.should_rate_limit_authenticated(
&client,
"198.51.100.51",
0.5, // Insufficient per-IP tokens
10.0, // Sufficient per-API-key tokens
);
assert!(
limited_by_ip,
"Request should be limited if either quota is exhausted"
);

// Other quota exhausted: request should be limited
let limited_by_key = checker.should_rate_limit_authenticated(
&client,
"198.51.100.52",
5.0, // Sufficient per-IP tokens
0.5, // Insufficient per-API-key tokens
);
assert!(
limited_by_key,
"Request should be limited if either quota is exhausted"
);
}

#[test]
fn test_admin_cannot_bypass_rate_limits_through_batch_operations() {
let checker = RateLimitChecker::new();
let admin_client = AuthenticatedClient {
api_key: "admin_api_key".to_string(),
tier: RateLimitTier::Enterprise,
};

// Simulate a batch operation attempting to exceed quotas
let batch_items = 100;
let tokens_per_item = 1.0;
let total_tokens_needed = batch_items as f64 * tokens_per_item;

let per_ip_tokens = 50.0;
let per_api_key_tokens = 50.0;

// After processing batch, both quotas should be depleted
let per_ip_after = per_ip_tokens - (batch_items / 2) as f64;
let per_api_key_after = per_api_key_tokens - (batch_items / 2) as f64;

// Verify subsequent requests are limited
let should_limit = checker.should_rate_limit_authenticated(
&admin_client,
"192.0.2.100",
per_ip_after,
per_api_key_after,
);

assert!(
should_limit,
"Admin must still be rate limited after batch operations"
);
}
}
Loading
Loading