Skip to content

Commit 292b0a8

Browse files
committed
Auto merge of #12622 - arlosi:cred-cache, r=ehuss
cargo-credential: change serialization of cache expiration Serde has [multiple options](https://serde.rs/enum-representations.html) for serialization of enum types. The default is to use the field name in a map (for variants with data), or a string (for variants without data). This causes forward compatibility problems when switching between these two cases. To avoid this, all `enum`s within `cargo-credential` used the "internally tagged" approach, but `CacheControl` did not. * Changes `CacheControl` to be internally tagged and `flattened` within `CredentialResponse::Get` * Adds forward compatibility tests to ensure adding additional fields will not break deserialization Within a credential response, this is the change: ```diff - "cache":{"expires":1684251794}, + "cache":"expires", "expiration":1684251794, ``` Other variants, such as `"cache":"session"` remain the same. ## How to review This PR contains two commits, one for the breaking change for `CacheControl` serialization, and one non-breaking change that makes several other fields skipped if none/empty. r? `@ehuss`
2 parents 5a856bf + e58b84d commit 292b0a8

File tree

8 files changed

+196
-40
lines changed

8 files changed

+196
-40
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ anyhow = "1.0.75"
2020
base64 = "0.21.3"
2121
bytesize = "1.3"
2222
cargo = { path = "" }
23-
cargo-credential = { version = "0.3.0", path = "credential/cargo-credential" }
23+
cargo-credential = { version = "0.4.0", path = "credential/cargo-credential" }
2424
cargo-credential-libsecret = { version = "0.3.1", path = "credential/cargo-credential-libsecret" }
2525
cargo-credential-wincred = { version = "0.3.0", path = "credential/cargo-credential-wincred" }
2626
cargo-credential-macos-keychain = { version = "0.3.0", path = "credential/cargo-credential-macos-keychain" }

credential/cargo-credential/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cargo-credential"
3-
version = "0.3.1"
3+
version = "0.4.0"
44
edition.workspace = true
55
license.workspace = true
66
repository = "https://github.com/rust-lang/cargo"

credential/cargo-credential/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Create a Cargo project with this as a dependency:
1818
# Add this to your Cargo.toml:
1919

2020
[dependencies]
21-
cargo-credential = "0.3"
21+
cargo-credential = "0.4"
2222
```
2323

2424
And then include a `main.rs` binary which implements the `Credential` trait, and calls

credential/cargo-credential/src/lib.rs

Lines changed: 169 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub use secret::Secret;
5050
use stdio::stdin_stdout_to_console;
5151

5252
/// Message sent by the credential helper on startup
53-
#[derive(Serialize, Deserialize, Clone, Debug)]
53+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
5454
pub struct CredentialHello {
5555
// Protocol versions supported by the credential process.
5656
pub v: Vec<u32>,
@@ -70,7 +70,7 @@ impl Credential for UnsupportedCredential {
7070
}
7171

7272
/// Message sent by Cargo to the credential helper after the hello
73-
#[derive(Serialize, Deserialize, Clone, Debug)]
73+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
7474
#[serde(rename_all = "kebab-case")]
7575
pub struct CredentialRequest<'a> {
7676
// Cargo will respond with the highest common protocol supported by both.
@@ -80,23 +80,25 @@ pub struct CredentialRequest<'a> {
8080
#[serde(borrow, flatten)]
8181
pub action: Action<'a>,
8282
/// Additional command-line arguments passed to the credential provider.
83+
#[serde(skip_serializing_if = "Vec::is_empty", default)]
8384
pub args: Vec<&'a str>,
8485
}
8586

86-
#[derive(Serialize, Deserialize, Clone, Debug)]
87+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
8788
#[serde(rename_all = "kebab-case")]
8889
pub struct RegistryInfo<'a> {
8990
/// Registry index url
9091
pub index_url: &'a str,
9192
/// Name of the registry in configuration. May not be available.
9293
/// The crates.io registry will be `crates-io` (`CRATES_IO_REGISTRY`).
94+
#[serde(skip_serializing_if = "Option::is_none")]
9395
pub name: Option<&'a str>,
9496
/// Headers from attempting to access a registry that resulted in a HTTP 401.
9597
#[serde(skip_serializing_if = "Vec::is_empty", default)]
9698
pub headers: Vec<String>,
9799
}
98100

99-
#[derive(Serialize, Deserialize, Clone, Debug)]
101+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
100102
#[non_exhaustive]
101103
#[serde(tag = "kind", rename_all = "kebab-case")]
102104
pub enum Action<'a> {
@@ -119,17 +121,19 @@ impl<'a> Display for Action<'a> {
119121
}
120122
}
121123

122-
#[derive(Serialize, Deserialize, Clone, Debug)]
124+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
123125
#[serde(rename_all = "kebab-case")]
124126
pub struct LoginOptions<'a> {
125127
/// Token passed on the command line via --token or from stdin
128+
#[serde(skip_serializing_if = "Option::is_none")]
126129
pub token: Option<Secret<&'a str>>,
127130
/// Optional URL that the user can visit to log in to the registry
131+
#[serde(skip_serializing_if = "Option::is_none")]
128132
pub login_url: Option<&'a str>,
129133
}
130134

131135
/// A record of what kind of operation is happening that we should generate a token for.
132-
#[derive(Serialize, Deserialize, Clone, Debug)]
136+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
133137
#[non_exhaustive]
134138
#[serde(tag = "operation", rename_all = "kebab-case")]
135139
pub enum Operation<'a> {
@@ -168,12 +172,13 @@ pub enum Operation<'a> {
168172
}
169173

170174
/// Message sent by the credential helper
171-
#[derive(Serialize, Deserialize, Clone, Debug)]
175+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
172176
#[serde(tag = "kind", rename_all = "kebab-case")]
173177
#[non_exhaustive]
174178
pub enum CredentialResponse {
175179
Get {
176180
token: Secret<String>,
181+
#[serde(flatten)]
177182
cache: CacheControl,
178183
operation_independent: bool,
179184
},
@@ -183,14 +188,17 @@ pub enum CredentialResponse {
183188
Unknown,
184189
}
185190

186-
#[derive(Serialize, Deserialize, Clone, Debug)]
187-
#[serde(rename_all = "kebab-case")]
191+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
192+
#[serde(tag = "cache", rename_all = "kebab-case")]
188193
#[non_exhaustive]
189194
pub enum CacheControl {
190195
/// Do not cache this result.
191196
Never,
192197
/// Cache this result and use it for subsequent requests in the current Cargo invocation until the specified time.
193-
Expires(#[serde(with = "time::serde::timestamp")] OffsetDateTime),
198+
Expires {
199+
#[serde(with = "time::serde::timestamp")]
200+
expiration: OffsetDateTime,
201+
},
194202
/// Cache this result and use it for all subsequent requests in the current Cargo invocation.
195203
Session,
196204
#[serde(other)]
@@ -238,11 +246,7 @@ fn doit(
238246
if len == 0 {
239247
return Ok(());
240248
}
241-
let request: CredentialRequest = serde_json::from_str(&buffer)?;
242-
if request.v != PROTOCOL_VERSION_1 {
243-
return Err(format!("unsupported protocol version {}", request.v).into());
244-
}
245-
249+
let request = deserialize_request(&buffer)?;
246250
let response = stdin_stdout_to_console(|| {
247251
credential.perform(&request.registry, &request.action, &request.args)
248252
})?;
@@ -252,6 +256,17 @@ fn doit(
252256
}
253257
}
254258

259+
/// Deserialize a request from Cargo.
260+
fn deserialize_request(
261+
value: &str,
262+
) -> Result<CredentialRequest<'_>, Box<dyn std::error::Error + Send + Sync>> {
263+
let request: CredentialRequest = serde_json::from_str(&value)?;
264+
if request.v != PROTOCOL_VERSION_1 {
265+
return Err(format!("unsupported protocol version {}", request.v).into());
266+
}
267+
Ok(request)
268+
}
269+
255270
/// Read a line of text from stdin.
256271
pub fn read_line() -> Result<String, io::Error> {
257272
let mut buf = String::new();
@@ -278,3 +293,142 @@ pub fn read_token(
278293

279294
Ok(Secret::from(read_line().map_err(Box::new)?))
280295
}
296+
297+
#[cfg(test)]
298+
mod tests {
299+
use super::*;
300+
301+
#[test]
302+
fn unsupported_version() {
303+
// This shouldn't ever happen in practice, since the credential provider signals to Cargo which
304+
// protocol versions it supports, and Cargo should only attempt to use one of those.
305+
let msg = r#"{"v":999, "registry": {"index-url":""}, "args":[], "kind": "unexpected"}"#;
306+
assert_eq!(
307+
"unsupported protocol version 999",
308+
deserialize_request(msg).unwrap_err().to_string()
309+
);
310+
}
311+
312+
#[test]
313+
fn cache_control() {
314+
let cc = CacheControl::Expires {
315+
expiration: OffsetDateTime::from_unix_timestamp(1693928537).unwrap(),
316+
};
317+
let json = serde_json::to_string(&cc).unwrap();
318+
assert_eq!(json, r#"{"cache":"expires","expiration":1693928537}"#);
319+
320+
let cc = CacheControl::Session;
321+
let json = serde_json::to_string(&cc).unwrap();
322+
assert_eq!(json, r#"{"cache":"session"}"#);
323+
324+
let cc: CacheControl = serde_json::from_str(r#"{"cache":"unknown-kind"}"#).unwrap();
325+
assert_eq!(cc, CacheControl::Unknown);
326+
327+
assert_eq!(
328+
"missing field `expiration`",
329+
serde_json::from_str::<CacheControl>(r#"{"cache":"expires"}"#)
330+
.unwrap_err()
331+
.to_string()
332+
);
333+
}
334+
335+
#[test]
336+
fn credential_response() {
337+
let cr = CredentialResponse::Get {
338+
cache: CacheControl::Never,
339+
operation_independent: true,
340+
token: Secret::from("value".to_string()),
341+
};
342+
let json = serde_json::to_string(&cr).unwrap();
343+
assert_eq!(
344+
json,
345+
r#"{"kind":"get","token":"value","cache":"never","operation_independent":true}"#
346+
);
347+
348+
let cr = CredentialResponse::Login;
349+
let json = serde_json::to_string(&cr).unwrap();
350+
assert_eq!(json, r#"{"kind":"login"}"#);
351+
352+
let cr: CredentialResponse =
353+
serde_json::from_str(r#"{"kind":"unknown-kind","extra-data":true}"#).unwrap();
354+
assert_eq!(cr, CredentialResponse::Unknown);
355+
356+
let cr: CredentialResponse =
357+
serde_json::from_str(r#"{"kind":"login","extra-data":true}"#).unwrap();
358+
assert_eq!(cr, CredentialResponse::Login);
359+
360+
let cr: CredentialResponse = serde_json::from_str(r#"{"kind":"get","token":"value","cache":"never","operation_independent":true,"extra-field-ignored":123}"#).unwrap();
361+
assert_eq!(
362+
cr,
363+
CredentialResponse::Get {
364+
cache: CacheControl::Never,
365+
operation_independent: true,
366+
token: Secret::from("value".to_string())
367+
}
368+
);
369+
}
370+
371+
#[test]
372+
fn credential_request() {
373+
let get_oweners = CredentialRequest {
374+
v: PROTOCOL_VERSION_1,
375+
args: vec![],
376+
registry: RegistryInfo {
377+
index_url: "url",
378+
name: None,
379+
headers: vec![],
380+
},
381+
action: Action::Get(Operation::Owners { name: "pkg" }),
382+
};
383+
384+
let json = serde_json::to_string(&get_oweners).unwrap();
385+
assert_eq!(
386+
json,
387+
r#"{"v":1,"registry":{"index-url":"url"},"kind":"get","operation":"owners","name":"pkg"}"#
388+
);
389+
390+
let cr: CredentialRequest =
391+
serde_json::from_str(r#"{"extra-1":true,"v":1,"registry":{"index-url":"url","extra-2":true},"kind":"get","operation":"owners","name":"pkg","args":[]}"#).unwrap();
392+
assert_eq!(cr, get_oweners);
393+
}
394+
395+
#[test]
396+
fn credential_request_logout() {
397+
let unknown = CredentialRequest {
398+
v: PROTOCOL_VERSION_1,
399+
args: vec![],
400+
registry: RegistryInfo {
401+
index_url: "url",
402+
name: None,
403+
headers: vec![],
404+
},
405+
action: Action::Logout,
406+
};
407+
408+
let cr: CredentialRequest = serde_json::from_str(
409+
r#"{"v":1,"registry":{"index-url":"url"},"kind":"logout","extra-1":true,"args":[]}"#,
410+
)
411+
.unwrap();
412+
assert_eq!(cr, unknown);
413+
}
414+
415+
#[test]
416+
fn credential_request_unknown() {
417+
let unknown = CredentialRequest {
418+
v: PROTOCOL_VERSION_1,
419+
args: vec![],
420+
registry: RegistryInfo {
421+
index_url: "",
422+
name: None,
423+
headers: vec![],
424+
},
425+
action: Action::Unknown,
426+
};
427+
428+
let cr: CredentialRequest = serde_json::from_str(
429+
r#"{"v":1,"registry":{"index-url":""},"kind":"unexpected-1","extra-1":true,"args":[]}"#,
430+
)
431+
.unwrap();
432+
assert_eq!(cr, unknown);
433+
}
434+
}

src/cargo/util/auth/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ fn auth_token_optional(
563563
let token = Secret::from(token);
564564
tracing::trace!("found token");
565565
let expiration = match cache_control {
566-
CacheControl::Expires(expiration) => Some(expiration),
566+
CacheControl::Expires { expiration } => Some(expiration),
567567
CacheControl::Session => None,
568568
CacheControl::Never | _ => return Ok(Some(token)),
569569
};

src/doc/src/reference/unstable.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,7 +1154,7 @@ Actual messages must not contain newlines.
11541154
"operation":"read",
11551155
// Registry information
11561156
"registry":{"index-url":"sparse+https://registry-url/index/", "name": "my-registry"},
1157-
// Additional command-line args
1157+
// Additional command-line args (optional)
11581158
"args":[]
11591159
}
11601160
```
@@ -1178,7 +1178,7 @@ Actual messages must not contain newlines.
11781178
"cksum":"...",
11791179
// Registry information
11801180
"registry":{"index-url":"sparse+https://registry-url/index/", "name": "my-registry"},
1181-
// Additional command-line args
1181+
// Additional command-line args (optional)
11821182
"args":[]
11831183
}
11841184
```
@@ -1195,8 +1195,10 @@ Actual messages must not contain newlines.
11951195
// Cache control. Can be one of the following:
11961196
// * "never"
11971197
// * "session"
1198-
// * { "expires": UNIX timestamp }
1199-
"cache":{"expires":1684251794},
1198+
// * "expires"
1199+
"cache":"expires",
1200+
// Unix timestamp (only for "cache": "expires")
1201+
"expiration":1693942857,
12001202
// Is the token operation independent?
12011203
"operation_independent":true
12021204
}}

0 commit comments

Comments
 (0)