Skip to content

Commit 55ecaad

Browse files
authored
Merge pull request #11894 from Turbo87/disable-token-creation
Add `DISABLE_TOKEN_CREATION` flag
2 parents d8ca587 + 276366a commit 55ecaad

File tree

4 files changed

+51
-3
lines changed

4 files changed

+51
-3
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: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ use anyhow::Context;
66

77
use crate::app::AppState;
88
use crate::auth::AuthCheck;
9+
use crate::middleware::real_ip::RealIp;
910
use crate::models::token::{CrateScope, EndpointScope};
10-
use crate::util::errors::{AppResult, bad_request};
11+
use crate::util::errors::{AppResult, bad_request, custom};
1112
use crate::util::token::PlainToken;
1213
use axum::Json;
1314
use axum::extract::{Path, Query};
@@ -20,12 +21,12 @@ use diesel::dsl::{IntervalDsl, now};
2021
use diesel::prelude::*;
2122
use diesel::sql_types::Timestamptz;
2223
use diesel_async::RunQueryDsl;
23-
use http::StatusCode;
2424
use http::request::Parts;
25+
use http::{StatusCode, header};
2526
use minijinja::context;
2627
use secrecy::ExposeSecret;
2728
use serde::{Deserialize, Serialize};
28-
use tracing::error;
29+
use tracing::{error, warn};
2930

3031
#[derive(Deserialize)]
3132
pub struct GetParams {
@@ -127,6 +128,26 @@ pub async fn create_api_token(
127128

128129
let user = auth.user();
129130

131+
// Check if token creation is disabled
132+
if let Some(disable_message) = &app.config.disable_token_creation {
133+
let client_ip = parts.extensions.get::<RealIp>().map(|ip| ip.to_string());
134+
let client_ip = client_ip.as_deref().unwrap_or("unknown");
135+
136+
let mut headers = parts.headers.clone();
137+
headers.remove(header::AUTHORIZATION);
138+
headers.remove(header::COOKIE);
139+
140+
warn!(
141+
network.client.ip = client_ip,
142+
http.headers = ?headers,
143+
"Blocked token creation for user `{}` (id: {}) due to disabled flag (token name: `{}`)",
144+
user.gh_login, user.id, new.api_token.name
145+
);
146+
147+
let message = disable_message.clone();
148+
return Err(custom(StatusCode::SERVICE_UNAVAILABLE, message));
149+
}
150+
130151
let max_token_per_user = 500;
131152
let count: i64 = ApiToken::belonging_to(user)
132153
.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)