diff options
Diffstat (limited to 'src/tools/cargo/credential/cargo-credential')
6 files changed, 190 insertions, 33 deletions
diff --git a/src/tools/cargo/credential/cargo-credential/Cargo.toml b/src/tools/cargo/credential/cargo-credential/Cargo.toml index 8cd1348be..c8db996bf 100644 --- a/src/tools/cargo/credential/cargo-credential/Cargo.toml +++ b/src/tools/cargo/credential/cargo-credential/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "cargo-credential" -version = "0.3.0" +version = "0.4.0" edition.workspace = true license.workspace = true +rust-version = "1.70.0" repository = "https://github.com/rust-lang/cargo" description = "A library to assist writing Cargo credential helpers." diff --git a/src/tools/cargo/credential/cargo-credential/README.md b/src/tools/cargo/credential/cargo-credential/README.md index 049b3ba55..d87d41bb8 100644 --- a/src/tools/cargo/credential/cargo-credential/README.md +++ b/src/tools/cargo/credential/cargo-credential/README.md @@ -5,7 +5,7 @@ provides an interface to store tokens for authorizing access to a registry such as https://crates.io/. Documentation about credential processes may be found at -https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#credential-process +https://doc.rust-lang.org/nightly/cargo/reference/credential-provider-protocol.html Example implementations may be found at https://github.com/rust-lang/cargo/tree/master/credential @@ -18,7 +18,7 @@ Create a Cargo project with this as a dependency: # Add this to your Cargo.toml: [dependencies] -cargo-credential = "0.3" +cargo-credential = "0.4" ``` And then include a `main.rs` binary which implements the `Credential` trait, and calls diff --git a/src/tools/cargo/credential/cargo-credential/examples/file-provider.rs b/src/tools/cargo/credential/cargo-credential/examples/file-provider.rs index d11958536..3ed312cb8 100644 --- a/src/tools/cargo/credential/cargo-credential/examples/file-provider.rs +++ b/src/tools/cargo/credential/cargo-credential/examples/file-provider.rs @@ -12,8 +12,8 @@ struct FileCredential; impl Credential for FileCredential { fn perform( &self, - registry: &RegistryInfo, - action: &Action, + registry: &RegistryInfo<'_>, + action: &Action<'_>, _args: &[&str], ) -> Result<CredentialResponse, cargo_credential::Error> { if registry.index_url != "https://github.com/rust-lang/crates.io-index" { diff --git a/src/tools/cargo/credential/cargo-credential/examples/stdout-redirected.rs b/src/tools/cargo/credential/cargo-credential/examples/stdout-redirected.rs index 0b9bcc2f7..75a2d16d1 100644 --- a/src/tools/cargo/credential/cargo-credential/examples/stdout-redirected.rs +++ b/src/tools/cargo/credential/cargo-credential/examples/stdout-redirected.rs @@ -7,8 +7,8 @@ struct MyCredential; impl Credential for MyCredential { fn perform( &self, - _registry: &RegistryInfo, - _action: &Action, + _registry: &RegistryInfo<'_>, + _action: &Action<'_>, _args: &[&str], ) -> Result<CredentialResponse, Error> { // Informational messages should be sent on stderr. diff --git a/src/tools/cargo/credential/cargo-credential/src/error.rs b/src/tools/cargo/credential/cargo-credential/src/error.rs index 2ebaf9977..8c5fe19e5 100644 --- a/src/tools/cargo/credential/cargo-credential/src/error.rs +++ b/src/tools/cargo/credential/cargo-credential/src/error.rs @@ -42,9 +42,9 @@ pub enum Error { } impl From<String> for Error { - fn from(err: String) -> Self { + fn from(message: String) -> Self { Box::new(StringTypedError { - message: err.to_string(), + message, source: None, }) .into() diff --git a/src/tools/cargo/credential/cargo-credential/src/lib.rs b/src/tools/cargo/credential/cargo-credential/src/lib.rs index 0fb495ed3..60bce65be 100644 --- a/src/tools/cargo/credential/cargo-credential/src/lib.rs +++ b/src/tools/cargo/credential/cargo-credential/src/lib.rs @@ -50,7 +50,7 @@ pub use secret::Secret; use stdio::stdin_stdout_to_console; /// Message sent by the credential helper on startup -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct CredentialHello { // Protocol versions supported by the credential process. pub v: Vec<u32>, @@ -61,8 +61,8 @@ pub struct UnsupportedCredential; impl Credential for UnsupportedCredential { fn perform( &self, - _registry: &RegistryInfo, - _action: &Action, + _registry: &RegistryInfo<'_>, + _action: &Action<'_>, _args: &[&str], ) -> Result<CredentialResponse, Error> { Err(Error::UrlNotSupported) @@ -70,7 +70,7 @@ impl Credential for UnsupportedCredential { } /// Message sent by Cargo to the credential helper after the hello -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct CredentialRequest<'a> { // Cargo will respond with the highest common protocol supported by both. @@ -80,23 +80,25 @@ pub struct CredentialRequest<'a> { #[serde(borrow, flatten)] pub action: Action<'a>, /// Additional command-line arguments passed to the credential provider. + #[serde(skip_serializing_if = "Vec::is_empty", default)] pub args: Vec<&'a str>, } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct RegistryInfo<'a> { /// Registry index url pub index_url: &'a str, /// Name of the registry in configuration. May not be available. /// The crates.io registry will be `crates-io` (`CRATES_IO_REGISTRY`). + #[serde(skip_serializing_if = "Option::is_none")] pub name: Option<&'a str>, /// Headers from attempting to access a registry that resulted in a HTTP 401. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub headers: Vec<String>, } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] #[serde(tag = "kind", rename_all = "kebab-case")] pub enum Action<'a> { @@ -119,17 +121,19 @@ impl<'a> Display for Action<'a> { } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct LoginOptions<'a> { /// Token passed on the command line via --token or from stdin + #[serde(skip_serializing_if = "Option::is_none")] pub token: Option<Secret<&'a str>>, /// Optional URL that the user can visit to log in to the registry + #[serde(skip_serializing_if = "Option::is_none")] pub login_url: Option<&'a str>, } /// A record of what kind of operation is happening that we should generate a token for. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] #[serde(tag = "operation", rename_all = "kebab-case")] pub enum Operation<'a> { @@ -168,12 +172,13 @@ pub enum Operation<'a> { } /// Message sent by the credential helper -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(tag = "kind", rename_all = "kebab-case")] #[non_exhaustive] pub enum CredentialResponse { Get { token: Secret<String>, + #[serde(flatten)] cache: CacheControl, operation_independent: bool, }, @@ -183,30 +188,35 @@ pub enum CredentialResponse { Unknown, } -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(tag = "cache", rename_all = "kebab-case")] #[non_exhaustive] pub enum CacheControl { /// Do not cache this result. Never, /// Cache this result and use it for subsequent requests in the current Cargo invocation until the specified time. - Expires(#[serde(with = "time::serde::timestamp")] OffsetDateTime), + Expires { + #[serde(with = "time::serde::timestamp")] + expiration: OffsetDateTime, + }, /// Cache this result and use it for all subsequent requests in the current Cargo invocation. Session, #[serde(other)] Unknown, } -/// Credential process JSON protocol version. Incrementing -/// this version will prevent new credential providers -/// from working with older versions of Cargo. +/// Credential process JSON protocol version. If the protocol needs to make +/// a breaking change, a new protocol version should be defined (`PROTOCOL_VERSION_2`). +/// This library should offer support for both protocols if possible, by signaling +/// in the `CredentialHello` message. Cargo will then choose which protocol to use, +/// or it will error if there are no common protocol versions available. pub const PROTOCOL_VERSION_1: u32 = 1; pub trait Credential { /// Retrieves a token for the given registry. fn perform( &self, - registry: &RegistryInfo, - action: &Action, + registry: &RegistryInfo<'_>, + action: &Action<'_>, args: &[&str], ) -> Result<CredentialResponse, Error>; } @@ -236,11 +246,7 @@ fn doit( if len == 0 { return Ok(()); } - let request: CredentialRequest = serde_json::from_str(&buffer)?; - if request.v != PROTOCOL_VERSION_1 { - return Err(format!("unsupported protocol version {}", request.v).into()); - } - + let request = deserialize_request(&buffer)?; let response = stdin_stdout_to_console(|| { credential.perform(&request.registry, &request.action, &request.args) })?; @@ -250,6 +256,17 @@ fn doit( } } +/// Deserialize a request from Cargo. +fn deserialize_request( + value: &str, +) -> Result<CredentialRequest<'_>, Box<dyn std::error::Error + Send + Sync>> { + let request: CredentialRequest<'_> = serde_json::from_str(&value)?; + if request.v != PROTOCOL_VERSION_1 { + return Err(format!("unsupported protocol version {}", request.v).into()); + } + Ok(request) +} + /// Read a line of text from stdin. pub fn read_line() -> Result<String, io::Error> { let mut buf = String::new(); @@ -259,8 +276,8 @@ pub fn read_line() -> Result<String, io::Error> { /// Prompt the user for a token. pub fn read_token( - login_options: &LoginOptions, - registry: &RegistryInfo, + login_options: &LoginOptions<'_>, + registry: &RegistryInfo<'_>, ) -> Result<Secret<String>, Error> { if let Some(token) = &login_options.token { return Ok(token.to_owned()); @@ -276,3 +293,142 @@ pub fn read_token( Ok(Secret::from(read_line().map_err(Box::new)?)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unsupported_version() { + // This shouldn't ever happen in practice, since the credential provider signals to Cargo which + // protocol versions it supports, and Cargo should only attempt to use one of those. + let msg = r#"{"v":999, "registry": {"index-url":""}, "args":[], "kind": "unexpected"}"#; + assert_eq!( + "unsupported protocol version 999", + deserialize_request(msg).unwrap_err().to_string() + ); + } + + #[test] + fn cache_control() { + let cc = CacheControl::Expires { + expiration: OffsetDateTime::from_unix_timestamp(1693928537).unwrap(), + }; + let json = serde_json::to_string(&cc).unwrap(); + assert_eq!(json, r#"{"cache":"expires","expiration":1693928537}"#); + + let cc = CacheControl::Session; + let json = serde_json::to_string(&cc).unwrap(); + assert_eq!(json, r#"{"cache":"session"}"#); + + let cc: CacheControl = serde_json::from_str(r#"{"cache":"unknown-kind"}"#).unwrap(); + assert_eq!(cc, CacheControl::Unknown); + + assert_eq!( + "missing field `expiration`", + serde_json::from_str::<CacheControl>(r#"{"cache":"expires"}"#) + .unwrap_err() + .to_string() + ); + } + + #[test] + fn credential_response() { + let cr = CredentialResponse::Get { + cache: CacheControl::Never, + operation_independent: true, + token: Secret::from("value".to_string()), + }; + let json = serde_json::to_string(&cr).unwrap(); + assert_eq!( + json, + r#"{"kind":"get","token":"value","cache":"never","operation_independent":true}"# + ); + + let cr = CredentialResponse::Login; + let json = serde_json::to_string(&cr).unwrap(); + assert_eq!(json, r#"{"kind":"login"}"#); + + let cr: CredentialResponse = + serde_json::from_str(r#"{"kind":"unknown-kind","extra-data":true}"#).unwrap(); + assert_eq!(cr, CredentialResponse::Unknown); + + let cr: CredentialResponse = + serde_json::from_str(r#"{"kind":"login","extra-data":true}"#).unwrap(); + assert_eq!(cr, CredentialResponse::Login); + + let cr: CredentialResponse = serde_json::from_str(r#"{"kind":"get","token":"value","cache":"never","operation_independent":true,"extra-field-ignored":123}"#).unwrap(); + assert_eq!( + cr, + CredentialResponse::Get { + cache: CacheControl::Never, + operation_independent: true, + token: Secret::from("value".to_string()) + } + ); + } + + #[test] + fn credential_request() { + let get_oweners = CredentialRequest { + v: PROTOCOL_VERSION_1, + args: vec![], + registry: RegistryInfo { + index_url: "url", + name: None, + headers: vec![], + }, + action: Action::Get(Operation::Owners { name: "pkg" }), + }; + + let json = serde_json::to_string(&get_oweners).unwrap(); + assert_eq!( + json, + r#"{"v":1,"registry":{"index-url":"url"},"kind":"get","operation":"owners","name":"pkg"}"# + ); + + let cr: CredentialRequest<'_> = + 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(); + assert_eq!(cr, get_oweners); + } + + #[test] + fn credential_request_logout() { + let unknown = CredentialRequest { + v: PROTOCOL_VERSION_1, + args: vec![], + registry: RegistryInfo { + index_url: "url", + name: None, + headers: vec![], + }, + action: Action::Logout, + }; + + let cr: CredentialRequest<'_> = serde_json::from_str( + r#"{"v":1,"registry":{"index-url":"url"},"kind":"logout","extra-1":true,"args":[]}"#, + ) + .unwrap(); + assert_eq!(cr, unknown); + } + + #[test] + fn credential_request_unknown() { + let unknown = CredentialRequest { + v: PROTOCOL_VERSION_1, + args: vec![], + registry: RegistryInfo { + index_url: "", + name: None, + headers: vec![], + }, + action: Action::Unknown, + }; + + let cr: CredentialRequest<'_> = serde_json::from_str( + r#"{"v":1,"registry":{"index-url":""},"kind":"unexpected-1","extra-1":true,"args":[]}"#, + ) + .unwrap(); + assert_eq!(cr, unknown); + } +} |