Skip to content

Commit a913075

Browse files
committed
Add DISABLE_TOKEN_CREATION flag
Setting this environment variable will cause crates.io to reject new API token creation requests with the error message set inside the environment variable. This can be useful to e.g. temporarily pause API token creation during ongoing phishing campaigns.
1 parent 6fa86ab commit a913075

File tree

4 files changed

+40
-2
lines changed

4 files changed

+40
-2
lines changed

src/config/server.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ pub struct Server {
9393
/// The expected audience claim (`aud`) for the Trusted Publishing
9494
/// token exchange.
9595
pub trustpub_audience: String,
96+
97+
/// Disables API token creation when set to any non-empty value.
98+
/// The value is used as the error message returned to users.
99+
pub disable_token_creation: Option<String>,
96100
}
97101

98102
impl Server {
@@ -128,6 +132,8 @@ impl Server {
128132
/// endpoint even with a healthy database pool.
129133
/// - `BLOCKED_ROUTES`: A comma separated list of HTTP route patterns that are manually blocked
130134
/// by an operator (e.g. `/crates/{crate_id}/{version}/download`).
135+
/// - `DISABLE_TOKEN_CREATION`: If set to any non-empty value, disables API token creation
136+
/// and uses the value as the error message returned to users.
131137
///
132138
/// # Panics
133139
///
@@ -195,6 +201,7 @@ impl Server {
195201

196202
let domain_name = dotenvy::var("DOMAIN_NAME").unwrap_or_else(|_| "crates.io".into());
197203
let trustpub_audience = var("TRUSTPUB_AUDIENCE")?.unwrap_or_else(|| domain_name.clone());
204+
let disable_token_creation = var("DISABLE_TOKEN_CREATION")?.filter(|s| !s.is_empty());
198205

199206
Ok(Server {
200207
db: DatabasePools::full_from_environment(&base)?,
@@ -245,6 +252,7 @@ impl Server {
245252
html_render_cache_max_capacity: var_parsed("HTML_RENDER_CACHE_CAP")?.unwrap_or(1024),
246253
content_security_policy: Some(content_security_policy.parse()?),
247254
trustpub_audience,
255+
disable_token_creation,
248256
})
249257
}
250258
}

src/controllers/token.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use anyhow::Context;
77
use crate::app::AppState;
88
use crate::auth::AuthCheck;
99
use crate::models::token::{CrateScope, EndpointScope};
10-
use crate::util::errors::{AppResult, bad_request};
10+
use crate::util::errors::{AppResult, bad_request, custom};
1111
use crate::util::token::PlainToken;
1212
use axum::Json;
1313
use axum::extract::{Path, Query};
@@ -25,7 +25,7 @@ use http::request::Parts;
2525
use minijinja::context;
2626
use secrecy::ExposeSecret;
2727
use serde::{Deserialize, Serialize};
28-
use tracing::error;
28+
use tracing::{error, warn};
2929

3030
#[derive(Deserialize)]
3131
pub struct GetParams {
@@ -127,6 +127,17 @@ pub async fn create_api_token(
127127

128128
let user = auth.user();
129129

130+
// Check if token creation is disabled
131+
if let Some(disable_message) = &app.config.disable_token_creation {
132+
warn!(
133+
"Blocked token creation for user `{}` (id: {}) due to disabled flag (token name: `{}`)",
134+
user.gh_login, user.id, new.api_token.name
135+
);
136+
137+
let message = disable_message.clone();
138+
return Err(custom(StatusCode::SERVICE_UNAVAILABLE, message));
139+
}
140+
130141
let max_token_per_user = 500;
131142
let count: i64 = ApiToken::belonging_to(user)
132143
.count()

src/tests/routes/me/tokens/create.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,21 @@ async fn create_token_with_expiry_date() {
279279

280280
assert_snapshot!(app.emails_snapshot().await);
281281
}
282+
283+
#[tokio::test(flavor = "multi_thread")]
284+
async fn create_token_disabled() {
285+
const ERROR_MESSAGE: &str =
286+
"Token creation is temporarily disabled due to an ongoing phishing campaign";
287+
288+
let (app, _, user) = TestApp::init()
289+
.with_config(|config| {
290+
config.disable_token_creation = Some(ERROR_MESSAGE.to_string());
291+
})
292+
.with_user()
293+
.await;
294+
295+
let response = user.put::<()>("/api/v1/me/tokens", NEW_BAR).await;
296+
assert_snapshot!(response.status(), @"503 Service Unavailable");
297+
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"Token creation is temporarily disabled due to an ongoing phishing campaign"}]}"#);
298+
assert!(app.emails().await.is_empty());
299+
}

src/tests/util/test_app.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ fn simple_config() -> config::Server {
526526
html_render_cache_max_capacity: 1024,
527527
content_security_policy: None,
528528
trustpub_audience: AUDIENCE.to_string(),
529+
disable_token_creation: None,
529530
}
530531
}
531532

0 commit comments

Comments
 (0)