| //! Helper library for writing Cargo credential providers. |
| //! |
| //! A credential process should have a `struct` that implements the `Credential` trait. |
| //! The `main` function should be called with an instance of that struct, such as: |
| //! |
| //! ```rust,ignore |
| //! fn main() { |
| //! cargo_credential::main(MyCredential); |
| //! } |
| //! ``` |
| //! |
| //! While in the `perform` function, stdin and stdout will be re-attached to the |
| //! active console. This allows credential providers to be interactive if necessary. |
| //! |
| //! ## Error handling |
| //! ### [`Error::UrlNotSupported`] |
| //! A credential provider may only support some registry URLs. If this is the case |
| //! and an unsupported index URL is passed to the provider, it should respond with |
| //! [`Error::UrlNotSupported`]. Other credential providers may be attempted by Cargo. |
| //! |
| //! ### [`Error::NotFound`] |
| //! When attempting an [`Action::Get`] or [`Action::Logout`], if a credential can not |
| //! be found, the provider should respond with [`Error::NotFound`]. Other credential |
| //! providers may be attempted by Cargo. |
| //! |
| //! ### [`Error::OperationNotSupported`] |
| //! A credential provider might not support all operations. For example if the provider |
| //! only supports [`Action::Get`], [`Error::OperationNotSupported`] should be returned |
| //! for all other requests. |
| //! |
| //! ### [`Error::Other`] |
| //! All other errors go here. The error will be shown to the user in Cargo, including |
| //! the full error chain using [`std::error::Error::source`]. |
| //! |
| //! ## Example |
| //! ```rust,ignore |
| #![doc = include_str!("../examples/file-provider.rs")] |
| //! ``` |
| |
| use serde::{Deserialize, Serialize}; |
| use std::{fmt::Display, io}; |
| use time::OffsetDateTime; |
| |
| mod error; |
| mod secret; |
| mod stdio; |
| |
| pub use error::Error; |
| pub use secret::Secret; |
| use stdio::stdin_stdout_to_console; |
| |
| /// Message sent by the credential helper on startup |
| #[derive(Serialize, Deserialize, Clone, Debug)] |
| pub struct CredentialHello { |
| // Protocol versions supported by the credential process. |
| pub v: Vec<u32>, |
| } |
| |
| /// Credential provider that doesn't support any registries. |
| pub struct UnsupportedCredential; |
| impl Credential for UnsupportedCredential { |
| fn perform( |
| &self, |
| _registry: &RegistryInfo, |
| _action: &Action, |
| _args: &[&str], |
| ) -> Result<CredentialResponse, Error> { |
| Err(Error::UrlNotSupported) |
| } |
| } |
| |
| /// Message sent by Cargo to the credential helper after the hello |
| #[derive(Serialize, Deserialize, Clone, Debug)] |
| #[serde(rename_all = "kebab-case")] |
| pub struct CredentialRequest<'a> { |
| // Cargo will respond with the highest common protocol supported by both. |
| pub v: u32, |
| #[serde(borrow)] |
| pub registry: RegistryInfo<'a>, |
| #[serde(borrow, flatten)] |
| pub action: Action<'a>, |
| /// Additional command-line arguments passed to the credential provider. |
| pub args: Vec<&'a str>, |
| } |
| |
| #[derive(Serialize, Deserialize, Clone, Debug)] |
| #[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`). |
| 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)] |
| #[non_exhaustive] |
| #[serde(tag = "kind", rename_all = "kebab-case")] |
| pub enum Action<'a> { |
| #[serde(borrow)] |
| Get(Operation<'a>), |
| Login(LoginOptions<'a>), |
| Logout, |
| #[serde(other)] |
| Unknown, |
| } |
| |
| impl<'a> Display for Action<'a> { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| match self { |
| Action::Get(_) => f.write_str("get"), |
| Action::Login(_) => f.write_str("login"), |
| Action::Logout => f.write_str("logout"), |
| Action::Unknown => f.write_str("<unknown>"), |
| } |
| } |
| } |
| |
| #[derive(Serialize, Deserialize, Clone, Debug)] |
| #[serde(rename_all = "kebab-case")] |
| pub struct LoginOptions<'a> { |
| /// Token passed on the command line via --token or from stdin |
| pub token: Option<Secret<&'a str>>, |
| /// Optional URL that the user can visit to log in to the registry |
| 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)] |
| #[non_exhaustive] |
| #[serde(tag = "operation", rename_all = "kebab-case")] |
| pub enum Operation<'a> { |
| /// The user is attempting to fetch a crate. |
| Read, |
| /// The user is attempting to publish a crate. |
| Publish { |
| /// The name of the crate |
| name: &'a str, |
| /// The version of the crate |
| vers: &'a str, |
| /// The checksum of the crate file being uploaded |
| cksum: &'a str, |
| }, |
| /// The user is attempting to yank a crate. |
| Yank { |
| /// The name of the crate |
| name: &'a str, |
| /// The version of the crate |
| vers: &'a str, |
| }, |
| /// The user is attempting to unyank a crate. |
| Unyank { |
| /// The name of the crate |
| name: &'a str, |
| /// The version of the crate |
| vers: &'a str, |
| }, |
| /// The user is attempting to modify the owners of a crate. |
| Owners { |
| /// The name of the crate |
| name: &'a str, |
| }, |
| #[serde(other)] |
| Unknown, |
| } |
| |
| /// Message sent by the credential helper |
| #[derive(Serialize, Deserialize, Clone, Debug)] |
| #[serde(tag = "kind", rename_all = "kebab-case")] |
| #[non_exhaustive] |
| pub enum CredentialResponse { |
| Get { |
| token: Secret<String>, |
| cache: CacheControl, |
| operation_independent: bool, |
| }, |
| Login, |
| Logout, |
| #[serde(other)] |
| Unknown, |
| } |
| |
| #[derive(Serialize, Deserialize, Clone, Debug)] |
| #[serde(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), |
| /// 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. |
| pub const PROTOCOL_VERSION_1: u32 = 1; |
| pub trait Credential { |
| /// Retrieves a token for the given registry. |
| fn perform( |
| &self, |
| registry: &RegistryInfo, |
| action: &Action, |
| args: &[&str], |
| ) -> Result<CredentialResponse, Error>; |
| } |
| |
| /// Runs the credential interaction |
| pub fn main(credential: impl Credential) { |
| let result = doit(credential).map_err(|e| Error::Other(e)); |
| if result.is_err() { |
| serde_json::to_writer(std::io::stdout(), &result) |
| .expect("failed to serialize credential provider error"); |
| println!(); |
| } |
| } |
| |
| fn doit( |
| credential: impl Credential, |
| ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { |
| let hello = CredentialHello { |
| v: vec![PROTOCOL_VERSION_1], |
| }; |
| serde_json::to_writer(std::io::stdout(), &hello)?; |
| println!(); |
| |
| loop { |
| let mut buffer = String::new(); |
| let len = std::io::stdin().read_line(&mut buffer)?; |
| 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 response = stdin_stdout_to_console(|| { |
| credential.perform(&request.registry, &request.action, &request.args) |
| })?; |
| |
| serde_json::to_writer(std::io::stdout(), &response)?; |
| println!(); |
| } |
| } |
| |
| /// Read a line of text from stdin. |
| pub fn read_line() -> Result<String, io::Error> { |
| let mut buf = String::new(); |
| io::stdin().read_line(&mut buf)?; |
| Ok(buf.trim().to_string()) |
| } |
| |
| /// Prompt the user for a token. |
| pub fn read_token( |
| login_options: &LoginOptions, |
| registry: &RegistryInfo, |
| ) -> Result<Secret<String>, Error> { |
| if let Some(token) = &login_options.token { |
| return Ok(token.to_owned()); |
| } |
| |
| if let Some(url) = login_options.login_url { |
| eprintln!("please paste the token found on {url} below"); |
| } else if let Some(name) = registry.name { |
| eprintln!("please paste the token for {name} below"); |
| } else { |
| eprintln!("please paste the token for {} below", registry.index_url); |
| } |
| |
| Ok(Secret::from(read_line().map_err(Box::new)?)) |
| } |