Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 13 additions & 2 deletions src/controllers/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use anyhow::Context;
use crate::app::AppState;
use crate::auth::AuthCheck;
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 @@ -25,7 +25,7 @@ use http::request::Parts;
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 +127,17 @@ 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 {
warn!(
"Blocked token creation for user `{}` (id: {}) due to disabled flag (token name: `{}`)",
user.gh_login, user.id, new.api_token.name
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@walterhpearce isn't that what we're doing here? 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed it because I am not smart. Can we log the full request during this period? Headers + addresses.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the IP address and the headers, minus cookie and authorization header to not unnecessarily expose these in our logs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm


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