Skip to content

Commit 4d5c036

Browse files
committed
Auto merge of #10592 - arlosi:auth, r=ehuss
Implement RFC 3139: alternative registry authentication support Allows registries to request Cargo to send the authentication token for all requests, rather than just publish/yank, implementing [RFC 3139](#10474). ### Items from the [tracking issue](#10474) > Do registries need a more fine-grained switch for which API commands require authentication? This PR uses the `auth_required` boolean as described in the RFC. > The RFC mentions adding --token to additional commands like install and search These flags are not added by this PR. > Consider changing the name and form of the X- header Changed to the `www-authenticate` header as suggested by the comments. > Will there be any concerns with the interaction with rust-lang/rfcs#3231 Not that I know of. ------------- Adds a new field `"auth-required": true` to `config.json` that indicates Cargo should include the token in all requests to a registry. For HTTP registries, Cargo first attempts an un-authenticated request, then if that fails with HTTP 401, an authenticated request is attempted. The registry server may include a `www-authenticate` header with the HTTP 401 to instruct Cargo with URL the user can visit to acquire a token (crates.io/me). Since the API URL is not known (because it's stored in the index), the unstable credential provider feature is modified to key off the index url, and the registry name is no longer provided. To handle the case where an alternative registry's name is not known (such as coming from a lock file, or via `--index`), Cargo can now look up the token in the configuration by matching on the index URL. This introduces a new error if two alternative registries are configured with the same index URL. Several operations, such as `cargo install` could have had a `--token` argument added, however it appears that Cargo would like to move away from passing the token on the command line for security reasons. In this case, users would need to configure the registry via the config file (or environment variables) when using `cargo install --index ...` or similar.
2 parents b690ab4 + 9827412 commit 4d5c036

File tree

42 files changed

+1468
-661
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1468
-661
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@
326326
- Added `-Zcheck-cfg=output` to support build-scripts declaring their
327327
supported set of `cfg` values with `cargo:rustc-check-cfg`.
328328
[#10539](https://github.com/rust-lang/cargo/pull/10539)
329-
- `-Z http-registry` now uses https://index.crates.io/ when accessing crates-io.
329+
- `-Z sparse-registry` now uses https://index.crates.io/ when accessing crates-io.
330330
[#10725](https://github.com/rust-lang/cargo/pull/10725)
331331
- Fixed formatting of `.workspace` key in `cargo add` for workspace inheritance.
332332
[#10705](https://github.com/rust-lang/cargo/pull/10705)

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ git2-curl = "0.16.0"
3333
glob = "0.3.0"
3434
hex = "0.4"
3535
home = "0.5"
36+
http-auth = { version = "0.1.6", default-features = false }
3637
humantime = "2.0.0"
3738
indexmap = "1"
3839
ignore = "0.4.7"

crates/cargo-test-support/src/registry.rs

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::fs::{self, File};
1111
use std::io::{BufRead, BufReader, Read, Write};
1212
use std::net::{SocketAddr, TcpListener, TcpStream};
1313
use std::path::PathBuf;
14-
use std::thread;
14+
use std::thread::{self, JoinHandle};
1515
use tar::{Builder, Header};
1616
use url::Url;
1717

@@ -61,6 +61,8 @@ pub struct RegistryBuilder {
6161
alternative: Option<String>,
6262
/// If set, the authorization token for the registry.
6363
token: Option<String>,
64+
/// If set, the registry requires authorization for all operations.
65+
auth_required: bool,
6466
/// If set, serves the index over http.
6567
http_index: bool,
6668
/// If set, serves the API over http.
@@ -76,7 +78,7 @@ pub struct RegistryBuilder {
7678
}
7779

7880
pub struct TestRegistry {
79-
_server: Option<HttpServerHandle>,
81+
server: Option<HttpServerHandle>,
8082
index_url: Url,
8183
path: PathBuf,
8284
api_url: Url,
@@ -98,6 +100,17 @@ impl TestRegistry {
98100
.as_deref()
99101
.expect("registry was not configured with a token")
100102
}
103+
104+
/// Shutdown the server thread and wait for it to stop.
105+
/// `Drop` automatically stops the server, but this additionally
106+
/// waits for the thread to stop.
107+
pub fn join(self) {
108+
if let Some(mut server) = self.server {
109+
server.stop();
110+
let handle = server.handle.take().unwrap();
111+
handle.join().unwrap();
112+
}
113+
}
101114
}
102115

103116
impl RegistryBuilder {
@@ -106,6 +119,7 @@ impl RegistryBuilder {
106119
RegistryBuilder {
107120
alternative: None,
108121
token: None,
122+
auth_required: false,
109123
http_api: false,
110124
http_index: false,
111125
api: true,
@@ -160,6 +174,14 @@ impl RegistryBuilder {
160174
self
161175
}
162176

177+
/// Sets this registry to require the authentication token for
178+
/// all operations.
179+
#[must_use]
180+
pub fn auth_required(mut self) -> Self {
181+
self.auth_required = true;
182+
self
183+
}
184+
163185
/// Operate the index over http
164186
#[must_use]
165187
pub fn http_index(mut self) -> Self {
@@ -207,6 +229,7 @@ impl RegistryBuilder {
207229
registry_path.clone(),
208230
dl_path,
209231
token.clone(),
232+
self.auth_required,
210233
self.custom_responders,
211234
);
212235
let index_url = if self.http_index {
@@ -226,7 +249,7 @@ impl RegistryBuilder {
226249
let registry = TestRegistry {
227250
api_url,
228251
index_url,
229-
_server: server,
252+
server,
230253
dl_url,
231254
path: registry_path,
232255
token,
@@ -293,6 +316,11 @@ impl RegistryBuilder {
293316
}
294317
}
295318

319+
let auth = if self.auth_required {
320+
r#","auth-required":true"#
321+
} else {
322+
""
323+
};
296324
let api = if self.api {
297325
format!(r#","api":"{}""#, registry.api_url)
298326
} else {
@@ -302,7 +330,7 @@ impl RegistryBuilder {
302330
repo(&registry.path)
303331
.file(
304332
"config.json",
305-
&format!(r#"{{"dl":"{}"{api}}}"#, registry.dl_url),
333+
&format!(r#"{{"dl":"{}"{api}{auth}}}"#, registry.dl_url),
306334
)
307335
.build();
308336
fs::create_dir_all(api_path.join("api/v1/crates")).unwrap();
@@ -442,6 +470,7 @@ pub fn alt_init() -> TestRegistry {
442470

443471
pub struct HttpServerHandle {
444472
addr: SocketAddr,
473+
handle: Option<JoinHandle<()>>,
445474
}
446475

447476
impl HttpServerHandle {
@@ -456,10 +485,8 @@ impl HttpServerHandle {
456485
pub fn dl_url(&self) -> Url {
457486
Url::parse(&format!("http://{}/dl", self.addr.to_string())).unwrap()
458487
}
459-
}
460488

461-
impl Drop for HttpServerHandle {
462-
fn drop(&mut self) {
489+
fn stop(&self) {
463490
if let Ok(mut stream) = TcpStream::connect(self.addr) {
464491
// shutdown the server
465492
let _ = stream.write_all(b"stop");
@@ -468,6 +495,12 @@ impl Drop for HttpServerHandle {
468495
}
469496
}
470497

498+
impl Drop for HttpServerHandle {
499+
fn drop(&mut self) {
500+
self.stop();
501+
}
502+
}
503+
471504
/// Request to the test http server
472505
#[derive(Clone)]
473506
pub struct Request {
@@ -504,6 +537,7 @@ pub struct HttpServer {
504537
registry_path: PathBuf,
505538
dl_path: PathBuf,
506539
token: Option<String>,
540+
auth_required: bool,
507541
custom_responders: HashMap<&'static str, Box<dyn Send + Fn(&Request, &HttpServer) -> Response>>,
508542
}
509543

@@ -512,6 +546,7 @@ impl HttpServer {
512546
registry_path: PathBuf,
513547
dl_path: PathBuf,
514548
token: Option<String>,
549+
auth_required: bool,
515550
api_responders: HashMap<
516551
&'static str,
517552
Box<dyn Send + Fn(&Request, &HttpServer) -> Response>,
@@ -524,10 +559,11 @@ impl HttpServer {
524559
registry_path,
525560
dl_path,
526561
token,
562+
auth_required,
527563
custom_responders: api_responders,
528564
};
529-
thread::spawn(move || server.start());
530-
HttpServerHandle { addr }
565+
let handle = Some(thread::spawn(move || server.start()));
566+
HttpServerHandle { addr, handle }
531567
}
532568

533569
fn start(&self) {
@@ -615,7 +651,7 @@ impl HttpServer {
615651
/// Route the request
616652
fn route(&self, req: &Request) -> Response {
617653
let authorized = |mutatation: bool| {
618-
if mutatation {
654+
if mutatation || self.auth_required {
619655
self.token == req.authorization
620656
} else {
621657
assert!(req.authorization.is_none(), "unexpected token");
@@ -676,7 +712,9 @@ impl HttpServer {
676712
pub fn unauthorized(&self, _req: &Request) -> Response {
677713
Response {
678714
code: 401,
679-
headers: vec![],
715+
headers: vec![
716+
r#"WWW-Authenticate: Cargo login_url="https://test-registry-login/me""#.to_string(),
717+
],
680718
body: b"Unauthorized message from server.".to_vec(),
681719
}
682720
}

crates/crates-io/lib.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub struct Registry {
2121
token: Option<String>,
2222
/// Curl handle for issuing requests.
2323
handle: Easy,
24+
/// Whether to include the authorization token with all requests.
25+
auth_required: bool,
2426
}
2527

2628
#[derive(PartialEq, Clone, Copy)]
@@ -199,11 +201,17 @@ impl Registry {
199201
/// handle.useragent("my_crawler (example.com/info)");
200202
/// let mut reg = Registry::new_handle(String::from("https://crates.io"), None, handle);
201203
/// ```
202-
pub fn new_handle(host: String, token: Option<String>, handle: Easy) -> Registry {
204+
pub fn new_handle(
205+
host: String,
206+
token: Option<String>,
207+
handle: Easy,
208+
auth_required: bool,
209+
) -> Registry {
203210
Registry {
204211
host,
205212
token,
206213
handle,
214+
auth_required,
207215
}
208216
}
209217

@@ -377,7 +385,7 @@ impl Registry {
377385
headers.append("Accept: application/json")?;
378386
headers.append("Content-Type: application/json")?;
379387

380-
if authorized == Auth::Authorized {
388+
if self.auth_required || authorized == Auth::Authorized {
381389
let token = match self.token.as_ref() {
382390
Some(s) => s,
383391
None => bail!("no upload token found, please run `cargo login`"),
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[package]
22
name = "cargo-credential-1password"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
55
license = "MIT OR Apache-2.0"
66
repository = "https://github.com/rust-lang/cargo"
77
description = "A Cargo credential process that stores tokens in a 1password vault."
88

99
[dependencies]
10-
cargo-credential = { version = "0.1.0", path = "../cargo-credential" }
10+
cargo-credential = { version = "0.2.0", path = "../cargo-credential" }
1111
serde = { version = "1.0.117", features = ["derive"] }
1212
serde_json = "1.0.59"

crates/credential/cargo-credential-1password/src/main.rs

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct ListItem {
4141

4242
#[derive(Deserialize)]
4343
struct Overview {
44-
title: String,
44+
url: String,
4545
}
4646

4747
impl OnePasswordKeychain {
@@ -175,11 +175,7 @@ impl OnePasswordKeychain {
175175
Ok(buffer)
176176
}
177177

178-
fn search(
179-
&self,
180-
session: &Option<String>,
181-
registry_name: &str,
182-
) -> Result<Option<String>, Error> {
178+
fn search(&self, session: &Option<String>, index_url: &str) -> Result<Option<String>, Error> {
183179
let cmd = self.make_cmd(
184180
session,
185181
&[
@@ -196,15 +192,15 @@ impl OnePasswordKeychain {
196192
.map_err(|e| format!("failed to deserialize JSON from 1password list: {}", e))?;
197193
let mut matches = items
198194
.into_iter()
199-
.filter(|item| item.overview.title == registry_name);
195+
.filter(|item| item.overview.url == index_url);
200196
match matches.next() {
201197
Some(login) => {
202198
// Should this maybe just sort on `updatedAt` and return the newest one?
203199
if matches.next().is_some() {
204200
return Err(format!(
205-
"too many 1password logins match registry name {}, \
201+
"too many 1password logins match registry `{}`, \
206202
consider deleting the excess entries",
207-
registry_name
203+
index_url
208204
)
209205
.into());
210206
}
@@ -214,7 +210,13 @@ impl OnePasswordKeychain {
214210
}
215211
}
216212

217-
fn modify(&self, session: &Option<String>, uuid: &str, token: &str) -> Result<(), Error> {
213+
fn modify(
214+
&self,
215+
session: &Option<String>,
216+
uuid: &str,
217+
token: &str,
218+
_name: Option<&str>,
219+
) -> Result<(), Error> {
218220
let cmd = self.make_cmd(
219221
session,
220222
&["edit", "item", uuid, &format!("password={}", token)],
@@ -226,20 +228,24 @@ impl OnePasswordKeychain {
226228
fn create(
227229
&self,
228230
session: &Option<String>,
229-
registry_name: &str,
230-
api_url: &str,
231+
index_url: &str,
231232
token: &str,
233+
name: Option<&str>,
232234
) -> Result<(), Error> {
235+
let title = match name {
236+
Some(name) => format!("Cargo registry token for {}", name),
237+
None => "Cargo registry token".to_string(),
238+
};
233239
let cmd = self.make_cmd(
234240
session,
235241
&[
236242
"create",
237243
"item",
238244
"Login",
239245
&format!("password={}", token),
240-
&format!("url={}", api_url),
246+
&format!("url={}", index_url),
241247
"--title",
242-
registry_name,
248+
&title,
243249
"--tags",
244250
CARGO_TAG,
245251
],
@@ -276,36 +282,36 @@ impl Credential for OnePasswordKeychain {
276282
env!("CARGO_PKG_NAME")
277283
}
278284

279-
fn get(&self, registry_name: &str, _api_url: &str) -> Result<String, Error> {
285+
fn get(&self, index_url: &str) -> Result<String, Error> {
280286
let session = self.signin()?;
281-
if let Some(uuid) = self.search(&session, registry_name)? {
287+
if let Some(uuid) = self.search(&session, index_url)? {
282288
self.get_token(&session, &uuid)
283289
} else {
284290
return Err(format!(
285291
"no 1password entry found for registry `{}`, try `cargo login` to add a token",
286-
registry_name
292+
index_url
287293
)
288294
.into());
289295
}
290296
}
291297

292-
fn store(&self, registry_name: &str, api_url: &str, token: &str) -> Result<(), Error> {
298+
fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
293299
let session = self.signin()?;
294300
// Check if an item already exists.
295-
if let Some(uuid) = self.search(&session, registry_name)? {
296-
self.modify(&session, &uuid, token)
301+
if let Some(uuid) = self.search(&session, index_url)? {
302+
self.modify(&session, &uuid, token, name)
297303
} else {
298-
self.create(&session, registry_name, api_url, token)
304+
self.create(&session, index_url, token, name)
299305
}
300306
}
301307

302-
fn erase(&self, registry_name: &str, _api_url: &str) -> Result<(), Error> {
308+
fn erase(&self, index_url: &str) -> Result<(), Error> {
303309
let session = self.signin()?;
304310
// Check if an item already exists.
305-
if let Some(uuid) = self.search(&session, registry_name)? {
311+
if let Some(uuid) = self.search(&session, index_url)? {
306312
self.delete(&session, &uuid)?;
307313
} else {
308-
eprintln!("not currently logged in to `{}`", registry_name);
314+
eprintln!("not currently logged in to `{}`", index_url);
309315
}
310316
Ok(())
311317
}

0 commit comments

Comments
 (0)