Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/config/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ pub struct Server {
/// The expected audience claim (`aud`) for the Trusted Publishing
/// token exchange.
pub trustpub_audience: String,

/// Disables API token creation when set to any non-empty value.
/// The value is used as the error message returned to users.
pub disable_token_creation: Option<String>,
}

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

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

Ok(Server {
db: DatabasePools::full_from_environment(&base)?,
Expand Down Expand Up @@ -245,6 +252,7 @@ impl Server {
html_render_cache_max_capacity: var_parsed("HTML_RENDER_CACHE_CAP")?.unwrap_or(1024),
content_security_policy: Some(content_security_policy.parse()?),
trustpub_audience,
disable_token_creation,
})
}
}
Expand Down
27 changes: 24 additions & 3 deletions src/controllers/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use anyhow::Context;

use crate::app::AppState;
use crate::auth::AuthCheck;
use crate::middleware::real_ip::RealIp;
use crate::models::token::{CrateScope, EndpointScope};
use crate::util::errors::{AppResult, bad_request};
use crate::util::errors::{AppResult, bad_request, custom};
use crate::util::token::PlainToken;
use axum::Json;
use axum::extract::{Path, Query};
Expand All @@ -20,12 +21,12 @@ use diesel::dsl::{IntervalDsl, now};
use diesel::prelude::*;
use diesel::sql_types::Timestamptz;
use diesel_async::RunQueryDsl;
use http::StatusCode;
use http::request::Parts;
use http::{StatusCode, header};
use minijinja::context;
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
use tracing::error;
use tracing::{error, warn};

#[derive(Deserialize)]
pub struct GetParams {
Expand Down Expand Up @@ -127,6 +128,26 @@ pub async fn create_api_token(

let user = auth.user();

// Check if token creation is disabled
if let Some(disable_message) = &app.config.disable_token_creation {
let client_ip = parts.extensions.get::<RealIp>().map(|ip| ip.to_string());
let client_ip = client_ip.as_deref().unwrap_or("unknown");

let mut headers = parts.headers.clone();
headers.remove(header::AUTHORIZATION);
headers.remove(header::COOKIE);

warn!(
network.client.ip = client_ip,
http.headers = ?headers,
"Blocked token creation for user `{}` (id: {}) due to disabled flag (token name: `{}`)",
user.gh_login, user.id, new.api_token.name
);

let message = disable_message.clone();
return Err(custom(StatusCode::SERVICE_UNAVAILABLE, message));
}

let max_token_per_user = 500;
let count: i64 = ApiToken::belonging_to(user)
.count()
Expand Down
18 changes: 18 additions & 0 deletions src/tests/routes/me/tokens/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,21 @@ async fn create_token_with_expiry_date() {

assert_snapshot!(app.emails_snapshot().await);
}

#[tokio::test(flavor = "multi_thread")]
async fn create_token_disabled() {
const ERROR_MESSAGE: &str =
"Token creation is temporarily disabled due to an ongoing phishing campaign";

let (app, _, user) = TestApp::init()
.with_config(|config| {
config.disable_token_creation = Some(ERROR_MESSAGE.to_string());
})
.with_user()
.await;

let response = user.put::<()>("/api/v1/me/tokens", NEW_BAR).await;
assert_snapshot!(response.status(), @"503 Service Unavailable");
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"Token creation is temporarily disabled due to an ongoing phishing campaign"}]}"#);
assert!(app.emails().await.is_empty());
}
1 change: 1 addition & 0 deletions src/tests/util/test_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ fn simple_config() -> config::Server {
html_render_cache_max_capacity: 1024,
content_security_policy: None,
trustpub_audience: AUDIENCE.to_string(),
disable_token_creation: None,
}
}

Expand Down