From d512dd4912a5f38cc563df1837addf03acf9b135 Mon Sep 17 00:00:00 2001 From: Nathan Hammond Date: Wed, 24 Sep 2025 16:51:54 +0800 Subject: [PATCH 1/2] Add tests. --- crates/cargo-test-support/src/registry.rs | 19 ++- tests/testsuite/alt_registry.rs | 195 ++++++++++++++++++++++ 2 files changed, 211 insertions(+), 3 deletions(-) diff --git a/crates/cargo-test-support/src/registry.rs b/crates/cargo-test-support/src/registry.rs index e4f83fbb769..c3ec71c184f 100644 --- a/crates/cargo-test-support/src/registry.rs +++ b/crates/cargo-test-support/src/registry.rs @@ -157,8 +157,10 @@ type RequestCallback = Box Response>; pub struct RegistryBuilder { /// If set, configures an alternate registry with the given name. alternative: Option, - /// The authorization token for the registry. + /// The client-supplied authorization token for the registry. token: Option, + /// The actual server authorization token for the registry. + server_token: Option, /// If set, the registry requires authorization for all operations. auth_required: bool, /// If set, serves the index over http. @@ -241,6 +243,7 @@ impl RegistryBuilder { RegistryBuilder { alternative: None, token: None, + server_token: None, auth_required: false, http_api: false, http_index: false, @@ -309,13 +312,20 @@ impl RegistryBuilder { self } - /// Sets the token value + /// Sets the client-supplied token value #[must_use] pub fn token(mut self, token: Token) -> Self { self.token = Some(token); self } + /// Sets the server token value + #[must_use] + pub fn server_token(mut self, token: Token) -> Self { + self.server_token = Some(token); + self + } + /// Sets this registry to require the authentication token for /// all operations. #[must_use] @@ -372,6 +382,9 @@ impl RegistryBuilder { .token .unwrap_or_else(|| Token::Plaintext(format!("{prefix}sekrit"))); + // Uses the client token unless otherwise set. + let server_token = self.server_token.unwrap_or_else(|| token.clone()); + let (server, index_url, api_url, dl_url) = if !self.http_index && !self.http_api { // No need to start the HTTP server. (None, index_url, api_url, dl_url) @@ -380,7 +393,7 @@ impl RegistryBuilder { registry_path.clone(), dl_path, api_path.clone(), - token.clone(), + server_token.clone(), self.auth_required, self.custom_responders, self.not_found_handler, diff --git a/tests/testsuite/alt_registry.rs b/tests/testsuite/alt_registry.rs index ab0ded2443a..a3972279825 100644 --- a/tests/testsuite/alt_registry.rs +++ b/tests/testsuite/alt_registry.rs @@ -8,6 +8,7 @@ use cargo_test_support::publish::validate_alt_upload; use cargo_test_support::registry::{self, Package, RegistryBuilder}; use cargo_test_support::str; use cargo_test_support::{basic_manifest, paths, project}; +use rand::Rng; #[cargo_test] fn depend_on_alt_registry() { @@ -1985,3 +1986,197 @@ fn empty_dependency_registry() { "#]]) .run(); } + +enum TokenStatus { + Valid, + Missing, + InvalidHasScheme, + InvalidNoScheme, +} + +struct TokenTest { + crates_io_token: TokenStatus, + alternative_token: TokenStatus, + expected_message: &'static str, +} + +fn token_test(token_test: TokenTest) { + let server_token = rand::rng() + .sample_iter(&rand::distr::Alphanumeric) + .take(8) + .map(char::from) + .collect::(); + + let crates_io_builder = RegistryBuilder::new() + .http_api() + .http_index() + .credential_provider(&[&"cargo:token"]) + .auth_required(); + + let alternative_builder = RegistryBuilder::new() + .http_api() + .http_index() + .credential_provider(&[&"cargo:token"]) + .alternative() + .auth_required(); + + let crates_io_mock = match token_test.crates_io_token { + TokenStatus::Valid => crates_io_builder.build(), + TokenStatus::Missing => crates_io_builder.no_configure_token().build(), + TokenStatus::InvalidHasScheme => crates_io_builder + .token(cargo_test_support::registry::Token::Plaintext( + "Bearer ".to_string(), + )) + .server_token(cargo_test_support::registry::Token::Plaintext( + server_token.clone(), + )) + .build(), + TokenStatus::InvalidNoScheme => crates_io_builder + .token(cargo_test_support::registry::Token::Plaintext( + "".to_string(), + )) + .server_token(cargo_test_support::registry::Token::Plaintext( + server_token.clone(), + )) + .build(), + }; + let _alternative_mock = match token_test.alternative_token { + TokenStatus::Valid => alternative_builder.build(), + TokenStatus::Missing => alternative_builder.no_configure_token().build(), + TokenStatus::InvalidHasScheme => alternative_builder + .token(cargo_test_support::registry::Token::Plaintext( + "Bearer ".to_string(), + )) + .server_token(cargo_test_support::registry::Token::Plaintext( + server_token.clone(), + )) + .build(), + TokenStatus::InvalidNoScheme => alternative_builder + .token(cargo_test_support::registry::Token::Plaintext( + "".to_string(), + )) + .server_token(cargo_test_support::registry::Token::Plaintext( + server_token.clone(), + )) + .build(), + }; + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + edition = "2015" + + [dependencies.bar_alt] + version = "0.0.1" + registry = "alternative" + + [dependencies.bar_crates] + version = "0.0.1" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + Package::new("bar_alt", "0.0.1").alternative(true).publish(); + Package::new("bar_crates", "0.0.1").publish(); + p.cargo("build") + .replace_crates_io(crates_io_mock.index_url()) + .with_status(101) + .with_stderr_contains(token_test.expected_message) + .run(); +} + +macro_rules! token_error_messages { + ($($name:ident: $value:expr)*) => { + $( + #[cargo_test] + fn $name() { + token_test($value); + } + )* + } +} + +token_error_messages! { + // Skips crates_valid_alt_valid because it would not error. + crates_valid_alt_missing: TokenTest { + crates_io_token: TokenStatus::Valid, + alternative_token: TokenStatus::Missing, + expected_message: " no token found for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN", + } + crates_valid_alt_invalid_has_scheme: TokenTest { + crates_io_token: TokenStatus::Valid, + alternative_token: TokenStatus::InvalidHasScheme, + expected_message: " token rejected for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN", + } + crates_valid_alt_invalid_no_scheme: TokenTest { + crates_io_token: TokenStatus::Valid, + alternative_token: TokenStatus::InvalidNoScheme, + expected_message: " token rejected for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN", + } + crates_missing_alt_valid: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::Valid, + expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN", + } + crates_missing_alt_missing: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::Missing, + expected_message: " no token found for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN", + } + crates_missing_alt_invalid_has_scheme: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::InvalidHasScheme, + expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN", + } + crates_missing_alt_invalid_no_scheme: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::InvalidNoScheme, + expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN", + } + crates_invalid_has_scheme_alt_valid: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::Valid, + expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN", + } + crates_invalid_has_scheme_alt_missing: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::Missing, + expected_message: " no token found for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN", + } + crates_invalid_has_scheme_alt_invalid_has_scheme: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::InvalidHasScheme, + expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN", + } + crates_invalid_has_scheme_alt_invalid_no_scheme: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::InvalidNoScheme, + expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN", + } + crates_invalid_no_scheme_alt_valid: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::Valid, + expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN", + } + crates_invalid_no_scheme_alt_missing: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::Missing, + expected_message: " no token found for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN", + } + crates_invalid_no_scheme_alt_invalid_has_scheme: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::InvalidHasScheme, + expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN", + } + crates_invalid_no_scheme_alt_invalid_no_scheme: TokenTest { + crates_io_token: TokenStatus::Missing, + alternative_token: TokenStatus::InvalidNoScheme, + expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN", + } +} From 5fcc082ef4a866f7171a93cc50e83e35f1ae081d Mon Sep 17 00:00:00 2001 From: Nathan Hammond Date: Fri, 19 Sep 2025 17:15:23 +0800 Subject: [PATCH 2/2] Sketch of improved error messages. --- src/cargo/sources/registry/http_remote.rs | 13 +++++++++ src/cargo/util/auth/mod.rs | 35 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/cargo/sources/registry/http_remote.rs b/src/cargo/sources/registry/http_remote.rs index 420c41cd810..2d20437238d 100644 --- a/src/cargo/sources/registry/http_remote.rs +++ b/src/cargo/sources/registry/http_remote.rs @@ -100,6 +100,9 @@ pub struct HttpRegistry<'gctx> { /// Should we include the authorization header? auth_required: bool, + /// The scheme of the included authorization header, if any. + authorization_scheme: Option, + /// Url to get a token for the registry. login_url: Option, @@ -231,6 +234,7 @@ impl<'gctx> HttpRegistry<'gctx> { fetch_started: false, registry_config: None, auth_required: false, + authorization_scheme: None, login_url: None, auth_error_headers: vec![], quiet: false, @@ -604,6 +608,7 @@ impl<'gctx> RegistryData for HttpRegistry<'gctx> { self.source_id, self.login_url.clone(), auth::AuthorizationErrorReason::TokenRejected, + self.authorization_scheme.clone(), )?; return Poll::Ready(err.context(auth_error)); } else { @@ -663,6 +668,14 @@ impl<'gctx> RegistryData for HttpRegistry<'gctx> { self.auth_error_headers.clone(), true, )?; + let (scheme, _token) = authorization + .split_once(" ") + .unwrap_or(("", &authorization)); + self.authorization_scheme = match scheme.to_ascii_lowercase().as_str() { + "basic" => Some(auth::AuthorizationScheme::Basic), + "bearer" => Some(auth::AuthorizationScheme::Bearer), + _ => Some(auth::AuthorizationScheme::Unrecognized), + }; headers.append(&format!("Authorization: {}", authorization))?; trace!(target: "network", "including authorization for {}", full_url); } diff --git a/src/cargo/util/auth/mod.rs b/src/cargo/util/auth/mod.rs index fc8179d8d8b..dededde9259 100644 --- a/src/cargo/util/auth/mod.rs +++ b/src/cargo/util/auth/mod.rs @@ -387,6 +387,23 @@ impl fmt::Display for AuthorizationErrorReason { } } +#[derive(Clone, Debug, PartialEq)] +pub enum AuthorizationScheme { + Basic, + Bearer, + Unrecognized, +} + +impl fmt::Display for AuthorizationScheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AuthorizationScheme::Basic => write!(f, "Basic"), + AuthorizationScheme::Bearer => write!(f, "Bearer"), + AuthorizationScheme::Unrecognized => write!(f, "unrecognized"), + } + } +} + /// An authorization error from accessing a registry. #[derive(Debug)] pub struct AuthorizationError { @@ -400,6 +417,8 @@ pub struct AuthorizationError { reason: AuthorizationErrorReason, /// Should `cargo login` and the `_TOKEN` env var be included when displaying this error? supports_cargo_token_credential_provider: bool, + /// What authorization scheme was specified in the token, if any? + scheme: Option, } impl AuthorizationError { @@ -408,6 +427,7 @@ impl AuthorizationError { sid: SourceId, login_url: Option, reason: AuthorizationErrorReason, + scheme: Option, ) -> CargoResult { // Only display the _TOKEN environment variable suggestion if the `cargo:token` credential // provider is available for the source. Otherwise setting the environment variable will @@ -422,6 +442,7 @@ impl AuthorizationError { login_url, reason, supports_cargo_token_credential_provider, + scheme, }) } } @@ -461,6 +482,19 @@ impl fmt::Display for AuthorizationError { "\nYou may need to log in using this registry's credential provider" )?; } + if let Some(scheme) = &self.scheme { + write!( + f, + "Your registry token is prefixed with an embedded {} authentication scheme. Is this correct?", + scheme, + )?; + } else { + write!( + f, + "Your registry token is not prefixed with an embedded authorization scheme (e.g. `Bearer `).\n\ + Does this registry require an authentication scheme?", + )?; + } Ok(()) } else if self.reason == AuthorizationErrorReason::TokenMissing { write!( @@ -624,6 +658,7 @@ pub fn auth_token( *sid, login_url.cloned(), AuthorizationErrorReason::TokenMissing, + None, )? .into()), }