summaryrefslogtreecommitdiffstats
path: root/src/tools/cargo/credential/cargo-credential
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/cargo/credential/cargo-credential')
-rw-r--r--src/tools/cargo/credential/cargo-credential/Cargo.toml3
-rw-r--r--src/tools/cargo/credential/cargo-credential/README.md4
-rw-r--r--src/tools/cargo/credential/cargo-credential/examples/file-provider.rs4
-rw-r--r--src/tools/cargo/credential/cargo-credential/examples/stdout-redirected.rs4
-rw-r--r--src/tools/cargo/credential/cargo-credential/src/error.rs4
-rw-r--r--src/tools/cargo/credential/cargo-credential/src/lib.rs204
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);
+ }
+}