api: add ingress request throttling

This commit is contained in:
a.tolmachev
2026-05-01 17:03:53 +00:00
parent 411d662676
commit 5f68fcbd04
7 changed files with 241 additions and 3 deletions
+80
View File
@@ -5,6 +5,7 @@ use axum::{
use crate::{
auth::{require_session, require_workspace_session},
rate_limit::apply_api_rate_limit,
request_context::apply_request_context,
routes::{
access::{
@@ -206,6 +207,10 @@ pub fn build_app(state: AppState) -> Router {
.merge(protected_auth_router),
)
.nest("/api/admin", admin_router)
.layer(middleware::from_fn_with_state(
state.clone(),
apply_api_rate_limit,
))
.layer(middleware::from_fn(apply_request_context))
.with_state(state)
}
@@ -2481,6 +2486,9 @@ mod tests {
test_auth_settings(),
test_secret_crypto(),
),
api_rate_limiter: crank_runtime::RequestRateLimiter::new(
crank_runtime::RequestRateLimitConfig::new(10_000, 10_000).unwrap(),
),
})
}
@@ -2570,6 +2578,78 @@ mod tests {
client
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn rejects_rapid_login_requests_with_429() {
let registry = test_registry().await;
let storage_root = test_storage_root("login_rate_limit");
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let app = build_app(AppState {
service: AdminService::new(
registry,
storage_root,
test_auth_settings(),
test_secret_crypto(),
),
api_rate_limiter: crank_runtime::RequestRateLimiter::new(
crank_runtime::RequestRateLimitConfig::new(1, 1).unwrap(),
),
});
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let handle = tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(async move {
let _ = shutdown_rx.await;
})
.await
.unwrap();
});
let client = reqwest::Client::new();
let root_url = format!("http://{address}");
let first_response = client
.post(format!("{root_url}/api/auth/login"))
.header("x-request-id", "req_admin_rate_01")
.json(&json!({
"email": TEST_AUTH_EMAIL,
"password": TEST_AUTH_PASSWORD,
}))
.send()
.await
.unwrap();
assert_eq!(first_response.status(), reqwest::StatusCode::OK);
let second_response = client
.post(format!("{root_url}/api/auth/login"))
.header("x-request-id", "req_admin_rate_02")
.json(&json!({
"email": TEST_AUTH_EMAIL,
"password": TEST_AUTH_PASSWORD,
}))
.send()
.await
.unwrap();
assert_eq!(
second_response.status(),
reqwest::StatusCode::TOO_MANY_REQUESTS
);
assert_eq!(
second_response.headers()["x-request-id"].to_str().unwrap(),
"req_admin_rate_02"
);
let payload = second_response.json::<Value>().await.unwrap();
assert_eq!(payload["error"]["code"], "rate_limited");
let retry_after_ms = payload["error"]["context"]["retry_after_ms"]
.as_u64()
.unwrap();
assert!((1..=1000).contains(&retry_after_ms));
let _ = shutdown_tx.send(());
let _ = handle.await;
}
async fn assert_success_json(response: reqwest::Response) -> Value {
let status = response.status();
let body = response.text().await.unwrap();