Skip to content

Commit 4eefc05

Browse files
committed
feat(flags): Add per-team rate limiting to flag definitions endpoint
FlagDefinitionsRateLimiterImplements configurable rate limiting for the `/flags/definitions` endpoint to protect against excessive requests and allow per-team customization. Key features: - Configurable default rate limit via `FLAG_DEFINITIONS_DEFAULT_RATE_PER_MINUTE` (default: 600/minute) - Per-team overrides via `FLAG_DEFINITIONS_RATE_LIMITS` environment variable Format: `{"team_id": "rate_string"}` (e.g., `{"123": "1200/minute", "456": "2400/hour"}`) - Supports Django `SimpleRateThrottle` rate format (N/second|minute|hour|day) - Rate limiting occurs after authentication to prevent enumeration attacks - Thread-safe implementation using governor with `Arc<RwLock>` - Prometheus metrics for monitoring: - `flags_flag_definitions_requests_total` - `flags_flag_definitions_rate_limited_total` Implementation: - Generic `KeyedRateLimiter<K>` struct for reusable rate limiting with any key type - Configurable Prometheus metrics via constructor parameters - Uses GCRA (Generic Cell Rate Algorithm) via `governor` crate for efficiency - `rate_parser` module for parsing Django-style rate strings - Renamed `local_evaluation` module to `flag_definitions` for clarity - Integrated into `flags_definitions` handler in `flag_definitions` module - Comprehensive test coverage (9 unit tests, 24 integration tests) Module refactoring: - local_evaluation.rs → flag_definitions.rs - test_local_evaluation.rs → test_flag_definitions.rs - LocalEvaluationResponse → FlagDefinitionsResponse - LocalEvaluationQueryParams → FlagDefinitionsQueryParams - authenticate_local_evaluation → authenticate_flag_definitions - FlagRequestType::LocalEvaluation → FlagRequestType::FlagDefinitions Environment variables: - `FLAG_DEFINITIONS_DEFAULT_RATE_PER_MINUTE`: Default rate for all teams (default: 600) - `FLAG_DEFINITIONS_RATE_LIMITS`: JSON map of team_id to rate string for overrides
1 parent d4d41c3 commit 4eefc05

File tree

13 files changed

+973
-38
lines changed

13 files changed

+973
-38
lines changed

rust/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/feature-flags/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ common-database = { path = "../common/database" }
1616
common-redis = { path = "../common/redis" }
1717
common-types = { path = "../common/types" }
1818
envconfig = { workspace = true }
19+
governor = { workspace = true }
1920
limiters = { path = "../common/limiters" }
2021
tokio = { workspace = true }
2122
tracing = { workspace = true }

rust/feature-flags/src/api/local_evaluation.rs renamed to rust/feature-flags/src/api/flag_definitions.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ use serde::{Deserialize, Serialize};
1414
use serde_json::Value;
1515
use tracing::{info, warn};
1616

17-
/// Response for local evaluation endpoint
17+
/// Response for flag definitions endpoint
1818
/// This is returned as raw JSON from cache to avoid deserialization overhead
19-
pub type LocalEvaluationResponse = Value;
19+
pub type FlagDefinitionsResponse = Value;
2020

21-
/// Query parameters for the local evaluation endpoint
21+
/// Query parameters for the flag definitions endpoint
2222
#[derive(Debug, Deserialize, Serialize)]
23-
pub struct LocalEvaluationQueryParams {
23+
pub struct FlagDefinitionsQueryParams {
2424
/// Team API token - required to specify which team's flags to return
2525
pub token: String,
2626
}
2727

28-
/// Local evaluation endpoint handler
28+
/// Flag definitions endpoint handler
2929
///
3030
/// This endpoint provides flag definitions for client-side evaluation.
3131
///
@@ -47,14 +47,14 @@ pub struct LocalEvaluationQueryParams {
4747
#[debug_handler]
4848
pub async fn flags_definitions(
4949
State(state): State<AppState>,
50-
Query(params): Query<LocalEvaluationQueryParams>,
50+
Query(params): Query<FlagDefinitionsQueryParams>,
5151
headers: HeaderMap,
5252
method: Method,
5353
) -> Result<Response, FlagError> {
5454
info!(
5555
method = %method,
5656
token = %params.token,
57-
"Processing local evaluation request (always includes cohorts)"
57+
"Processing flag definitions request (always includes cohorts)"
5858
);
5959

6060
// Only GET is supported for this read-only endpoint
@@ -67,7 +67,10 @@ pub async fn flags_definitions(
6767
let team = fetch_team_by_token(&state, &params.token).await?;
6868

6969
// Authenticate against the specified team
70-
authenticate_local_evaluation(&state, &team, &headers).await?;
70+
authenticate_flag_definitions(&state, &team, &headers).await?;
71+
72+
// Check rate limit for this team
73+
state.flag_definitions_limiter.check_rate_limit(team.id)?;
7174

7275
// Retrieve cached response from HyperCache (always with cohorts)
7376
let cached_response = get_from_cache(&state, &team).await?;
@@ -122,7 +125,7 @@ async fn fetch_team_by_token(state: &AppState, token: &str) -> Result<Team, Flag
122125
async fn get_from_cache(
123126
state: &AppState,
124127
team: &Team,
125-
) -> Result<LocalEvaluationResponse, FlagError> {
128+
) -> Result<FlagDefinitionsResponse, FlagError> {
126129
// Configure HyperCache to use the flags_with_cohorts.json cache key
127130
// This ensures we always return cohort definitions along with flag definitions
128131
let hypercache_config = HyperCacheConfig::new(
@@ -159,13 +162,13 @@ async fn get_from_cache(
159162
info!(
160163
team_id = team.id,
161164
source = source_name,
162-
"Cache hit for local evaluation (with cohorts)"
165+
"Cache hit for flag definitions (with cohorts)"
163166
);
164167

165168
Ok(data)
166169
}
167170

168-
/// Authenticates local evaluation requests using team secret API tokens or personal API keys
171+
/// Authenticates flag definitions requests using team secret API tokens or personal API keys
169172
///
170173
/// Validates that the authentication credential has access to the specified team.
171174
///
@@ -176,7 +179,7 @@ async fn get_from_cache(
176179
/// Priority: Secret API tokens take precedence over personal API keys when both are provided.
177180
///
178181
/// Returns Ok(()) if authentication succeeds, Err otherwise
179-
async fn authenticate_local_evaluation(
182+
async fn authenticate_flag_definitions(
180183
state: &AppState,
181184
team: &Team,
182185
headers: &HeaderMap,

rust/feature-flags/src/api/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod auth;
22
pub mod endpoint;
33
pub mod errors;
4-
pub mod local_evaluation;
4+
pub mod flag_definitions;
5+
pub mod rate_limiter;
6+
pub mod rate_parser;
57
pub mod types;

0 commit comments

Comments
 (0)