From 9827412fee4f5a88ac85e013edd954b2b63f399b Mon Sep 17 00:00:00 2001 From: Arlo Siemsen Date: Mon, 31 Oct 2022 11:44:01 -0500 Subject: [PATCH] Implement RFC 3139: alternative registry authentication support --- CHANGELOG.md | 2 +- Cargo.toml | 1 + crates/cargo-test-support/src/registry.rs | 60 ++- crates/crates-io/lib.rs | 12 +- .../cargo-credential-1password/Cargo.toml | 4 +- .../cargo-credential-1password/src/main.rs | 54 +- .../cargo-credential-gnome-secret/Cargo.toml | 4 +- .../cargo-credential-gnome-secret/src/main.rs | 42 +- .../Cargo.toml | 4 +- .../src/main.rs | 12 +- .../cargo-credential-wincred/Cargo.toml | 4 +- .../cargo-credential-wincred/src/main.rs | 19 +- crates/credential/cargo-credential/Cargo.toml | 2 +- crates/credential/cargo-credential/src/lib.rs | 16 +- src/bin/cargo/commands/login.rs | 4 +- src/bin/cargo/commands/logout.rs | 6 +- src/bin/cargo/commands/owner.rs | 2 - src/bin/cargo/commands/publish.rs | 2 - src/bin/cargo/commands/yank.rs | 2 - src/cargo/core/features.rs | 2 + src/cargo/core/package.rs | 15 +- src/cargo/core/source/mod.rs | 6 +- src/cargo/ops/mod.rs | 2 +- src/cargo/ops/registry.rs | 210 +++----- src/cargo/ops/registry/auth.rs | 237 --------- src/cargo/sources/registry/download.rs | 8 + src/cargo/sources/registry/http_remote.rs | 328 ++++++++---- src/cargo/sources/registry/mod.rs | 24 +- src/cargo/sources/registry/remote.rs | 8 +- src/cargo/util/auth.rs | 489 ++++++++++++++++++ src/cargo/util/config/mod.rs | 88 +++- src/cargo/util/mod.rs | 1 + src/doc/src/reference/unstable.md | 37 +- tests/testsuite/alt_registry.rs | 8 +- tests/testsuite/credential_process.rs | 40 +- tests/testsuite/login.rs | 2 +- tests/testsuite/logout.rs | 17 +- tests/testsuite/main.rs | 1 + tests/testsuite/publish.rs | 19 +- tests/testsuite/registry.rs | 8 +- tests/testsuite/registry_auth.rs | 304 +++++++++++ tests/testsuite/search.rs | 23 + 42 files changed, 1468 insertions(+), 661 deletions(-) delete mode 100644 src/cargo/ops/registry/auth.rs create mode 100644 src/cargo/util/auth.rs create mode 100644 tests/testsuite/registry_auth.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 19dd18603fd..2a48b0be801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -326,7 +326,7 @@ - Added `-Zcheck-cfg=output` to support build-scripts declaring their supported set of `cfg` values with `cargo:rustc-check-cfg`. [#10539](https://github.com/rust-lang/cargo/pull/10539) -- `-Z http-registry` now uses https://index.crates.io/ when accessing crates-io. +- `-Z sparse-registry` now uses https://index.crates.io/ when accessing crates-io. [#10725](https://github.com/rust-lang/cargo/pull/10725) - Fixed formatting of `.workspace` key in `cargo add` for workspace inheritance. [#10705](https://github.com/rust-lang/cargo/pull/10705) diff --git a/Cargo.toml b/Cargo.toml index 105190f118d..f94d1e85820 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ git2-curl = "0.16.0" glob = "0.3.0" hex = "0.4" home = "0.5" +http-auth = { version = "0.1.6", default-features = false } humantime = "2.0.0" indexmap = "1" ignore = "0.4.7" diff --git a/crates/cargo-test-support/src/registry.rs b/crates/cargo-test-support/src/registry.rs index 1f30ea01134..5290a83ede6 100644 --- a/crates/cargo-test-support/src/registry.rs +++ b/crates/cargo-test-support/src/registry.rs @@ -11,7 +11,7 @@ use std::fs::{self, File}; use std::io::{BufRead, BufReader, Read, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::path::PathBuf; -use std::thread; +use std::thread::{self, JoinHandle}; use tar::{Builder, Header}; use url::Url; @@ -61,6 +61,8 @@ pub struct RegistryBuilder { alternative: Option, /// If set, the authorization token for the registry. token: Option, + /// If set, the registry requires authorization for all operations. + auth_required: bool, /// If set, serves the index over http. http_index: bool, /// If set, serves the API over http. @@ -76,7 +78,7 @@ pub struct RegistryBuilder { } pub struct TestRegistry { - _server: Option, + server: Option, index_url: Url, path: PathBuf, api_url: Url, @@ -98,6 +100,17 @@ impl TestRegistry { .as_deref() .expect("registry was not configured with a token") } + + /// Shutdown the server thread and wait for it to stop. + /// `Drop` automatically stops the server, but this additionally + /// waits for the thread to stop. + pub fn join(self) { + if let Some(mut server) = self.server { + server.stop(); + let handle = server.handle.take().unwrap(); + handle.join().unwrap(); + } + } } impl RegistryBuilder { @@ -106,6 +119,7 @@ impl RegistryBuilder { RegistryBuilder { alternative: None, token: None, + auth_required: false, http_api: false, http_index: false, api: true, @@ -160,6 +174,14 @@ impl RegistryBuilder { self } + /// Sets this registry to require the authentication token for + /// all operations. + #[must_use] + pub fn auth_required(mut self) -> Self { + self.auth_required = true; + self + } + /// Operate the index over http #[must_use] pub fn http_index(mut self) -> Self { @@ -207,6 +229,7 @@ impl RegistryBuilder { registry_path.clone(), dl_path, token.clone(), + self.auth_required, self.custom_responders, ); let index_url = if self.http_index { @@ -226,7 +249,7 @@ impl RegistryBuilder { let registry = TestRegistry { api_url, index_url, - _server: server, + server, dl_url, path: registry_path, token, @@ -293,6 +316,11 @@ impl RegistryBuilder { } } + let auth = if self.auth_required { + r#","auth-required":true"# + } else { + "" + }; let api = if self.api { format!(r#","api":"{}""#, registry.api_url) } else { @@ -302,7 +330,7 @@ impl RegistryBuilder { repo(®istry.path) .file( "config.json", - &format!(r#"{{"dl":"{}"{api}}}"#, registry.dl_url), + &format!(r#"{{"dl":"{}"{api}{auth}}}"#, registry.dl_url), ) .build(); fs::create_dir_all(api_path.join("api/v1/crates")).unwrap(); @@ -442,6 +470,7 @@ pub fn alt_init() -> TestRegistry { pub struct HttpServerHandle { addr: SocketAddr, + handle: Option>, } impl HttpServerHandle { @@ -456,10 +485,8 @@ impl HttpServerHandle { pub fn dl_url(&self) -> Url { Url::parse(&format!("http://{}/dl", self.addr.to_string())).unwrap() } -} -impl Drop for HttpServerHandle { - fn drop(&mut self) { + fn stop(&self) { if let Ok(mut stream) = TcpStream::connect(self.addr) { // shutdown the server let _ = stream.write_all(b"stop"); @@ -468,6 +495,12 @@ impl Drop for HttpServerHandle { } } +impl Drop for HttpServerHandle { + fn drop(&mut self) { + self.stop(); + } +} + /// Request to the test http server #[derive(Clone)] pub struct Request { @@ -504,6 +537,7 @@ pub struct HttpServer { registry_path: PathBuf, dl_path: PathBuf, token: Option, + auth_required: bool, custom_responders: HashMap<&'static str, Box Response>>, } @@ -512,6 +546,7 @@ impl HttpServer { registry_path: PathBuf, dl_path: PathBuf, token: Option, + auth_required: bool, api_responders: HashMap< &'static str, Box Response>, @@ -524,10 +559,11 @@ impl HttpServer { registry_path, dl_path, token, + auth_required, custom_responders: api_responders, }; - thread::spawn(move || server.start()); - HttpServerHandle { addr } + let handle = Some(thread::spawn(move || server.start())); + HttpServerHandle { addr, handle } } fn start(&self) { @@ -615,7 +651,7 @@ impl HttpServer { /// Route the request fn route(&self, req: &Request) -> Response { let authorized = |mutatation: bool| { - if mutatation { + if mutatation || self.auth_required { self.token == req.authorization } else { assert!(req.authorization.is_none(), "unexpected token"); @@ -676,7 +712,9 @@ impl HttpServer { pub fn unauthorized(&self, _req: &Request) -> Response { Response { code: 401, - headers: vec![], + headers: vec![ + r#"WWW-Authenticate: Cargo login_url="https://test-registry-login/me""#.to_string(), + ], body: b"Unauthorized message from server.".to_vec(), } } diff --git a/crates/crates-io/lib.rs b/crates/crates-io/lib.rs index ed835523aa1..86217d1d229 100644 --- a/crates/crates-io/lib.rs +++ b/crates/crates-io/lib.rs @@ -21,6 +21,8 @@ pub struct Registry { token: Option, /// Curl handle for issuing requests. handle: Easy, + /// Whether to include the authorization token with all requests. + auth_required: bool, } #[derive(PartialEq, Clone, Copy)] @@ -199,11 +201,17 @@ impl Registry { /// handle.useragent("my_crawler (example.com/info)"); /// let mut reg = Registry::new_handle(String::from("https://crates.io"), None, handle); /// ``` - pub fn new_handle(host: String, token: Option, handle: Easy) -> Registry { + pub fn new_handle( + host: String, + token: Option, + handle: Easy, + auth_required: bool, + ) -> Registry { Registry { host, token, handle, + auth_required, } } @@ -377,7 +385,7 @@ impl Registry { headers.append("Accept: application/json")?; headers.append("Content-Type: application/json")?; - if authorized == Auth::Authorized { + if self.auth_required || authorized == Auth::Authorized { let token = match self.token.as_ref() { Some(s) => s, None => bail!("no upload token found, please run `cargo login`"), diff --git a/crates/credential/cargo-credential-1password/Cargo.toml b/crates/credential/cargo-credential-1password/Cargo.toml index 6d24ccdc693..093fde8e531 100644 --- a/crates/credential/cargo-credential-1password/Cargo.toml +++ b/crates/credential/cargo-credential-1password/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "cargo-credential-1password" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/cargo" description = "A Cargo credential process that stores tokens in a 1password vault." [dependencies] -cargo-credential = { version = "0.1.0", path = "../cargo-credential" } +cargo-credential = { version = "0.2.0", path = "../cargo-credential" } serde = { version = "1.0.117", features = ["derive"] } serde_json = "1.0.59" diff --git a/crates/credential/cargo-credential-1password/src/main.rs b/crates/credential/cargo-credential-1password/src/main.rs index 7eea3a0b6ed..354eaa9e454 100644 --- a/crates/credential/cargo-credential-1password/src/main.rs +++ b/crates/credential/cargo-credential-1password/src/main.rs @@ -41,7 +41,7 @@ struct ListItem { #[derive(Deserialize)] struct Overview { - title: String, + url: String, } impl OnePasswordKeychain { @@ -175,11 +175,7 @@ impl OnePasswordKeychain { Ok(buffer) } - fn search( - &self, - session: &Option, - registry_name: &str, - ) -> Result, Error> { + fn search(&self, session: &Option, index_url: &str) -> Result, Error> { let cmd = self.make_cmd( session, &[ @@ -196,15 +192,15 @@ impl OnePasswordKeychain { .map_err(|e| format!("failed to deserialize JSON from 1password list: {}", e))?; let mut matches = items .into_iter() - .filter(|item| item.overview.title == registry_name); + .filter(|item| item.overview.url == index_url); match matches.next() { Some(login) => { // Should this maybe just sort on `updatedAt` and return the newest one? if matches.next().is_some() { return Err(format!( - "too many 1password logins match registry name {}, \ + "too many 1password logins match registry `{}`, \ consider deleting the excess entries", - registry_name + index_url ) .into()); } @@ -214,7 +210,13 @@ impl OnePasswordKeychain { } } - fn modify(&self, session: &Option, uuid: &str, token: &str) -> Result<(), Error> { + fn modify( + &self, + session: &Option, + uuid: &str, + token: &str, + _name: Option<&str>, + ) -> Result<(), Error> { let cmd = self.make_cmd( session, &["edit", "item", uuid, &format!("password={}", token)], @@ -226,10 +228,14 @@ impl OnePasswordKeychain { fn create( &self, session: &Option, - registry_name: &str, - api_url: &str, + index_url: &str, token: &str, + name: Option<&str>, ) -> Result<(), Error> { + let title = match name { + Some(name) => format!("Cargo registry token for {}", name), + None => "Cargo registry token".to_string(), + }; let cmd = self.make_cmd( session, &[ @@ -237,9 +243,9 @@ impl OnePasswordKeychain { "item", "Login", &format!("password={}", token), - &format!("url={}", api_url), + &format!("url={}", index_url), "--title", - registry_name, + &title, "--tags", CARGO_TAG, ], @@ -276,36 +282,36 @@ impl Credential for OnePasswordKeychain { env!("CARGO_PKG_NAME") } - fn get(&self, registry_name: &str, _api_url: &str) -> Result { + fn get(&self, index_url: &str) -> Result { let session = self.signin()?; - if let Some(uuid) = self.search(&session, registry_name)? { + if let Some(uuid) = self.search(&session, index_url)? { self.get_token(&session, &uuid) } else { return Err(format!( "no 1password entry found for registry `{}`, try `cargo login` to add a token", - registry_name + index_url ) .into()); } } - fn store(&self, registry_name: &str, api_url: &str, token: &str) -> Result<(), Error> { + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> { let session = self.signin()?; // Check if an item already exists. - if let Some(uuid) = self.search(&session, registry_name)? { - self.modify(&session, &uuid, token) + if let Some(uuid) = self.search(&session, index_url)? { + self.modify(&session, &uuid, token, name) } else { - self.create(&session, registry_name, api_url, token) + self.create(&session, index_url, token, name) } } - fn erase(&self, registry_name: &str, _api_url: &str) -> Result<(), Error> { + fn erase(&self, index_url: &str) -> Result<(), Error> { let session = self.signin()?; // Check if an item already exists. - if let Some(uuid) = self.search(&session, registry_name)? { + if let Some(uuid) = self.search(&session, index_url)? { self.delete(&session, &uuid)?; } else { - eprintln!("not currently logged in to `{}`", registry_name); + eprintln!("not currently logged in to `{}`", index_url); } Ok(()) } diff --git a/crates/credential/cargo-credential-gnome-secret/Cargo.toml b/crates/credential/cargo-credential-gnome-secret/Cargo.toml index bfa073479fd..12e25cfb646 100644 --- a/crates/credential/cargo-credential-gnome-secret/Cargo.toml +++ b/crates/credential/cargo-credential-gnome-secret/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "cargo-credential-gnome-secret" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/cargo" description = "A Cargo credential process that stores tokens with GNOME libsecret." [dependencies] -cargo-credential = { version = "0.1.0", path = "../cargo-credential" } +cargo-credential = { version = "0.2.0", path = "../cargo-credential" } [build-dependencies] pkg-config = "0.3.19" diff --git a/crates/credential/cargo-credential-gnome-secret/src/main.rs b/crates/credential/cargo-credential-gnome-secret/src/main.rs index 582a5bfb371..40972b05dcc 100644 --- a/crates/credential/cargo-credential-gnome-secret/src/main.rs +++ b/crates/credential/cargo-credential-gnome-secret/src/main.rs @@ -76,8 +76,8 @@ extern "C" { struct GnomeSecret; -fn label(registry_name: &str) -> CString { - CString::new(format!("cargo-registry:{}", registry_name)).unwrap() +fn label(index_url: &str) -> CString { + CString::new(format!("cargo-registry:{}", index_url)).unwrap() } fn schema() -> SecretSchema { @@ -86,10 +86,6 @@ fn schema() -> SecretSchema { attr_type: SecretSchemaAttributeType::String, }; 32]; attributes[0] = SecretSchemaAttribute { - name: b"registry\0".as_ptr() as *const gchar, - attr_type: SecretSchemaAttributeType::String, - }; - attributes[1] = SecretSchemaAttribute { name: b"url\0".as_ptr() as *const gchar, attr_type: SecretSchemaAttributeType::String, }; @@ -105,22 +101,18 @@ impl Credential for GnomeSecret { env!("CARGO_PKG_NAME") } - fn get(&self, registry_name: &str, api_url: &str) -> Result { + fn get(&self, index_url: &str) -> Result { let mut error: *mut GError = null_mut(); - let attr_registry = CString::new("registry").unwrap(); let attr_url = CString::new("url").unwrap(); - let registry_name_c = CString::new(registry_name).unwrap(); - let api_url_c = CString::new(api_url).unwrap(); + let index_url_c = CString::new(index_url).unwrap(); let schema = schema(); unsafe { let token_c = secret_password_lookup_sync( &schema, null_mut(), &mut error, - attr_registry.as_ptr(), - registry_name_c.as_ptr(), attr_url.as_ptr(), - api_url_c.as_ptr(), + index_url_c.as_ptr(), null() as *const gchar, ); if !error.is_null() { @@ -131,7 +123,7 @@ impl Credential for GnomeSecret { .into()); } if token_c.is_null() { - return Err(format!("cannot find token for {}", registry_name).into()); + return Err(format!("cannot find token for {}", index_url).into()); } let token = CStr::from_ptr(token_c) .to_str() @@ -141,14 +133,12 @@ impl Credential for GnomeSecret { } } - fn store(&self, registry_name: &str, api_url: &str, token: &str) -> Result<(), Error> { - let label = label(registry_name); + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> { + let label = label(name.unwrap_or(index_url)); let token = CString::new(token).unwrap(); let mut error: *mut GError = null_mut(); - let attr_registry = CString::new("registry").unwrap(); let attr_url = CString::new("url").unwrap(); - let registry_name_c = CString::new(registry_name).unwrap(); - let api_url_c = CString::new(api_url).unwrap(); + let index_url_c = CString::new(index_url).unwrap(); let schema = schema(); unsafe { secret_password_store_sync( @@ -158,10 +148,8 @@ impl Credential for GnomeSecret { token.as_ptr(), null_mut(), &mut error, - attr_registry.as_ptr(), - registry_name_c.as_ptr(), attr_url.as_ptr(), - api_url_c.as_ptr(), + index_url_c.as_ptr(), null() as *const gchar, ); if !error.is_null() { @@ -175,22 +163,18 @@ impl Credential for GnomeSecret { Ok(()) } - fn erase(&self, registry_name: &str, api_url: &str) -> Result<(), Error> { + fn erase(&self, index_url: &str) -> Result<(), Error> { let schema = schema(); let mut error: *mut GError = null_mut(); - let attr_registry = CString::new("registry").unwrap(); let attr_url = CString::new("url").unwrap(); - let registry_name_c = CString::new(registry_name).unwrap(); - let api_url_c = CString::new(api_url).unwrap(); + let index_url_c = CString::new(index_url).unwrap(); unsafe { secret_password_clear_sync( &schema, null_mut(), &mut error, - attr_registry.as_ptr(), - registry_name_c.as_ptr(), attr_url.as_ptr(), - api_url_c.as_ptr(), + index_url_c.as_ptr(), null() as *const gchar, ); if !error.is_null() { diff --git a/crates/credential/cargo-credential-macos-keychain/Cargo.toml b/crates/credential/cargo-credential-macos-keychain/Cargo.toml index 8b78c34bc08..c2c22a425ae 100644 --- a/crates/credential/cargo-credential-macos-keychain/Cargo.toml +++ b/crates/credential/cargo-credential-macos-keychain/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "cargo-credential-macos-keychain" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/cargo" description = "A Cargo credential process that stores tokens in a macOS keychain." [dependencies] -cargo-credential = { version = "0.1.0", path = "../cargo-credential" } +cargo-credential = { version = "0.2.0", path = "../cargo-credential" } security-framework = "2.0.0" diff --git a/crates/credential/cargo-credential-macos-keychain/src/main.rs b/crates/credential/cargo-credential-macos-keychain/src/main.rs index 4f12585ed18..3fef3f92a6f 100644 --- a/crates/credential/cargo-credential-macos-keychain/src/main.rs +++ b/crates/credential/cargo-credential-macos-keychain/src/main.rs @@ -17,17 +17,17 @@ impl Credential for MacKeychain { env!("CARGO_PKG_NAME") } - fn get(&self, registry_name: &str, _api_url: &str) -> Result { + fn get(&self, index_url: &str) -> Result { let keychain = SecKeychain::default().unwrap(); - let service_name = registry(registry_name); + let service_name = registry(index_url); let (pass, _item) = keychain.find_generic_password(&service_name, ACCOUNT)?; String::from_utf8(pass.as_ref().to_vec()) .map_err(|_| "failed to convert token to UTF8".into()) } - fn store(&self, registry_name: &str, _api_url: &str, token: &str) -> Result<(), Error> { + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> { let keychain = SecKeychain::default().unwrap(); - let service_name = registry(registry_name); + let service_name = registry(name.unwrap_or(index_url)); if let Ok((_pass, mut item)) = keychain.find_generic_password(&service_name, ACCOUNT) { item.set_password(token.as_bytes())?; } else { @@ -36,9 +36,9 @@ impl Credential for MacKeychain { Ok(()) } - fn erase(&self, registry_name: &str, _api_url: &str) -> Result<(), Error> { + fn erase(&self, index_url: &str) -> Result<(), Error> { let keychain = SecKeychain::default().unwrap(); - let service_name = registry(registry_name); + let service_name = registry(index_url); let (_pass, item) = keychain.find_generic_password(&service_name, ACCOUNT)?; item.delete(); Ok(()) diff --git a/crates/credential/cargo-credential-wincred/Cargo.toml b/crates/credential/cargo-credential-wincred/Cargo.toml index 4711fb788f7..b612c26fe7a 100644 --- a/crates/credential/cargo-credential-wincred/Cargo.toml +++ b/crates/credential/cargo-credential-wincred/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "cargo-credential-wincred" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/cargo" description = "A Cargo credential process that stores tokens with Windows Credential Manager." [dependencies] -cargo-credential = { version = "0.1.0", path = "../cargo-credential" } +cargo-credential = { version = "0.2.0", path = "../cargo-credential" } winapi = { version = "0.3.9", features = ["wincred", "winerror", "impl-default"] } diff --git a/crates/credential/cargo-credential-wincred/src/main.rs b/crates/credential/cargo-credential-wincred/src/main.rs index 5e534b3a673..8661d28b80b 100644 --- a/crates/credential/cargo-credential-wincred/src/main.rs +++ b/crates/credential/cargo-credential-wincred/src/main.rs @@ -29,8 +29,8 @@ impl Credential for WindowsCredential { env!("CARGO_PKG_NAME") } - fn get(&self, registry_name: &str, _api_url: &str) -> Result { - let target_name = target_name(registry_name); + fn get(&self, index_url: &str) -> Result { + let target_name = target_name(index_url); let mut p_credential: wincred::PCREDENTIALW = std::ptr::null_mut(); unsafe { if wincred::CredReadW( @@ -52,10 +52,13 @@ impl Credential for WindowsCredential { } } - fn store(&self, registry_name: &str, _api_url: &str, token: &str) -> Result<(), Error> { + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> { let token = token.as_bytes(); - let target_name = target_name(registry_name); - let comment = wstr("Cargo registry token"); + let target_name = target_name(index_url); + let comment = match name { + Some(name) => wstr(&format!("Cargo registry token for {}", name)), + None => wstr("Cargo registry token"), + }; let mut credential = wincred::CREDENTIALW { Flags: 0, Type: wincred::CRED_TYPE_GENERIC, @@ -78,14 +81,14 @@ impl Credential for WindowsCredential { Ok(()) } - fn erase(&self, registry_name: &str, _api_url: &str) -> Result<(), Error> { - let target_name = target_name(registry_name); + fn erase(&self, index_url: &str) -> Result<(), Error> { + let target_name = target_name(index_url); let result = unsafe { wincred::CredDeleteW(target_name.as_ptr(), wincred::CRED_TYPE_GENERIC, 0) }; if result != TRUE { let err = std::io::Error::last_os_error(); if err.raw_os_error() == Some(winerror::ERROR_NOT_FOUND as i32) { - eprintln!("not currently logged in to `{}`", registry_name); + eprintln!("not currently logged in to `{}`", index_url); return Ok(()); } return Err(format!("failed to remove token: {}", err).into()); diff --git a/crates/credential/cargo-credential/Cargo.toml b/crates/credential/cargo-credential/Cargo.toml index 6821fa8ed7e..2addaf5afad 100644 --- a/crates/credential/cargo-credential/Cargo.toml +++ b/crates/credential/cargo-credential/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-credential" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/cargo" diff --git a/crates/credential/cargo-credential/src/lib.rs b/crates/credential/cargo-credential/src/lib.rs index 43dc0ba3176..3baf42d77db 100644 --- a/crates/credential/cargo-credential/src/lib.rs +++ b/crates/credential/cargo-credential/src/lib.rs @@ -21,17 +21,17 @@ pub trait Credential { fn name(&self) -> &'static str; /// Retrieves a token for the given registry. - fn get(&self, registry_name: &str, api_url: &str) -> Result; + fn get(&self, index_url: &str) -> Result; /// Stores the given token for the given registry. - fn store(&self, registry_name: &str, api_url: &str, token: &str) -> Result<(), Error>; + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error>; /// Removes the token for the given registry. /// /// If the user is not logged in, this should print a message to stderr if /// possible indicating that the user is not currently logged in, and /// return `Ok`. - fn erase(&self, registry_name: &str, api_url: &str) -> Result<(), Error>; + fn erase(&self, index_url: &str) -> Result<(), Error>; } /// Runs the credential interaction by processing the command-line and @@ -54,17 +54,17 @@ fn doit(credential: impl Credential) -> Result<(), Error> { .skip_while(|arg| arg.starts_with('-')) .next() .ok_or_else(|| "first argument must be the {action}")?; - let registry_name = env("CARGO_REGISTRY_NAME")?; - let api_url = env("CARGO_REGISTRY_API_URL")?; + let index_url = env("CARGO_REGISTRY_INDEX_URL")?; + let name = std::env::var("CARGO_REGISTRY_NAME_OPT").ok(); let result = match which.as_ref() { - "get" => credential.get(®istry_name, &api_url).and_then(|token| { + "get" => credential.get(&index_url).and_then(|token| { println!("{}", token); Ok(()) }), "store" => { - read_token().and_then(|token| credential.store(®istry_name, &api_url, &token)) + read_token().and_then(|token| credential.store(&index_url, &token, name.as_deref())) } - "erase" => credential.erase(®istry_name, &api_url), + "erase" => credential.erase(&index_url), _ => { return Err(format!( "unexpected command-line argument `{}`, expected get/store/erase", diff --git a/src/bin/cargo/commands/login.rs b/src/bin/cargo/commands/login.rs index 6dc74200aa2..05595abca77 100644 --- a/src/bin/cargo/commands/login.rs +++ b/src/bin/cargo/commands/login.rs @@ -17,8 +17,8 @@ pub fn cli() -> Command { pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { ops::registry_login( config, - args.get_one::("token").cloned(), - args.get_one::("registry").cloned(), + args.get_one("token").map(String::as_str), + args.get_one("registry").map(String::as_str), )?; Ok(()) } diff --git a/src/bin/cargo/commands/logout.rs b/src/bin/cargo/commands/logout.rs index ede4800a53a..bc16ee55ecf 100644 --- a/src/bin/cargo/commands/logout.rs +++ b/src/bin/cargo/commands/logout.rs @@ -15,7 +15,9 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { .cli_unstable() .fail_if_stable_command(config, "logout", 8933)?; } - config.load_credentials()?; - ops::registry_logout(config, args.get_one::("registry").cloned())?; + ops::registry_logout( + config, + args.get_one::("registry").map(String::as_str), + )?; Ok(()) } diff --git a/src/bin/cargo/commands/owner.rs b/src/bin/cargo/commands/owner.rs index 2e0d818ba3d..170dd69e71e 100644 --- a/src/bin/cargo/commands/owner.rs +++ b/src/bin/cargo/commands/owner.rs @@ -31,8 +31,6 @@ pub fn cli() -> Command { } pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { - config.load_credentials()?; - let registry = args.registry(config)?; let opts = OwnersOptions { krate: args.get_one::("crate").cloned(), diff --git a/src/bin/cargo/commands/publish.rs b/src/bin/cargo/commands/publish.rs index c33e74f1013..7dd6414bada 100644 --- a/src/bin/cargo/commands/publish.rs +++ b/src/bin/cargo/commands/publish.rs @@ -28,8 +28,6 @@ pub fn cli() -> Command { } pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { - config.load_credentials()?; - let registry = args.registry(config)?; let ws = args.workspace(config)?; let index = args.index()?; diff --git a/src/bin/cargo/commands/yank.rs b/src/bin/cargo/commands/yank.rs index 0f84a0a5a8a..dda766dd13c 100644 --- a/src/bin/cargo/commands/yank.rs +++ b/src/bin/cargo/commands/yank.rs @@ -23,8 +23,6 @@ pub fn cli() -> Command { } pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { - config.load_credentials()?; - let registry = args.registry(config)?; let (krate, version) = resolve_crate( diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index c2046ec3db5..a87785bb045 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -682,6 +682,7 @@ unstable_cli_options!( panic_abort_tests: bool = ("Enable support to run tests with -Cpanic=abort"), host_config: bool = ("Enable the [host] section in the .cargo/config.toml file"), sparse_registry: bool = ("Support plain-HTTP-based crate registries"), + registry_auth: bool = ("Authentication for alternative registries"), target_applies_to_host: bool = ("Enable the `target-applies-to-host` key in the .cargo/config.toml file"), rustdoc_map: bool = ("Allow passing external documentation mappings to rustdoc"), separate_nightlies: bool = (HIDDEN), @@ -958,6 +959,7 @@ impl CliUnstable { "rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?, "terminal-width" => self.terminal_width = Some(parse_usize_opt(v)?), "sparse-registry" => self.sparse_registry = parse_empty(k, v)?, + "registry-auth" => self.registry_auth = parse_empty(k, v)?, "namespaced-features" => stabilized_warn(k, "1.60", STABILISED_NAMESPACED_FEATURES), "weak-dep-features" => stabilized_warn(k, "1.60", STABILIZED_WEAK_DEP_FEATURES), "credential-process" => self.credential_process = parse_empty(k, v)?, diff --git a/src/cargo/core/package.rs b/src/cargo/core/package.rs index ff5af357d72..f3fed18484e 100644 --- a/src/cargo/core/package.rs +++ b/src/cargo/core/package.rs @@ -703,13 +703,17 @@ impl<'a, 'cfg> Downloads<'a, 'cfg> { let pkg = source .download(id) .with_context(|| "unable to get packages from source")?; - let (url, descriptor) = match pkg { + let (url, descriptor, authorization) = match pkg { MaybePackage::Ready(pkg) => { debug!("{} doesn't need a download", id); assert!(slot.fill(pkg).is_ok()); return Ok(Some(slot.borrow().unwrap())); } - MaybePackage::Download { url, descriptor } => (url, descriptor), + MaybePackage::Download { + url, + descriptor, + authorization, + } => (url, descriptor, authorization), }; // Ok we're going to download this crate, so let's set up all our @@ -726,6 +730,13 @@ impl<'a, 'cfg> Downloads<'a, 'cfg> { handle.url(&url)?; handle.follow_location(true)?; // follow redirects + // Add authorization header. + if let Some(authorization) = authorization { + let mut headers = curl::easy::List::new(); + headers.append(&format!("Authorization: {}", authorization))?; + handle.http_headers(headers)?; + } + // Enable HTTP/2 to be used as it'll allow true multiplexing which makes // downloads much faster. // diff --git a/src/cargo/core/source/mod.rs b/src/cargo/core/source/mod.rs index 229e6cdea0c..e82e7e184fb 100644 --- a/src/cargo/core/source/mod.rs +++ b/src/cargo/core/source/mod.rs @@ -121,7 +121,11 @@ pub enum QueryKind { pub enum MaybePackage { Ready(Package), - Download { url: String, descriptor: String }, + Download { + url: String, + descriptor: String, + authorization: Option, + }, } impl<'a, T: Source + ?Sized + 'a> Source for Box { diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index ea04aa0be32..397719f689e 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -25,7 +25,7 @@ pub use self::registry::HttpTimeout; pub use self::registry::{configure_http_handle, http_handle, http_handle_and_timeout}; pub use self::registry::{modify_owners, yank, OwnersOptions, PublishOpts}; pub use self::registry::{needs_custom_http_transport, registry_login, registry_logout, search}; -pub use self::registry::{publish, registry_configuration, RegistryConfig}; +pub use self::registry::{publish, RegistryCredentialConfig}; pub use self::resolve::{ add_overrides, get_resolved_packages, resolve_with_previous, resolve_ws, resolve_ws_with_opts, WorkspaceResolve, diff --git a/src/cargo/ops/registry.rs b/src/cargo/ops/registry.rs index cc37222f8c0..3346e5b41a6 100644 --- a/src/cargo/ops/registry.rs +++ b/src/cargo/ops/registry.rs @@ -27,19 +27,18 @@ use crate::core::{Package, SourceId, Workspace}; use crate::ops; use crate::ops::Packages; use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_DOMAIN, CRATES_IO_REGISTRY}; -use crate::util::config::{self, Config, SslVersionConfig, SslVersionConfigRange}; +use crate::util::auth::{self, AuthorizationError}; +use crate::util::config::{Config, SslVersionConfig, SslVersionConfigRange}; use crate::util::errors::CargoResult; use crate::util::important_paths::find_root_manifest_for_wd; use crate::util::{truncate_with_ellipsis, IntoUrl}; use crate::{drop_print, drop_println, version}; -mod auth; - /// Registry settings loaded from config files. /// /// This is loaded based on the `--registry` flag and the config settings. #[derive(Debug)] -pub enum RegistryConfig { +pub enum RegistryCredentialConfig { None, /// The authentication token. Token(String), @@ -47,7 +46,7 @@ pub enum RegistryConfig { Process((PathBuf, Vec)), } -impl RegistryConfig { +impl RegistryCredentialConfig { /// Returns `true` if the credential is [`None`]. /// /// [`None`]: Self::None @@ -150,9 +149,9 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> { } } - let (mut registry, _reg_cfg, reg_ids) = registry( + let (mut registry, reg_ids) = registry( opts.config, - opts.token.clone(), + opts.token.as_deref(), opts.index.as_deref(), publish_registry.as_deref(), true, @@ -461,95 +460,36 @@ fn wait_for_publish( Ok(()) } -/// Returns the index and token from the config file for the given registry. -/// -/// `registry` is typically the registry specified on the command-line. If -/// `None`, `index` is set to `None` to indicate it should use crates.io. -pub fn registry_configuration( - config: &Config, - registry: Option<&str>, -) -> CargoResult { - let err_both = |token_key: &str, proc_key: &str| { - Err(format_err!( - "both `{token_key}` and `{proc_key}` \ - were specified in the config\n\ - Only one of these values may be set, remove one or the other to proceed.", - )) - }; - // `registry.default` is handled in command-line parsing. - let (token, process) = match registry { - Some("crates-io") | None => { - // Use crates.io default. - config.check_registry_index_not_set()?; - let token = config.get_string("registry.token")?.map(|p| p.val); - let process = if config.cli_unstable().credential_process { - let process = - config.get::>("registry.credential-process")?; - if token.is_some() && process.is_some() { - return err_both("registry.token", "registry.credential-process"); - } - process - } else { - None - }; - (token, process) - } - Some(registry) => { - let token_key = format!("registries.{registry}.token"); - let token = config.get_string(&token_key)?.map(|p| p.val); - let process = if config.cli_unstable().credential_process { - let mut proc_key = format!("registries.{registry}.credential-process"); - let mut process = config.get::>(&proc_key)?; - if process.is_none() && token.is_none() { - // This explicitly ignores the global credential-process if - // the token is set, as that is "more specific". - proc_key = String::from("registry.credential-process"); - process = config.get::>(&proc_key)?; - } else if process.is_some() && token.is_some() { - return err_both(&token_key, &proc_key); - } - process - } else { - None - }; - (token, process) - } - }; - - let credential_process = - process.map(|process| (process.path.resolve_program(config), process.args)); - - Ok(match (token, credential_process) { - (None, None) => RegistryConfig::None, - (None, Some(process)) => RegistryConfig::Process(process), - (Some(x), None) => RegistryConfig::Token(x), - (Some(_), Some(_)) => unreachable!("Only one of these values may be set."), - }) -} - /// Returns the `Registry` and `Source` based on command-line and config settings. /// -/// * `token`: The token from the command-line. If not set, uses the token +/// * `token_from_cmdline`: The token from the command-line. If not set, uses the token /// from the config. /// * `index`: The index URL from the command-line. /// * `registry`: The registry name from the command-line. If neither /// `registry`, or `index` are set, then uses `crates-io`. /// * `force_update`: If `true`, forces the index to be updated. -/// * `validate_token`: If `true`, the token must be set. +/// * `token_required`: If `true`, the token will be set. fn registry( config: &Config, - token: Option, + token_from_cmdline: Option<&str>, index: Option<&str>, registry: Option<&str>, force_update: bool, - validate_token: bool, -) -> CargoResult<(Registry, RegistryConfig, RegistrySourceIds)> { + token_required: bool, +) -> CargoResult<(Registry, RegistrySourceIds)> { let source_ids = get_source_id(config, index, registry)?; - let reg_cfg = registry_configuration(config, registry)?; - let api_host = { + + if token_required && index.is_some() && token_from_cmdline.is_none() { + bail!("command-line argument --index requires --token to be specified"); + } + if let Some(token) = token_from_cmdline { + auth::cache_token(config, &source_ids.original, token); + } + + let cfg = { let _lock = config.acquire_package_cache_lock()?; let mut src = RegistrySource::remote(source_ids.replacement, &HashSet::new(), config)?; - // Only update the index if the config is not available or `force` is set. + // Only update the index if `force_update` is set. if force_update { src.invalidate_cache() } @@ -561,28 +501,19 @@ fn registry( Poll::Ready(cfg) => break cfg, } }; - - cfg.and_then(|cfg| cfg.api).ok_or_else(|| { - format_err!("{} does not support API commands", source_ids.replacement) - })? + cfg.expect("remote registries must have config") }; - let token = if validate_token { - if index.is_some() { - if token.is_none() { - bail!("command-line argument --index requires --token to be specified"); - } - token - } else { - let token = auth::auth_token(config, token.as_deref(), ®_cfg, registry, &api_host)?; - Some(token) - } + let api_host = cfg + .api + .ok_or_else(|| format_err!("{} does not support API commands", source_ids.replacement))?; + let token = if token_required || cfg.auth_required { + Some(auth::auth_token(config, &source_ids.original, None)?) } else { None }; let handle = http_handle(config)?; Ok(( - Registry::new_handle(api_host, token, handle), - reg_cfg, + Registry::new_handle(api_host, token, handle, cfg.auth_required), source_ids, )) } @@ -807,22 +738,36 @@ fn http_proxy_exists(config: &Config) -> CargoResult { } } -pub fn registry_login( - config: &Config, - token: Option, - reg: Option, -) -> CargoResult<()> { - let (registry, reg_cfg, _) = - registry(config, token.clone(), None, reg.as_deref(), false, false)?; +pub fn registry_login(config: &Config, token: Option<&str>, reg: Option<&str>) -> CargoResult<()> { + let source_ids = get_source_id(config, None, reg)?; + let reg_cfg = auth::registry_credential_config(config, &source_ids.original)?; + let login_url = match registry(config, token, None, reg, false, false) { + Ok((registry, _)) => Some(format!("{}/me", registry.host())), + Err(e) if e.is::() => e + .downcast::() + .unwrap() + .login_url + .map(|u| u.to_string()), + Err(e) => return Err(e), + }; let token = match token { - Some(token) => token, + Some(token) => token.to_string(), None => { - drop_println!( - config, - "please paste the API Token found on {}/me below", - registry.host() - ); + if let Some(login_url) = login_url { + drop_println!( + config, + "please paste the token found on {} below", + login_url + ) + } else { + drop_println!( + config, + "please paste the token for {} below", + source_ids.original.display_registry_name() + ) + } + let mut line = String::new(); let input = io::stdin(); input @@ -839,34 +784,26 @@ pub fn registry_login( bail!("please provide a non-empty token"); } - if let RegistryConfig::Token(old_token) = ®_cfg { + if let RegistryCredentialConfig::Token(old_token) = ®_cfg { if old_token == &token { config.shell().status("Login", "already logged in")?; return Ok(()); } } - auth::login( - config, - token, - reg_cfg.as_process(), - reg.as_deref(), - registry.host(), - )?; + auth::login(config, &source_ids.original, token)?; config.shell().status( "Login", - format!( - "token for `{}` saved", - reg.as_ref().map_or(CRATES_IO_DOMAIN, String::as_str) - ), + format!("token for `{}` saved", reg.unwrap_or(CRATES_IO_DOMAIN)), )?; Ok(()) } -pub fn registry_logout(config: &Config, reg: Option) -> CargoResult<()> { - let (registry, reg_cfg, _) = registry(config, None, None, reg.as_deref(), false, false)?; - let reg_name = reg.as_deref().unwrap_or(CRATES_IO_DOMAIN); +pub fn registry_logout(config: &Config, reg: Option<&str>) -> CargoResult<()> { + let source_ids = get_source_id(config, None, reg)?; + let reg_cfg = auth::registry_credential_config(config, &source_ids.original)?; + let reg_name = source_ids.original.display_registry_name(); if reg_cfg.is_none() { config.shell().status( "Logout", @@ -874,12 +811,7 @@ pub fn registry_logout(config: &Config, reg: Option) -> CargoResult<()> )?; return Ok(()); } - auth::logout( - config, - reg_cfg.as_process(), - reg.as_deref(), - registry.host(), - )?; + auth::logout(config, &source_ids.original)?; config.shell().status( "Logout", format!( @@ -910,9 +842,9 @@ pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> { } }; - let (mut registry, _, _) = registry( + let (mut registry, _) = registry( config, - opts.token.clone(), + opts.token.as_deref(), opts.index.as_deref(), opts.registry.as_deref(), true, @@ -989,8 +921,14 @@ pub fn yank( None => bail!("a version must be specified to yank"), }; - let (mut registry, _, _) = - registry(config, token, index.as_deref(), reg.as_deref(), true, true)?; + let (mut registry, _) = registry( + config, + token.as_deref(), + index.as_deref(), + reg.as_deref(), + true, + true, + )?; let package_spec = format!("{}@{}", name, version); if undo { @@ -1076,7 +1014,7 @@ pub fn search( limit: u32, reg: Option, ) -> CargoResult<()> { - let (mut registry, _, source_ids) = + let (mut registry, source_ids) = registry(config, None, index.as_deref(), reg.as_deref(), false, false)?; let (crates, total_crates) = registry.search(query, limit).with_context(|| { format!( diff --git a/src/cargo/ops/registry/auth.rs b/src/cargo/ops/registry/auth.rs deleted file mode 100644 index 85b51a50ab0..00000000000 --- a/src/cargo/ops/registry/auth.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! Registry authentication support. - -use crate::sources::CRATES_IO_REGISTRY; -use crate::util::{config, CargoResult, Config}; -use anyhow::{bail, format_err, Context as _}; -use cargo_util::ProcessError; -use std::io::{Read, Write}; -use std::path::PathBuf; -use std::process::{Command, Stdio}; - -use super::RegistryConfig; - -enum Action { - Get, - Store(String), - Erase, -} - -/// Returns the token to use for the given registry. -pub(super) fn auth_token( - config: &Config, - cli_token: Option<&str>, - credential: &RegistryConfig, - registry_name: Option<&str>, - api_url: &str, -) -> CargoResult { - let token = match (cli_token, credential) { - (None, RegistryConfig::None) => { - bail!("no upload token found, please run `cargo login` or pass `--token`"); - } - (Some(cli_token), _) => cli_token.to_string(), - (None, RegistryConfig::Token(config_token)) => config_token.to_string(), - (None, RegistryConfig::Process(process)) => { - let registry_name = registry_name.unwrap_or(CRATES_IO_REGISTRY); - run_command(config, process, registry_name, api_url, Action::Get)?.unwrap() - } - }; - Ok(token) -} - -/// Saves the given token. -pub(super) fn login( - config: &Config, - token: String, - credential_process: Option<&(PathBuf, Vec)>, - registry_name: Option<&str>, - api_url: &str, -) -> CargoResult<()> { - if let Some(process) = credential_process { - let registry_name = registry_name.unwrap_or(CRATES_IO_REGISTRY); - run_command( - config, - process, - registry_name, - api_url, - Action::Store(token), - )?; - } else { - config::save_credentials(config, Some(token), registry_name)?; - } - Ok(()) -} - -/// Removes the token for the given registry. -pub(super) fn logout( - config: &Config, - credential_process: Option<&(PathBuf, Vec)>, - registry_name: Option<&str>, - api_url: &str, -) -> CargoResult<()> { - if let Some(process) = credential_process { - let registry_name = registry_name.unwrap_or(CRATES_IO_REGISTRY); - run_command(config, process, registry_name, api_url, Action::Erase)?; - } else { - config::save_credentials(config, None, registry_name)?; - } - Ok(()) -} - -fn run_command( - config: &Config, - process: &(PathBuf, Vec), - name: &str, - api_url: &str, - action: Action, -) -> CargoResult> { - let cred_proc; - let (exe, args) = if process.0.to_str().unwrap_or("").starts_with("cargo:") { - cred_proc = sysroot_credential(config, process)?; - &cred_proc - } else { - process - }; - if !args.iter().any(|arg| arg.contains("{action}")) { - let msg = |which| { - format!( - "credential process `{}` cannot be used to {}, \ - the credential-process configuration value must pass the \ - `{{action}}` argument in the config to support this command", - exe.display(), - which - ) - }; - match action { - Action::Get => {} - Action::Store(_) => bail!(msg("log in")), - Action::Erase => bail!(msg("log out")), - } - } - let action_str = match action { - Action::Get => "get", - Action::Store(_) => "store", - Action::Erase => "erase", - }; - let args: Vec<_> = args - .iter() - .map(|arg| { - arg.replace("{action}", action_str) - .replace("{name}", name) - .replace("{api_url}", api_url) - }) - .collect(); - - let mut cmd = Command::new(&exe); - cmd.args(args) - .env(crate::CARGO_ENV, config.cargo_exe()?) - .env("CARGO_REGISTRY_NAME", name) - .env("CARGO_REGISTRY_API_URL", api_url); - match action { - Action::Get => { - cmd.stdout(Stdio::piped()); - } - Action::Store(_) => { - cmd.stdin(Stdio::piped()); - } - Action::Erase => {} - } - let mut child = cmd.spawn().with_context(|| { - let verb = match action { - Action::Get => "fetch", - Action::Store(_) => "store", - Action::Erase => "erase", - }; - format!( - "failed to execute `{}` to {} authentication token for registry `{}`", - exe.display(), - verb, - name - ) - })?; - let mut token = None; - match &action { - Action::Get => { - let mut buffer = String::new(); - log::debug!("reading into buffer"); - child - .stdout - .as_mut() - .unwrap() - .read_to_string(&mut buffer) - .with_context(|| { - format!( - "failed to read token from registry credential process `{}`", - exe.display() - ) - })?; - if let Some(end) = buffer.find('\n') { - if buffer.len() > end + 1 { - bail!( - "credential process `{}` returned more than one line of output; \ - expected a single token", - exe.display() - ); - } - buffer.truncate(end); - } - token = Some(buffer); - } - Action::Store(token) => { - writeln!(child.stdin.as_ref().unwrap(), "{}", token).with_context(|| { - format!( - "failed to send token to registry credential process `{}`", - exe.display() - ) - })?; - } - Action::Erase => {} - } - let status = child.wait().with_context(|| { - format!( - "registry credential process `{}` exit failure", - exe.display() - ) - })?; - if !status.success() { - let msg = match action { - Action::Get => "failed to authenticate to registry", - Action::Store(_) => "failed to store token to registry", - Action::Erase => "failed to erase token from registry", - }; - return Err(ProcessError::new( - &format!( - "registry credential process `{}` {} `{}`", - exe.display(), - msg, - name - ), - Some(status), - None, - ) - .into()); - } - Ok(token) -} - -/// Gets the path to the libexec processes in the sysroot. -fn sysroot_credential( - config: &Config, - process: &(PathBuf, Vec), -) -> CargoResult<(PathBuf, Vec)> { - let cred_name = process.0.to_str().unwrap().strip_prefix("cargo:").unwrap(); - let cargo = config.cargo_exe()?; - let root = cargo - .parent() - .and_then(|p| p.parent()) - .ok_or_else(|| format_err!("expected cargo path {}", cargo.display()))?; - let exe = root.join("libexec").join(format!( - "cargo-credential-{}{}", - cred_name, - std::env::consts::EXE_SUFFIX - )); - let mut args = process.1.clone(); - if !args.iter().any(|arg| arg == "{action}") { - args.push("{action}".to_string()); - } - Ok((exe, args)) -} diff --git a/src/cargo/sources/registry/download.rs b/src/cargo/sources/registry/download.rs index cc39d7c1113..bde75b9da8d 100644 --- a/src/cargo/sources/registry/download.rs +++ b/src/cargo/sources/registry/download.rs @@ -8,6 +8,7 @@ use crate::sources::registry::{ RegistryConfig, CHECKSUM_TEMPLATE, CRATE_TEMPLATE, LOWER_PREFIX_TEMPLATE, PREFIX_TEMPLATE, VERSION_TEMPLATE, }; +use crate::util::auth; use crate::util::errors::CargoResult; use crate::util::{Config, Filesystem}; use std::fmt::Write as FmtWrite; @@ -69,9 +70,16 @@ pub(super) fn download( .replace(CHECKSUM_TEMPLATE, checksum); } + let authorization = if registry_config.auth_required { + Some(auth::auth_token(config, &pkg.source_id(), None)?) + } else { + None + }; + Ok(MaybeLock::Download { url, descriptor: pkg.to_string(), + authorization: authorization, }) } diff --git a/src/cargo/sources/registry/http_remote.rs b/src/cargo/sources/registry/http_remote.rs index 805942274c0..306b825aad7 100644 --- a/src/cargo/sources/registry/http_remote.rs +++ b/src/cargo/sources/registry/http_remote.rs @@ -3,13 +3,13 @@ //! See [`HttpRegistry`] for details. use crate::core::{PackageId, SourceId}; -use crate::ops; +use crate::ops::{self}; use crate::sources::registry::download; use crate::sources::registry::MaybeLock; use crate::sources::registry::{LoadResponse, RegistryConfig, RegistryData}; use crate::util::errors::{CargoResult, HttpNotSuccessful}; use crate::util::network::Retry; -use crate::util::{internal, Config, Filesystem, Progress, ProgressStyle}; +use crate::util::{auth, Config, Filesystem, IntoUrl, Progress, ProgressStyle}; use anyhow::Context; use cargo_util::paths; use curl::easy::{HttpVersion, List}; @@ -18,14 +18,20 @@ use log::{debug, trace}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fs::{self, File}; +use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::str; use std::task::{ready, Poll}; use std::time::Duration; use url::Url; -const ETAG: &'static str = "ETag"; -const LAST_MODIFIED: &'static str = "Last-Modified"; +// HTTP headers +const ETAG: &'static str = "etag"; +const LAST_MODIFIED: &'static str = "last-modified"; +const WWW_AUTHENTICATE: &'static str = "www-authenticate"; +const IF_NONE_MATCH: &'static str = "if-none-match"; +const IF_MODIFIED_SINCE: &'static str = "if-modified-since"; + const UNKNOWN: &'static str = "Unknown"; /// A registry served by the HTTP-based registry API. @@ -77,6 +83,12 @@ pub struct HttpRegistry<'cfg> { /// Cached registry configuration. registry_config: Option, + + /// Should we include the authorization header? + auth_required: bool, + + /// Url to get a token for the registry. + login_url: Option, } /// Helper for downloading crates. @@ -112,17 +124,31 @@ struct Download<'cfg> { /// Actual downloaded data, updated throughout the lifetime of this download. data: RefCell>, - /// ETag or Last-Modified header received from the server (if any). - index_version: RefCell>, + /// HTTP headers. + header_map: RefCell, /// Logic used to track retrying this download if it's a spurious failure. retry: Retry<'cfg>, } +#[derive(Default)] +struct Headers { + last_modified: Option, + etag: Option, + www_authenticate: Vec, +} + +enum StatusCode { + Success, + NotModified, + NotFound, + Unauthorized, +} + struct CompletedDownload { - response_code: u32, + response_code: StatusCode, data: Vec, - index_version: String, + header_map: Headers, } impl<'cfg> HttpRegistry<'cfg> { @@ -165,6 +191,8 @@ impl<'cfg> HttpRegistry<'cfg> { requested_update: false, fetch_started: false, registry_config: None, + auth_required: false, + login_url: None, }) } @@ -235,24 +263,27 @@ impl<'cfg> HttpRegistry<'cfg> { result.with_context(|| format!("failed to download from `{}`", url))?; let code = handle.response_code()?; // Keep this list of expected status codes in sync with the codes handled in `load` - if !matches!(code, 200 | 304 | 410 | 404 | 451) { - let url = handle.effective_url()?.unwrap_or(&url); - return Err(HttpNotSuccessful { - code, - url: url.to_owned(), - body: data, + let code = match code { + 200 => StatusCode::Success, + 304 => StatusCode::NotModified, + 401 => StatusCode::Unauthorized, + 404 | 410 | 451 => StatusCode::NotFound, + code => { + let url = handle.effective_url()?.unwrap_or(&url); + return Err(HttpNotSuccessful { + code, + url: url.to_owned(), + body: data, + } + .into()); } - .into()); - } - Ok(data) + }; + Ok((data, code)) }) { - Ok(Some(data)) => Ok(CompletedDownload { - response_code: handle.response_code()?, + Ok(Some((data, code))) => Ok(CompletedDownload { + response_code: code, data, - index_version: download - .index_version - .take() - .unwrap_or_else(|| UNKNOWN.to_string()), + header_map: download.header_map.take(), }), Ok(None) => { // retry the operation @@ -299,6 +330,69 @@ impl<'cfg> HttpRegistry<'cfg> { false } } + + fn check_registry_auth_unstable(&self) -> CargoResult<()> { + if self.auth_required && !self.config.cli_unstable().registry_auth { + anyhow::bail!("authenticated registries require `-Z registry-auth`"); + } + Ok(()) + } + + /// Get the cached registry configuration, if it exists. + fn config_cached(&mut self) -> CargoResult> { + if self.registry_config.is_some() { + return Ok(self.registry_config.as_ref()); + } + let config_json_path = self + .assert_index_locked(&self.index_path) + .join("config.json"); + match fs::read(&config_json_path) { + Ok(raw_data) => match serde_json::from_slice(&raw_data) { + Ok(json) => { + self.registry_config = Some(json); + } + Err(e) => log::debug!("failed to decode cached config.json: {}", e), + }, + Err(e) => { + if e.kind() != ErrorKind::NotFound { + log::debug!("failed to read config.json cache: {}", e) + } + } + } + Ok(self.registry_config.as_ref()) + } + + /// Get the registry configuration. + fn config(&mut self) -> Poll> { + debug!("loading config"); + let index_path = self.assert_index_locked(&self.index_path); + let config_json_path = index_path.join("config.json"); + if self.is_fresh(Path::new("config.json")) && self.config_cached()?.is_some() { + return Poll::Ready(Ok(self.registry_config.as_ref().unwrap())); + } + + match ready!(self.load(Path::new(""), Path::new("config.json"), None)?) { + LoadResponse::Data { + raw_data, + index_version: _, + } => { + trace!("config loaded"); + self.registry_config = Some(serde_json::from_slice(&raw_data)?); + if paths::create_dir_all(&config_json_path.parent().unwrap()).is_ok() { + if let Err(e) = fs::write(&config_json_path, &raw_data) { + log::debug!("failed to write config.json cache: {}", e); + } + } + Poll::Ready(Ok(self.registry_config.as_ref().unwrap())) + } + LoadResponse::NotFound => { + Poll::Ready(Err(anyhow::anyhow!("config.json not found in registry"))) + } + LoadResponse::CacheValid => Poll::Ready(Err(crate::util::internal( + "config.json is never stored in the index cache", + ))), + } + } } impl<'cfg> RegistryData for HttpRegistry<'cfg> { @@ -340,30 +434,42 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> { return Poll::Ready(Ok(LoadResponse::CacheValid)); } } else if self.fresh.contains(path) { + // We have no cached copy of this file, and we already downloaded it. debug!( "cache did not contain previously downloaded file {}", path.display() ); + return Poll::Ready(Ok(LoadResponse::NotFound)); } if let Some(result) = self.downloads.results.remove(path) { let result = result.with_context(|| format!("download of {} failed", path.display()))?; - debug!( - "index file downloaded with status code {}", - result.response_code - ); - trace!("index file version: {}", result.index_version); - if !self.fresh.insert(path.to_path_buf()) { - debug!("downloaded the index file `{}` twice", path.display()) - } + assert!( + self.fresh.insert(path.to_path_buf()), + "downloaded the index file `{}` twice", + path.display() + ); // The status handled here need to be kept in sync with the codes handled // in `handle_completed_downloads` match result.response_code { - 200 => {} - 304 => { + StatusCode::Success => { + let response_index_version = if let Some(etag) = result.header_map.etag { + format!("{}: {}", ETAG, etag) + } else if let Some(lm) = result.header_map.last_modified { + format!("{}: {}", LAST_MODIFIED, lm) + } else { + UNKNOWN.to_string() + }; + trace!("index file version: {}", response_index_version); + return Poll::Ready(Ok(LoadResponse::Data { + raw_data: result.data, + index_version: Some(response_index_version), + })); + } + StatusCode::NotModified => { // Not Modified: the data in the cache is still the latest. if index_version.is_none() { return Poll::Ready(Err(anyhow::anyhow!( @@ -372,29 +478,70 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> { } return Poll::Ready(Ok(LoadResponse::CacheValid)); } - 404 | 410 | 451 => { + StatusCode::NotFound => { // The crate was not found or deleted from the registry. return Poll::Ready(Ok(LoadResponse::NotFound)); } - code => { - return Err(internal(format!("unexpected HTTP status code {code}"))).into(); + StatusCode::Unauthorized + if !self.auth_required && path == Path::new("config.json") => + { + debug!("re-attempting request for config.json with authorization included."); + self.fresh.remove(path); + self.auth_required = true; + + // Look for a `www-authenticate` header with the `Cargo` scheme. + for header in &result.header_map.www_authenticate { + for challenge in http_auth::ChallengeParser::new(header) { + match challenge { + Ok(challenge) if challenge.scheme.eq_ignore_ascii_case("Cargo") => { + // Look for the `login_url` parameter. + for (param, value) in challenge.params { + if param.eq_ignore_ascii_case("login_url") { + self.login_url = Some(value.to_unescaped().into_url()?); + } + } + } + Ok(challenge) => { + debug!("ignoring non-Cargo challenge: {}", challenge.scheme) + } + Err(e) => debug!("failed to parse challenge: {}", e), + } + } + } + } + StatusCode::Unauthorized => { + let err = Err(HttpNotSuccessful { + code: 401, + body: result.data, + url: self.full_url(path), + } + .into()); + if self.auth_required { + return Poll::Ready(err.context(auth::AuthorizationError { + sid: self.source_id.clone(), + login_url: self.login_url.clone(), + reason: auth::AuthorizationErrorReason::TokenRejected, + })); + } else { + return Poll::Ready(err); + } } } + } - return Poll::Ready(Ok(LoadResponse::Data { - raw_data: result.data, - index_version: Some(result.index_version), - })); + if path != Path::new("config.json") { + self.auth_required = ready!(self.config()?).auth_required; + } else if !self.auth_required { + // Check if there's a cached config that says auth is required. + // This allows avoiding the initial unauthenticated request to probe. + if let Some(config) = self.config_cached()? { + self.auth_required = config.auth_required; + } } // Looks like we're going to have to do a network request. self.start_fetch()?; - // Load the registry config. - if self.registry_config.is_none() && path != Path::new("config.json") { - ready!(self.config()?); - } - let mut handle = ops::http_handle(self.config)?; let full_url = self.full_url(path); debug!("fetch {}", full_url); @@ -418,19 +565,31 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> { // reduces the number of connections done to a more manageable state. handle.pipewait(true)?; - // Make sure we don't send data back if it's the same as we have in the index. let mut headers = List::new(); + // Include a header to identify the protocol. This allows the server to + // know that Cargo is attempting to use the sparse protocol. + headers.append("cargo-protocol: version=1")?; + headers.append("accept: text/plain")?; + + // If we have a cached copy of the file, include IF_NONE_MATCH or IF_MODIFIED_SINCE header. if let Some(index_version) = index_version { if let Some((key, value)) = index_version.split_once(':') { match key { - ETAG => headers.append(&format!("If-None-Match: {}", value.trim()))?, + ETAG => headers.append(&format!("{}: {}", IF_NONE_MATCH, value.trim()))?, LAST_MODIFIED => { - headers.append(&format!("If-Modified-Since: {}", value.trim()))? + headers.append(&format!("{}: {}", IF_MODIFIED_SINCE, value.trim()))? } _ => debug!("unexpected index version: {}", index_version), } } } + if self.auth_required { + self.check_registry_auth_unstable()?; + let authorization = + auth::auth_token(self.config, &self.source_id, self.login_url.as_ref())?; + headers.append(&format!("Authorization: {}", authorization))?; + trace!("including authorization for {}", full_url); + } handle.http_headers(headers)?; // We're going to have a bunch of downloads all happening "at the same time". @@ -465,21 +624,17 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> { // And ditto for the header function. handle.header_function(move |buf| { if let Some((tag, value)) = Self::handle_http_header(buf) { - let is_etag = tag.eq_ignore_ascii_case(ETAG); - let is_lm = tag.eq_ignore_ascii_case(LAST_MODIFIED); - if is_etag || is_lm { - tls::with(|downloads| { - if let Some(downloads) = downloads { - let mut index_version = - downloads.pending[&token].0.index_version.borrow_mut(); - if is_etag { - *index_version = Some(format!("{}: {}", ETAG, value)); - } else if index_version.is_none() && is_lm { - *index_version = Some(format!("{}: {}", LAST_MODIFIED, value)); - }; + tls::with(|downloads| { + if let Some(downloads) = downloads { + let mut header_map = downloads.pending[&token].0.header_map.borrow_mut(); + match tag.to_ascii_lowercase().as_str() { + LAST_MODIFIED => header_map.last_modified = Some(value.to_string()), + ETAG => header_map.etag = Some(value.to_string()), + WWW_AUTHENTICATE => header_map.www_authenticate.push(value.to_string()), + _ => {} } - }) - } + } + }); } true @@ -487,9 +642,9 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> { let dl = Download { token, - data: RefCell::new(Vec::new()), path: path.to_path_buf(), - index_version: RefCell::new(None), + data: RefCell::new(Vec::new()), + header_map: Default::default(), retry: Retry::new(self.config)?, }; @@ -502,46 +657,9 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> { } fn config(&mut self) -> Poll>> { - if self.registry_config.is_some() { - return Poll::Ready(Ok(self.registry_config.clone())); - } - debug!("loading config"); - let index_path = self.config.assert_package_cache_locked(&self.index_path); - let config_json_path = index_path.join("config.json"); - if self.is_fresh(Path::new("config.json")) { - match fs::read(&config_json_path) { - Ok(raw_data) => match serde_json::from_slice(&raw_data) { - Ok(json) => { - self.registry_config = Some(json); - return Poll::Ready(Ok(self.registry_config.clone())); - } - Err(e) => log::debug!("failed to decode cached config.json: {}", e), - }, - Err(e) => log::debug!("failed to read config.json cache: {}", e), - } - } - - match ready!(self.load(Path::new(""), Path::new("config.json"), None)?) { - LoadResponse::Data { - raw_data, - index_version: _, - } => { - trace!("config loaded"); - self.registry_config = Some(serde_json::from_slice(&raw_data)?); - if paths::create_dir_all(&config_json_path.parent().unwrap()).is_ok() { - if let Err(e) = fs::write(&config_json_path, &raw_data) { - log::debug!("failed to write config.json cache: {}", e); - } - } - Poll::Ready(Ok(self.registry_config.clone())) - } - LoadResponse::NotFound => { - Poll::Ready(Err(anyhow::anyhow!("config.json not found in registry"))) - } - LoadResponse::CacheValid => { - panic!("config.json is not stored in the index cache") - } - } + let cfg = ready!(self.config()?).clone(); + self.check_registry_auth_unstable()?; + Poll::Ready(Ok(Some(cfg))) } fn invalidate_cache(&mut self) { @@ -557,7 +675,7 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> { let registry_config = loop { match self.config()? { Poll::Pending => self.block_until_ready()?, - Poll::Ready(cfg) => break cfg.unwrap(), + Poll::Ready(cfg) => break cfg.to_owned(), } }; download::download( diff --git a/src/cargo/sources/registry/mod.rs b/src/cargo/sources/registry/mod.rs index cabd4f9c603..8fd1a3b86c6 100644 --- a/src/cargo/sources/registry/mod.rs +++ b/src/cargo/sources/registry/mod.rs @@ -250,6 +250,10 @@ pub struct RegistryConfig { /// operations like yanks, owner modifications, publish new crates, etc. /// If this is None, the registry does not support API commands. pub api: Option, + + /// Whether all operations require authentication. + #[serde(default)] + pub auth_required: bool, } /// The maximum version of the `v` field in the index this version of cargo @@ -417,6 +421,7 @@ impl<'a> RegistryDependency<'a> { } } +/// Result from loading data from a registry. pub enum LoadResponse { /// The cache is valid. The cached data should be used. CacheValid, @@ -527,7 +532,11 @@ pub enum MaybeLock { /// /// `descriptor` is just a text string to display to the user of what is /// being downloaded. - Download { url: String, descriptor: String }, + Download { + url: String, + descriptor: String, + authorization: Option, + }, } mod download; @@ -548,6 +557,7 @@ impl<'cfg> RegistrySource<'cfg> { yanked_whitelist: &HashSet, config: &'cfg Config, ) -> CargoResult> { + assert!(source_id.is_remote_registry()); let name = short_name(source_id); let ops = if source_id.is_sparse() { Box::new(http_remote::HttpRegistry::new(source_id, config, &name)?) as Box<_> @@ -782,9 +792,15 @@ impl<'cfg> Source for RegistrySource<'cfg> { }; match self.ops.download(package, hash)? { MaybeLock::Ready(file) => self.get_pkg(package, &file).map(MaybePackage::Ready), - MaybeLock::Download { url, descriptor } => { - Ok(MaybePackage::Download { url, descriptor }) - } + MaybeLock::Download { + url, + descriptor, + authorization, + } => Ok(MaybePackage::Download { + url, + descriptor, + authorization, + }), } } diff --git a/src/cargo/sources/registry/remote.rs b/src/cargo/sources/registry/remote.rs index b9283b819e6..70aa8efd3f1 100644 --- a/src/cargo/sources/registry/remote.rs +++ b/src/cargo/sources/registry/remote.rs @@ -245,7 +245,13 @@ impl<'cfg> RegistryData for RemoteRegistry<'cfg> { match ready!(self.load(Path::new(""), Path::new("config.json"), None)?) { LoadResponse::Data { raw_data, .. } => { trace!("config loaded"); - Poll::Ready(Ok(Some(serde_json::from_slice(&raw_data)?))) + let cfg: RegistryConfig = serde_json::from_slice(&raw_data)?; + if cfg.auth_required && !self.config.cli_unstable().registry_auth { + return Poll::Ready(Err(anyhow::anyhow!( + "authenticated registries require `-Z registry-auth`" + ))); + } + Poll::Ready(Ok(Some(cfg))) } _ => Poll::Ready(Ok(None)), } diff --git a/src/cargo/util/auth.rs b/src/cargo/util/auth.rs new file mode 100644 index 00000000000..d67f874f132 --- /dev/null +++ b/src/cargo/util/auth.rs @@ -0,0 +1,489 @@ +//! Registry authentication support. + +use crate::util::{config, config::ConfigKey, CanonicalUrl, CargoResult, Config, IntoUrl}; +use anyhow::{bail, format_err, Context as _}; +use cargo_util::ProcessError; +use core::fmt; +use serde::Deserialize; +use std::collections::HashMap; +use std::error::Error; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use url::Url; + +use crate::core::SourceId; +use crate::ops::RegistryCredentialConfig; + +/// Get the credential configuration for a `SourceId`. +pub fn registry_credential_config( + config: &Config, + sid: &SourceId, +) -> CargoResult { + #[derive(Deserialize)] + #[serde(rename_all = "kebab-case")] + struct RegistryConfig { + index: Option, + token: Option, + credential_process: Option, + #[serde(rename = "default")] + _default: Option, + } + + log::trace!("loading credential config for {}", sid); + config.load_credentials()?; + if !sid.is_remote_registry() { + bail!( + "{} does not support API commands.\n\ + Check for a source-replacement in .cargo/config.", + sid + ); + } + + // Handle crates.io specially, since it uses different configuration keys. + if sid.is_crates_io() { + config.check_registry_index_not_set()?; + let RegistryConfig { + token, + credential_process, + .. + } = config.get::("registry")?; + let credential_process = + credential_process.filter(|_| config.cli_unstable().credential_process); + + return Ok(match (token, credential_process) { + (Some(_), Some(_)) => { + return Err(format_err!( + "both `token` and `credential-process` \ + were specified in the config`.\n\ + Only one of these values may be set, remove one or the other to proceed.", + )) + } + (Some(token), _) => RegistryCredentialConfig::Token(token), + (_, Some(process)) => RegistryCredentialConfig::Process(( + process.path.resolve_program(config), + process.args, + )), + (None, None) => RegistryCredentialConfig::None, + }); + } + + // Find the SourceId's name by its index URL. If environment variables + // are available they will be preferred over configuration values. + // + // The fundimental problem is that we only know the index url of the registry + // for certain. For example, an unnamed registry source can come from the `--index` + // command line argument, or from a Cargo.lock file. For this reason, we always + // attempt to discover the name by looking it up by the index URL. + // + // This also allows the authorization token for a registry to be set + // without knowing the registry name by using the _INDEX and _TOKEN + // environment variables. + let name = { + // Discover names from environment variables. + let index = sid.canonical_url(); + let mut names: Vec<_> = config + .env() + .iter() + .filter_map(|(k, v)| { + Some(( + k.strip_prefix("CARGO_REGISTRIES_")? + .strip_suffix("_INDEX")?, + v, + )) + }) + .filter_map(|(k, v)| Some((k, CanonicalUrl::new(&v.into_url().ok()?).ok()?))) + .filter(|(_, v)| v == index) + .map(|(k, _)| k.to_lowercase()) + .collect(); + + // Discover names from the configuration only if none were found in the environment. + if names.len() == 0 { + names = config + .get::>("registries")? + .iter() + .filter_map(|(k, v)| Some((k, v.index.as_deref()?))) + .filter_map(|(k, v)| Some((k, CanonicalUrl::new(&v.into_url().ok()?).ok()?))) + .filter(|(_, v)| v == index) + .map(|(k, _)| k.to_string()) + .collect(); + } + names.sort(); + match names.len() { + 0 => None, + 1 => Some(std::mem::take(&mut names[0])), + _ => anyhow::bail!( + "multiple registries are configured with the same index url '{}': {}", + &sid.as_url(), + names.join(", ") + ), + } + }; + + // It's possible to have a registry configured in a Cargo config file, + // then override it with configuration from environment variables. + // If the name doesn't match, leave a note to help the user understand + // the potentially confusing situation. + if let Some(name) = name.as_deref() { + if Some(name) != sid.alt_registry_key() { + config.shell().note(format!( + "name of alternative registry `{}` set to `{name}`", + sid.url() + ))? + } + } + + let (token, credential_process) = if let Some(name) = &name { + log::debug!("found alternative registry name `{name}` for {sid}"); + let RegistryConfig { + token, + credential_process, + .. + } = config.get::(&format!("registries.{name}"))?; + let credential_process = + credential_process.filter(|_| config.cli_unstable().credential_process); + (token, credential_process) + } else { + log::debug!("no registry name found for {sid}"); + (None, None) + }; + + let name = name.as_deref(); + Ok(match (token, credential_process) { + (Some(_), Some(_)) => { + return { + Err(format_err!( + "both `token` and `credential-process` \ + were specified in the config for registry `{name}`.\n\ + Only one of these values may be set, remove one or the other to proceed.", + name = name.unwrap() + )) + } + } + (Some(token), _) => RegistryCredentialConfig::Token(token), + (_, Some(process)) => { + RegistryCredentialConfig::Process((process.path.resolve_program(config), process.args)) + } + (None, None) => { + // If we couldn't find a registry-specific credential, try the global credential process. + if let Some(process) = config + .get::>("registry.credential-process")? + .filter(|_| config.cli_unstable().credential_process) + { + RegistryCredentialConfig::Process(( + process.path.resolve_program(config), + process.args, + )) + } else { + RegistryCredentialConfig::None + } + } + }) +} + +#[derive(Debug, PartialEq)] +pub enum AuthorizationErrorReason { + TokenMissing, + TokenRejected, +} + +impl fmt::Display for AuthorizationErrorReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AuthorizationErrorReason::TokenMissing => write!(f, "no token found"), + AuthorizationErrorReason::TokenRejected => write!(f, "token rejected"), + } + } +} + +/// An authorization error from accessing a registry. +#[derive(Debug)] +pub struct AuthorizationError { + /// Url that was attempted + pub sid: SourceId, + /// Url where the user could log in. + pub login_url: Option, + /// Specific reason indicating what failed + pub reason: AuthorizationErrorReason, +} +impl Error for AuthorizationError {} +impl fmt::Display for AuthorizationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.sid.is_crates_io() { + write!( + f, + "{}, please run `cargo login`\nor use environment variable CARGO_REGISTRY_TOKEN", + self.reason + ) + } else if let Some(name) = self.sid.alt_registry_key() { + let key = ConfigKey::from_str(&format!("registries.{name}.token")); + write!( + f, + "{} for `{}`, please run `cargo login --registry {name}`\nor use environment variable {}", + self.reason, + self.sid.display_registry_name(), + key.as_env_key(), + ) + } else if self.reason == AuthorizationErrorReason::TokenMissing { + write!( + f, + r#"{} for `{}` +consider setting up an alternate registry in Cargo's configuration +as described by https://doc.rust-lang.org/cargo/reference/registries.html + +[registries] +my-registry = {{ index = "{}" }} +"#, + self.reason, + self.sid.display_registry_name(), + self.sid.url() + ) + } else { + write!( + f, + r#"{} for `{}`"#, + self.reason, + self.sid.display_registry_name(), + ) + } + } +} + +// Store a token in the cache for future calls. +pub fn cache_token(config: &Config, sid: &SourceId, token: &str) { + let url = sid.canonical_url(); + config + .credential_cache() + .insert(url.clone(), token.to_string()); +} + +/// Returns the token to use for the given registry. +/// If a `login_url` is provided and a token is not available, the +/// login_url will be included in the returned error. +pub fn auth_token(config: &Config, sid: &SourceId, login_url: Option<&Url>) -> CargoResult { + match auth_token_optional(config, sid)? { + Some(token) => Ok(token), + None => Err(AuthorizationError { + sid: sid.clone(), + login_url: login_url.cloned(), + reason: AuthorizationErrorReason::TokenMissing, + } + .into()), + } +} + +/// Returns the token to use for the given registry. +fn auth_token_optional(config: &Config, sid: &SourceId) -> CargoResult> { + let mut cache = config.credential_cache(); + let url = sid.canonical_url(); + + if let Some(token) = cache.get(url) { + return Ok(Some(token.clone())); + } + + let credential = registry_credential_config(config, sid)?; + let token = match credential { + RegistryCredentialConfig::None => return Ok(None), + RegistryCredentialConfig::Token(config_token) => config_token.to_string(), + RegistryCredentialConfig::Process(process) => { + run_command(config, &process, sid, Action::Get)?.unwrap() + } + }; + + cache.insert(url.clone(), token.clone()); + Ok(Some(token)) +} + +enum Action { + Get, + Store(String), + Erase, +} + +/// Saves the given token. +pub fn login(config: &Config, sid: &SourceId, token: String) -> CargoResult<()> { + match registry_credential_config(config, sid)? { + RegistryCredentialConfig::Process(process) => { + run_command(config, &process, sid, Action::Store(token))?; + } + _ => { + config::save_credentials(config, Some(token), &sid)?; + } + }; + Ok(()) +} + +/// Removes the token for the given registry. +pub fn logout(config: &Config, sid: &SourceId) -> CargoResult<()> { + match registry_credential_config(config, sid)? { + RegistryCredentialConfig::Process(process) => { + run_command(config, &process, sid, Action::Erase)?; + } + _ => { + config::save_credentials(config, None, &sid)?; + } + }; + Ok(()) +} + +fn run_command( + config: &Config, + process: &(PathBuf, Vec), + sid: &SourceId, + action: Action, +) -> CargoResult> { + let index_url = sid.url().as_str(); + let cred_proc; + let (exe, args) = if process.0.to_str().unwrap_or("").starts_with("cargo:") { + cred_proc = sysroot_credential(config, process)?; + &cred_proc + } else { + process + }; + if !args.iter().any(|arg| arg.contains("{action}")) { + let msg = |which| { + format!( + "credential process `{}` cannot be used to {}, \ + the credential-process configuration value must pass the \ + `{{action}}` argument in the config to support this command", + exe.display(), + which + ) + }; + match action { + Action::Get => {} + Action::Store(_) => bail!(msg("log in")), + Action::Erase => bail!(msg("log out")), + } + } + let action_str = match action { + Action::Get => "get", + Action::Store(_) => "store", + Action::Erase => "erase", + }; + let args: Vec<_> = args + .iter() + .map(|arg| { + arg.replace("{action}", action_str) + .replace("{index_url}", index_url) + }) + .collect(); + + let mut cmd = Command::new(&exe); + cmd.args(args) + .env(crate::CARGO_ENV, config.cargo_exe()?) + .env("CARGO_REGISTRY_INDEX_URL", index_url); + if sid.is_crates_io() { + cmd.env("CARGO_REGISTRY_NAME_OPT", "crates-io"); + } else if let Some(name) = sid.alt_registry_key() { + cmd.env("CARGO_REGISTRY_NAME_OPT", name); + } + match action { + Action::Get => { + cmd.stdout(Stdio::piped()); + } + Action::Store(_) => { + cmd.stdin(Stdio::piped()); + } + Action::Erase => {} + } + let mut child = cmd.spawn().with_context(|| { + let verb = match action { + Action::Get => "fetch", + Action::Store(_) => "store", + Action::Erase => "erase", + }; + format!( + "failed to execute `{}` to {} authentication token for registry `{}`", + exe.display(), + verb, + sid.display_registry_name(), + ) + })?; + let mut token = None; + match &action { + Action::Get => { + let mut buffer = String::new(); + log::debug!("reading into buffer"); + child + .stdout + .as_mut() + .unwrap() + .read_to_string(&mut buffer) + .with_context(|| { + format!( + "failed to read token from registry credential process `{}`", + exe.display() + ) + })?; + if let Some(end) = buffer.find('\n') { + if buffer.len() > end + 1 { + bail!( + "credential process `{}` returned more than one line of output; \ + expected a single token", + exe.display() + ); + } + buffer.truncate(end); + } + token = Some(buffer); + } + Action::Store(token) => { + writeln!(child.stdin.as_ref().unwrap(), "{}", token).with_context(|| { + format!( + "failed to send token to registry credential process `{}`", + exe.display() + ) + })?; + } + Action::Erase => {} + } + let status = child.wait().with_context(|| { + format!( + "registry credential process `{}` exit failure", + exe.display() + ) + })?; + if !status.success() { + let msg = match action { + Action::Get => "failed to authenticate to registry", + Action::Store(_) => "failed to store token to registry", + Action::Erase => "failed to erase token from registry", + }; + return Err(ProcessError::new( + &format!( + "registry credential process `{}` {} `{}`", + exe.display(), + msg, + sid.display_registry_name() + ), + Some(status), + None, + ) + .into()); + } + Ok(token) +} + +/// Gets the path to the libexec processes in the sysroot. +fn sysroot_credential( + config: &Config, + process: &(PathBuf, Vec), +) -> CargoResult<(PathBuf, Vec)> { + let cred_name = process.0.to_str().unwrap().strip_prefix("cargo:").unwrap(); + let cargo = config.cargo_exe()?; + let root = cargo + .parent() + .and_then(|p| p.parent()) + .ok_or_else(|| format_err!("expected cargo path {}", cargo.display()))?; + let exe = root.join("libexec").join(format!( + "cargo-credential-{}{}", + cred_name, + std::env::consts::EXE_SUFFIX + )); + let mut args = process.1.clone(); + if !args.iter().any(|arg| arg == "{action}") { + args.push("{action}".to_string()); + } + Ok((exe, args)) +} diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index d2fa0b5caf9..5743f9baf3f 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -71,8 +71,9 @@ use crate::core::shell::Verbosity; use crate::core::{features, CliUnstable, Shell, SourceId, Workspace, WorkspaceRootConfig}; use crate::ops; use crate::util::errors::CargoResult; -use crate::util::toml as cargo_toml; use crate::util::validate_package_name; +use crate::util::CanonicalUrl; +use crate::util::{internal, toml as cargo_toml}; use crate::util::{FileLock, Filesystem, IntoUrl, IntoUrlWithBase, Rustc}; use anyhow::{anyhow, bail, format_err, Context as _}; use cargo_util::paths; @@ -145,6 +146,8 @@ pub struct Config { shell: RefCell, /// A collection of configuration options values: LazyCell>, + /// A collection of configuration options from the credentials file + credential_values: LazyCell>, /// CLI config values, passed in via `configure`. cli_config: Option>, /// The current working directory of cargo @@ -188,6 +191,9 @@ pub struct Config { upper_case_env: HashMap, /// Tracks which sources have been updated to avoid multiple updates. updated_sources: LazyCell>>, + /// Cache of credentials from configuration or credential providers. + /// Maps from url to credential value. + credential_cache: LazyCell>>, /// Lock, if held, of the global package cache along with the number of /// acquisitions so far. package_cache_lock: RefCell, usize)>>, @@ -267,6 +273,7 @@ impl Config { cwd, search_stop_path: None, values: LazyCell::new(), + credential_values: LazyCell::new(), cli_config: None, cargo_exe: LazyCell::new(), rustdoc: LazyCell::new(), @@ -291,6 +298,7 @@ impl Config { env, upper_case_env, updated_sources: LazyCell::new(), + credential_cache: LazyCell::new(), package_cache_lock: RefCell::new(None), http_config: LazyCell::new(), future_incompat_config: LazyCell::new(), @@ -459,6 +467,13 @@ impl Config { .borrow_mut() } + /// Cached credentials from credential providers or configuration. + pub fn credential_cache(&self) -> RefMut<'_, HashMap> { + self.credential_cache + .borrow_with(|| RefCell::new(HashMap::new())) + .borrow_mut() + } + /// Gets all config values from disk. /// /// This will lazy-load the values as necessary. Callers are responsible @@ -475,10 +490,11 @@ impl Config { /// entries. This doesn't respect environment variables. You should avoid /// using this if possible. pub fn values_mut(&mut self) -> CargoResult<&mut HashMap> { - match self.values.borrow_mut() { - Some(map) => Ok(map), - None => bail!("config values not loaded yet"), - } + let _ = self.values()?; + Ok(self + .values + .borrow_mut() + .expect("already loaded config values")) } // Note: this is used by RLS, not Cargo. @@ -555,8 +571,21 @@ impl Config { /// This does NOT look at environment variables. See `get_cv_with_env` for /// a variant that supports environment variables. fn get_cv(&self, key: &ConfigKey) -> CargoResult> { + if let Some(vals) = self.credential_values.borrow() { + let val = self.get_cv_helper(key, vals)?; + if val.is_some() { + return Ok(val); + } + } + self.get_cv_helper(key, self.values()?) + } + + fn get_cv_helper( + &self, + key: &ConfigKey, + vals: &HashMap, + ) -> CargoResult> { log::trace!("get cv {:?}", key); - let vals = self.values()?; if key.is_root() { // Returning the entire root table (for example `cargo config get` // with no key). The definition here shouldn't matter. @@ -1350,8 +1379,6 @@ impl Config { CV::Table(table, _def) => table, _ => unreachable!(), }; - // Force values to be loaded. - let _ = self.values()?; let values = self.values_mut()?; for (key, value) in loaded_map.into_iter() { match values.entry(key) { @@ -1482,7 +1509,17 @@ impl Config { } /// Loads credentials config from the credentials file, if present. - pub fn load_credentials(&mut self) -> CargoResult<()> { + /// + /// The credentials are loaded into a separate field to enable them + /// to be lazy-loaded after the main configuration has been loaded, + /// without requiring `mut` access to the `Config`. + /// + /// If the credentials are already loaded, this function does nothing. + pub fn load_credentials(&self) -> CargoResult<()> { + if self.credential_values.filled() { + return Ok(()); + } + let home_path = self.home_path.clone().into_path_unlocked(); let credentials = match self.get_file_path(&home_path, "credentials", true)? { Some(credentials) => credentials, @@ -1506,20 +1543,24 @@ impl Config { } } + let mut credential_values = HashMap::new(); if let CV::Table(map, _) = value { - let base_map = self.values_mut()?; + let base_map = self.values()?; for (k, v) in map { - match base_map.entry(k) { - Vacant(entry) => { - entry.insert(v); - } - Occupied(mut entry) => { - entry.get_mut().merge(v, true)?; + let entry = match base_map.get(&k) { + Some(base_entry) => { + let mut entry = base_entry.clone(); + entry.merge(v, true)?; + entry } - } + None => v, + }; + credential_values.insert(k, entry); } } - + self.credential_values + .fill(credential_values) + .expect("was not filled at beginning of the function"); Ok(()) } @@ -2041,8 +2082,17 @@ pub fn homedir(cwd: &Path) -> Option { pub fn save_credentials( cfg: &Config, token: Option, - registry: Option<&str>, + registry: &SourceId, ) -> CargoResult<()> { + let registry = if registry.is_crates_io() { + None + } else { + let name = registry + .alt_registry_key() + .ok_or_else(|| internal("can't save credentials for anonymous registry"))?; + Some(name) + }; + // If 'credentials.toml' exists, we should write to that, otherwise // use the legacy 'credentials'. There's no need to print the warning // here, because it would already be printed at load time. diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index 8fb24368856..ccd6d59a4d1 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -29,6 +29,7 @@ pub use self::workspace::{ print_available_examples, print_available_packages, print_available_tests, }; +pub mod auth; mod canonical_url; pub mod command_prelude; pub mod config; diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 13cb61cff47..b34a8f0c0ca 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -100,6 +100,7 @@ Each new feature described below should explain how to use it. * [`cargo logout`](#cargo-logout) — Adds the `logout` command to remove the currently saved registry token. * [sparse-registry](#sparse-registry) — Adds support for fetching from static-file HTTP registries (`sparse+`) * [publish-timeout](#publish-timeout) — Controls the timeout between uploading the crate and being available in the index + * [registry-auth](#registry-auth) — Adds support for authenticated registries. ### allow-features @@ -847,7 +848,13 @@ for crates.io. This option requires `-Z sparse-registry` to be enabled. * `sparse` — Use sparse index. * `git` — Use git index. -* If the option is unset, it will be sparse index if `-Z sparse-registry` is enabled, otherwise it will be git index. +* If the option is unset, it will be sparse index if `-Z sparse-registry` is enabled, + otherwise it will be git index. + +Cargo locally caches the crate metadata files, and captures an `ETag` or `Last-Modified` +HTTP header from the server for each entry. When refreshing crate metadata, Cargo will +send the `If-None-Match` or `If-Modified-Since` header to allow the server to respond +with HTTP 304 if the local cache is valid, saving time and bandwidth. ### publish-timeout * Tracking Issue: [11222](https://github.com/rust-lang/cargo/issues/11222) @@ -866,6 +873,30 @@ It requires the `-Zpublish-timeout` command-line options to be set. timeout = 300 # in seconds ``` +### registry-auth +* Tracking Issue: [10474](https://github.com/rust-lang/cargo/issues/10474) +* RFC: [#3139](https://github.com/rust-lang/rfcs/pull/3139) + +Enables Cargo to include the authorization token for API requests, crate downloads +and sparse index updates by adding a configuration option to config.json +in the registry index. + +To use this feature, the registry server must include `"auth-required": true` in +`config.json`, and you must pass the `-Z registry-auth` flag on the Cargo command line. + +When using the sparse protocol, Cargo will attempt to fetch the `config.json` file before +fetching any other files. If the server responds with an HTTP 401, then Cargo will assume +that the registry requires authentication and re-attempt the request for `config.json` +with the authentication token included. + +On authentication failure (or missing authentication token) the server MAY include a +`WWW-Authenticate` header with a `Cargo login_url` challenge to indicate where the user +can go to get a token. + +``` +WWW-Authenticate: Cargo login_url="https://test-registry-login/me +``` + ### credential-process * Tracking Issue: [#8933](https://github.com/rust-lang/cargo/issues/8933) * RFC: [#2730](https://github.com/rust-lang/rfcs/pull/2730) @@ -1010,8 +1041,8 @@ interactions are: The following environment variables will be provided to the executed command: * `CARGO` — Path to the `cargo` binary executing the command. -* `CARGO_REGISTRY_NAME` — Name of the registry the authentication token is for. -* `CARGO_REGISTRY_API_URL` — The URL of the registry API. +* `CARGO_REGISTRY_INDEX_URL` — The URL of the registry index. +* `CARGO_REGISTRY_NAME_OPT` — Optional name of the registry. Should not be used as a storage key. Not always available. #### `cargo logout` diff --git a/tests/testsuite/alt_registry.rs b/tests/testsuite/alt_registry.rs index e041a9a7a74..284cdd1de57 100644 --- a/tests/testsuite/alt_registry.rs +++ b/tests/testsuite/alt_registry.rs @@ -404,9 +404,11 @@ fn block_publish_due_to_no_token() { // Now perform the actual publish p.cargo("publish --registry alternative") .with_status(101) - .with_stderr_contains( - "error: no upload token found, \ - please run `cargo login` or pass `--token`", + .with_stderr( + "\ +[UPDATING] `alternative` index +error: no token found for `alternative`, please run `cargo login --registry alternative` +or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN", ) .run(); } diff --git a/tests/testsuite/credential_process.rs b/tests/testsuite/credential_process.rs index 0739e8e2bff..566508c8639 100644 --- a/tests/testsuite/credential_process.rs +++ b/tests/testsuite/credential_process.rs @@ -38,7 +38,8 @@ fn gated() { .with_stderr( "\ [UPDATING] [..] -[ERROR] no upload token found, please run `cargo login` or pass `--token` +[ERROR] no token found, please run `cargo login` +or use environment variable CARGO_REGISTRY_TOKEN ", ) .run(); @@ -57,7 +58,8 @@ fn gated() { .with_stderr( "\ [UPDATING] [..] -[ERROR] no upload token found, please run `cargo login` or pass `--token` +[ERROR] no token found for `alternative`, please run `cargo login --registry alternative` +or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN ", ) .run(); @@ -109,8 +111,8 @@ fn warn_both_token_and_process() { .with_status(101) .with_stderr( "\ -[ERROR] both `registries.alternative.token` and `registries.alternative.credential-process` \ -were specified in the config\n\ +[UPDATING] [..] +[ERROR] both `token` and `credential-process` were specified in the config for registry `alternative`. Only one of these values may be set, remove one or the other to proceed. ", ) @@ -238,7 +240,7 @@ fn basic_unsupported() { .with_status(101) .with_stderr( "\ -[UPDATING] [..] +[UPDATING] crates.io index [ERROR] credential process `false` cannot be used to log in, \ the credential-process configuration value must pass the \ `{action}` argument in the config to support this command @@ -271,20 +273,19 @@ fn login() { .file("Cargo.toml", &basic_manifest("test-cred", "1.0.0")) .file( "src/main.rs", - &r#" + r#" use std::io::Read; - fn main() { - assert_eq!(std::env::var("CARGO_REGISTRY_NAME").unwrap(), "crates-io"); - assert_eq!(std::env::var("CARGO_REGISTRY_API_URL").unwrap(), "__API__"); + fn main() {{ + assert_eq!(std::env::var("CARGO_REGISTRY_NAME_OPT").unwrap(), "crates-io"); + assert_eq!(std::env::var("CARGO_REGISTRY_INDEX_URL").unwrap(), "https://github.com/rust-lang/crates.io-index"); assert_eq!(std::env::args().skip(1).next().unwrap(), "store"); let mut buffer = String::new(); std::io::stdin().read_to_string(&mut buffer).unwrap(); assert_eq!(buffer, "abcdefg\n"); std::fs::write("token-store", buffer).unwrap(); - } - "# - .replace("__API__", server.api_url().as_str()), + }} + "#, ) .build(); cred_proj.cargo("build").run(); @@ -329,16 +330,16 @@ fn logout() { .file("Cargo.toml", &basic_manifest("test-cred", "1.0.0")) .file( "src/main.rs", - r#" + r#" use std::io::Read; - fn main() { - assert_eq!(std::env::var("CARGO_REGISTRY_NAME").unwrap(), "crates-io"); + fn main() {{ + assert_eq!(std::env::var("CARGO_REGISTRY_NAME_OPT").unwrap(), "crates-io"); + assert_eq!(std::env::var("CARGO_REGISTRY_INDEX_URL").unwrap(), "https://github.com/rust-lang/crates.io-index"); assert_eq!(std::env::args().skip(1).next().unwrap(), "erase"); std::fs::write("token-store", "").unwrap(); - eprintln!("token for `{}` has been erased!", - std::env::var("CARGO_REGISTRY_NAME").unwrap()); - } + eprintln!("token for `crates-io` has been erased!") + }} "#, ) .build(); @@ -362,9 +363,8 @@ fn logout() { .replace_crates_io(server.index_url()) .with_stderr( "\ -[UPDATING] [..] token for `crates-io` has been erased! -[LOGOUT] token for `crates.io` has been removed from local storage +[LOGOUT] token for `crates-io` has been removed from local storage ", ) .run(); diff --git a/tests/testsuite/login.rs b/tests/testsuite/login.rs index b1c5d00db38..b645e8bf691 100644 --- a/tests/testsuite/login.rs +++ b/tests/testsuite/login.rs @@ -101,7 +101,7 @@ fn empty_login_token() { cargo_process("login") .replace_crates_io(registry.index_url()) - .with_stdout("please paste the API Token found on [..]/me below") + .with_stdout("please paste the token found on [..]/me below") .with_stdin("\t\n") .with_stderr( "\ diff --git a/tests/testsuite/logout.rs b/tests/testsuite/logout.rs index ce9e29bd0c8..db15657edd8 100644 --- a/tests/testsuite/logout.rs +++ b/tests/testsuite/logout.rs @@ -46,14 +46,16 @@ fn check_config_token(registry: Option<&str>, should_be_set: bool) { } fn simple_logout_test(registry: &TestRegistry, reg: Option<&str>, flag: &str) { - let msg = reg.unwrap_or("crates.io"); + let msg = reg.unwrap_or("crates-io"); check_config_token(reg, true); - cargo_process(&format!("logout -Z unstable-options {}", flag)) + let mut cargo = cargo_process(&format!("logout -Z unstable-options {}", flag)); + if reg.is_none() { + cargo.replace_crates_io(registry.index_url()); + } + cargo .masquerade_as_nightly_cargo(&["cargo-logout"]) - .replace_crates_io(registry.index_url()) .with_stderr(&format!( "\ -[UPDATING] [..] [LOGOUT] token for `{}` has been removed from local storage ", msg @@ -61,9 +63,12 @@ fn simple_logout_test(registry: &TestRegistry, reg: Option<&str>, flag: &str) { .run(); check_config_token(reg, false); - cargo_process(&format!("logout -Z unstable-options {}", flag)) + let mut cargo = cargo_process(&format!("logout -Z unstable-options {}", flag)); + if reg.is_none() { + cargo.replace_crates_io(registry.index_url()); + } + cargo .masquerade_as_nightly_cargo(&["cargo-logout"]) - .replace_crates_io(registry.index_url()) .with_stderr(&format!( "\ [LOGOUT] not currently logged in to `{}` diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index bdbcc33d0fa..b1f0121beb6 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -105,6 +105,7 @@ mod publish; mod publish_lockfile; mod read_manifest; mod registry; +mod registry_auth; mod rename_deps; mod replace; mod required_features; diff --git a/tests/testsuite/publish.rs b/tests/testsuite/publish.rs index aed1aa396a4..91fe0c3b621 100644 --- a/tests/testsuite/publish.rs +++ b/tests/testsuite/publish.rs @@ -162,8 +162,8 @@ fn old_token_location() { .replace_crates_io(registry.index_url()) .with_status(101) .with_stderr_contains( - "[ERROR] no upload token found, \ - please run `cargo login` or pass `--token`", + "[ERROR] no token found, \ + please run `cargo login`", ) .run(); @@ -1214,10 +1214,7 @@ fn publish_checks_for_token_before_verify() { p.cargo("publish") .replace_crates_io(registry.index_url()) .with_status(101) - .with_stderr_contains( - "[ERROR] no upload token found, \ - please run `cargo login` or pass `--token`", - ) + .with_stderr_contains("[ERROR] no token found, please run `cargo login`") .with_stderr_does_not_contain("[VERIFYING] foo v0.0.1 ([CWD])") .run(); @@ -1562,8 +1559,8 @@ fn credentials_ambiguous_filename() { .replace_crates_io(registry.index_url()) .with_stderr( "\ -[WARNING] Both `[..]/credentials` and `[..]/credentials.toml` exist. Using `[..]/credentials` [..] +[WARNING] Both `[..]/credentials` and `[..]/credentials.toml` exist. Using `[..]/credentials` [..] [..] [..] @@ -1605,7 +1602,6 @@ fn index_requires_token() { .with_status(101) .with_stderr( "\ -[UPDATING] [..] [ERROR] command-line argument --index requires --token to be specified ", ) @@ -2410,7 +2406,6 @@ fn wait_for_first_publish() { .add_responder("/index/de/la/delay", move |req, server| { let mut lock = arc.lock().unwrap(); *lock += 1; - // if the package name contains _ or - if *lock <= 1 { server.not_found(req) } else { @@ -2495,8 +2490,7 @@ fn wait_for_first_publish_underscore() { .add_responder("/index/de/la/delay_with_underscore", move |req, server| { let mut lock = arc.lock().unwrap(); *lock += 1; - // package names with - or _ hit the responder twice per cargo invocation - if *lock <= 2 { + if *lock <= 1 { server.not_found(req) } else { server.index(req) @@ -2540,8 +2534,7 @@ See [..] // Verify the repsponder has been pinged let lock = arc2.lock().unwrap(); - // NOTE: package names with - or _ hit the responder twice per cargo invocation - assert_eq!(*lock, 3); + assert_eq!(*lock, 2); drop(lock); let p = project() diff --git a/tests/testsuite/registry.rs b/tests/testsuite/registry.rs index 5311c0b8cd7..fe59c4dd69f 100644 --- a/tests/testsuite/registry.rs +++ b/tests/testsuite/registry.rs @@ -28,6 +28,12 @@ fn setup_http() -> TestRegistry { RegistryBuilder::new().http_index().build() } +#[cargo_test] +fn test_server_stops() { + let server = setup_http(); + server.join(); // ensure the server fully shuts down +} + #[cargo_test] fn simple_http() { let _server = setup_http(); @@ -1114,7 +1120,7 @@ fn login_with_token_on_stdin() { .run(); cargo_process("login") .replace_crates_io(registry.index_url()) - .with_stdout("please paste the API Token found on [..]/me below") + .with_stdout("please paste the token found on [..]/me below") .with_stdin("some token") .run(); let credentials = fs::read_to_string(&credentials).unwrap(); diff --git a/tests/testsuite/registry_auth.rs b/tests/testsuite/registry_auth.rs new file mode 100644 index 00000000000..3aa42e8f6a3 --- /dev/null +++ b/tests/testsuite/registry_auth.rs @@ -0,0 +1,304 @@ +//! Tests for normal registry dependencies. + +use cargo_test_support::registry::{Package, RegistryBuilder}; +use cargo_test_support::{project, Execs, Project}; + +fn cargo(p: &Project, s: &str) -> Execs { + let mut e = p.cargo(s); + e.masquerade_as_nightly_cargo(&["sparse-registry", "registry-auth"]) + .arg("-Zsparse-registry") + .arg("-Zregistry-auth"); + e +} + +fn make_project() -> Project { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies.bar] + version = "0.0.1" + registry = "alternative" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + Package::new("bar", "0.0.1").alternative(true).publish(); + p +} + +static SUCCCESS_OUTPUT: &'static str = "\ +[UPDATING] `alternative` index +[DOWNLOADING] crates ... +[DOWNLOADED] bar v0.0.1 (registry `alternative`) +[COMPILING] bar v0.0.1 (registry `alternative`) +[COMPILING] foo v0.0.1 ([CWD]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +"; + +#[cargo_test] +fn requires_nightly() { + let _registry = RegistryBuilder::new().alternative().auth_required().build(); + + let p = make_project(); + p.cargo("build") + .with_status(101) + .with_stderr_contains(" authenticated registries require `-Z registry-auth`") + .run(); +} + +#[cargo_test] +fn simple() { + let _registry = RegistryBuilder::new() + .alternative() + .auth_required() + .http_index() + .build(); + + let p = make_project(); + cargo(&p, "build").with_stderr(SUCCCESS_OUTPUT).run(); +} + +#[cargo_test] +fn environment_config() { + let registry = RegistryBuilder::new() + .alternative() + .auth_required() + .no_configure_registry() + .no_configure_token() + .http_index() + .build(); + let p = make_project(); + cargo(&p, "build") + .env( + "CARGO_REGISTRIES_ALTERNATIVE_INDEX", + registry.index_url().as_str(), + ) + .env("CARGO_REGISTRIES_ALTERNATIVE_TOKEN", registry.token()) + .with_stderr(SUCCCESS_OUTPUT) + .run(); +} + +#[cargo_test] +fn environment_token() { + let registry = RegistryBuilder::new() + .alternative() + .auth_required() + .no_configure_token() + .http_index() + .build(); + + let p = make_project(); + cargo(&p, "build") + .env("CARGO_REGISTRIES_ALTERNATIVE_TOKEN", registry.token()) + .with_stderr(SUCCCESS_OUTPUT) + .run(); +} + +#[cargo_test] +fn missing_token() { + let _registry = RegistryBuilder::new() + .alternative() + .auth_required() + .no_configure_token() + .http_index() + .build(); + + let p = make_project(); + cargo(&p, "build") + .with_status(101) + .with_stderr( + "\ +[UPDATING] `alternative` index +[ERROR] failed to get `bar` as a dependency of package `foo v0.0.1 ([..])` + +Caused by: + no token found for `alternative`, please run `cargo login --registry alternative` + or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN", + ) + .run(); +} + +#[cargo_test] +fn missing_token_git() { + let _registry = RegistryBuilder::new() + .alternative() + .auth_required() + .no_configure_token() + .build(); + + let p = make_project(); + cargo(&p, "build") + .with_status(101) + .with_stderr( + "\ +[UPDATING] `alternative` index +[ERROR] failed to download `bar v0.0.1 (registry `alternative`)` + +Caused by: + unable to get packages from source + +Caused by: + no token found for `alternative`, please run `cargo login --registry alternative` + or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN", + ) + .run(); +} + +#[cargo_test] +fn incorrect_token() { + let _registry = RegistryBuilder::new() + .alternative() + .auth_required() + .no_configure_token() + .http_index() + .build(); + + let p = make_project(); + cargo(&p, "build") + .env("CARGO_REGISTRIES_ALTERNATIVE_TOKEN", "incorrect") + .with_status(101) + .with_stderr( + "\ +[UPDATING] `alternative` index +[ERROR] failed to get `bar` as a dependency of package `foo v0.0.1 ([..])` + +Caused by: + token rejected for `alternative`, please run `cargo login --registry alternative` + or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN + +Caused by: + failed to get successful HTTP response from `http://[..]/index/config.json`, got 401 + body: + Unauthorized message from server.", + ) + .run(); +} + +#[cargo_test] +fn incorrect_token_git() { + let _registry = RegistryBuilder::new() + .alternative() + .auth_required() + .no_configure_token() + .http_api() + .build(); + + let p = make_project(); + cargo(&p, "build") + .env("CARGO_REGISTRIES_ALTERNATIVE_TOKEN", "incorrect") + .with_status(101) + .with_stderr( + "\ +[UPDATING] `alternative` index +[DOWNLOADING] crates ... +[ERROR] failed to download from `http://[..]/dl/bar/0.0.1/download` + +Caused by: + failed to get successful HTTP response from `http://[..]/dl/bar/0.0.1/download`, got 401 + body: + Unauthorized message from server.", + ) + .run(); +} + +#[cargo_test] +fn anonymous_alt_registry() { + // An alternative registry that requires auth, but is not in the config. + let registry = RegistryBuilder::new() + .alternative() + .auth_required() + .no_configure_token() + .no_configure_registry() + .http_index() + .build(); + + let p = make_project(); + cargo(&p, &format!("install --index {} bar", registry.index_url())) + .with_status(101) + .with_stderr( + "\ +[UPDATING] `[..]` index +[ERROR] no token found for `[..]` +consider setting up an alternate registry in Cargo's configuration +as described by https://doc.rust-lang.org/cargo/reference/registries.html + +[registries] +my-registry = { index = \"[..]\" } + +", + ) + .run(); +} + +#[cargo_test] +fn login() { + let _registry = RegistryBuilder::new() + .alternative() + .no_configure_token() + .auth_required() + .http_index() + .build(); + + let p = make_project(); + cargo(&p, "login --registry alternative") + .with_stdout("please paste the token found on https://test-registry-login/me below") + .with_stdin("sekrit") + .run(); +} + +#[cargo_test] +fn login_existing_token() { + let _registry = RegistryBuilder::new() + .alternative() + .auth_required() + .http_index() + .build(); + + let p = make_project(); + cargo(&p, "login --registry alternative") + .with_stdout("please paste the token found on file://[..]/me below") + .with_stdin("sekrit") + .run(); +} + +#[cargo_test] +fn duplicate_index() { + let server = RegistryBuilder::new() + .alternative() + .no_configure_token() + .auth_required() + .build(); + let p = make_project(); + + // Two alternative registries with the same index. + cargo(&p, "build") + .env( + "CARGO_REGISTRIES_ALTERNATIVE1_INDEX", + server.index_url().as_str(), + ) + .env( + "CARGO_REGISTRIES_ALTERNATIVE2_INDEX", + server.index_url().as_str(), + ) + .with_status(101) + .with_stderr( + "\ +[UPDATING] `alternative` index +[ERROR] failed to download `bar v0.0.1 (registry `alternative`)` + +Caused by: + unable to get packages from source + +Caused by: + multiple registries are configured with the same index url \ + 'registry+file://[..]/alternative-registry': alternative1, alternative2 +", + ) + .run(); +} diff --git a/tests/testsuite/search.rs b/tests/testsuite/search.rs index dbf95257f82..1f6f403272c 100644 --- a/tests/testsuite/search.rs +++ b/tests/testsuite/search.rs @@ -167,3 +167,26 @@ fn colored_results() { .with_stdout_contains("[..]\x1b[[..]") .run(); } + +#[cargo_test] +fn auth_required_failure() { + let server = setup().auth_required().no_configure_token().build(); + + cargo_process("-Zregistry-auth search postgres") + .masquerade_as_nightly_cargo(&["registry-auth"]) + .replace_crates_io(server.index_url()) + .with_status(101) + .with_stderr_contains("[ERROR] no token found, please run `cargo login`") + .run(); +} + +#[cargo_test] +fn auth_required() { + let server = setup().auth_required().build(); + + cargo_process("-Zregistry-auth search postgres") + .masquerade_as_nightly_cargo(&["registry-auth"]) + .replace_crates_io(server.index_url()) + .with_stdout_contains(SEARCH_RESULTS) + .run(); +}