diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 81eae47900..10f4348421 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -16568,6 +16568,7 @@ dependencies = [ "argon2", "axum 0.8.4", "chrono", + "dashmap 6.1.0", "http 1.4.0", "hyper 1.9.0", "lazy_static", diff --git a/backend/windmill-api-users/Cargo.toml b/backend/windmill-api-users/Cargo.toml index 13ab8143d8..c322d6cfda 100644 --- a/backend/windmill-api-users/Cargo.toml +++ b/backend/windmill-api-users/Cargo.toml @@ -21,6 +21,7 @@ windmill-api-auth.workspace = true windmill-audit.workspace = true windmill-git-sync.workspace = true +dashmap.workspace = true argon2.workspace = true axum.workspace = true chrono.workspace = true diff --git a/backend/windmill-api-users/src/users.rs b/backend/windmill-api-users/src/users.rs index 6024c756fb..dcab4c62c9 100644 --- a/backend/windmill-api-users/src/users.rs +++ b/backend/windmill-api-users/src/users.rs @@ -12,6 +12,7 @@ use sqlx::{Postgres, Transaction}; use std::sync::atomic::AtomicBool; use std::sync::Arc; +use std::sync::LazyLock; use std::time::Duration; use windmill_api_auth::ApiAuthed; @@ -60,6 +61,43 @@ use windmill_git_sync::handle_deployment_metadata; pub const COOKIE_PATH: &str = "/"; +const TOKEN_CREATE_LIMIT_PER_MINUTE: i32 = 10; + +struct TokenRateLimitEntry { + count: i32, + minute_bucket: i64, +} + +static TOKEN_CREATE_RATE_LIMIT: LazyLock> = + LazyLock::new(dashmap::DashMap::new); + +fn check_token_create_rate_limit(username: &str) -> Result<()> { + if !*CLOUD_HOSTED { + return Ok(()); + } + + let current_minute = chrono::Utc::now().timestamp() / 60; + + let mut entry = TOKEN_CREATE_RATE_LIMIT + .entry(username.to_string()) + .or_insert(TokenRateLimitEntry { count: 0, minute_bucket: current_minute }); + + if entry.minute_bucket != current_minute { + entry.count = 0; + entry.minute_bucket = current_minute; + } + + if entry.count >= TOKEN_CREATE_LIMIT_PER_MINUTE { + return Err(Error::Generic( + StatusCode::TOO_MANY_REQUESTS, + "Too many token creation requests. Please try again later.".to_string(), + )); + } + + entry.count += 1; + Ok(()) +} + pub fn workspaced_service() -> Router { Router::new() .route("/list", get(list_users)) @@ -1975,6 +2013,8 @@ async fn create_token( authed: ApiAuthed, Json(token_config): Json, ) -> Result<(StatusCode, String)> { + check_token_create_rate_limit(&authed.username)?; + let mut tx = db.begin().await?; let token = create_token_internal(&mut *tx, &db, &authed, token_config).await?;