Crate cargo_credential

source ·
Expand description

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:

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

//! Example credential provider that stores credentials in a JSON file.
//! This is not secure

use cargo_credential::{
    Action, CacheControl, Credential, CredentialResponse, RegistryInfo, Secret,
};
use std::{collections::HashMap, fs::File, io::ErrorKind};
type Error = Box<dyn std::error::Error + Send + Sync + 'static>;

struct FileCredential;

impl Credential for FileCredential {
    fn perform(
        &self,
        registry: &RegistryInfo<'_>,
        action: &Action<'_>,
        _args: &[&str],
    ) -> Result<CredentialResponse, cargo_credential::Error> {
        if registry.index_url != "https://github.com/rust-lang/crates.io-index" {
            // Restrict this provider to only work for crates.io. Cargo will skip it and attempt
            // another provider for any other registry.
            //
            // If a provider supports any registry, then this check should be omitted.
            return Err(cargo_credential::Error::UrlNotSupported);
        }

        // `Error::Other` takes a boxed `std::error::Error` type that causes Cargo to show the error.
        let mut creds = FileCredential::read().map_err(cargo_credential::Error::Other)?;

        match action {
            Action::Get(_) => {
                // Cargo requested a token, look it up.
                if let Some(token) = creds.get(registry.index_url) {
                    Ok(CredentialResponse::Get {
                        token: token.clone(),
                        cache: CacheControl::Session,
                        operation_independent: true,
                    })
                } else {
                    // Credential providers should respond with `NotFound` when a credential can not be
                    // found, allowing Cargo to attempt another provider.
                    Err(cargo_credential::Error::NotFound)
                }
            }
            Action::Login(login_options) => {
                // The token for `cargo login` can come from the `login_options` parameter or i
                // interactively reading from stdin.
                //
                // `cargo_credential::read_token` automatically handles this.
                let token = cargo_credential::read_token(login_options, registry)?;
                creds.insert(registry.index_url.to_string(), token);

                FileCredential::write(&creds).map_err(cargo_credential::Error::Other)?;

                // Credentials were successfully stored.
                Ok(CredentialResponse::Login)
            }
            Action::Logout => {
                if creds.remove(registry.index_url).is_none() {
                    // If the user attempts to log out from a registry that has no credentials
                    // stored, then NotFound is the appropriate error.
                    Err(cargo_credential::Error::NotFound)
                } else {
                    // Credentials were successfully erased.
                    Ok(CredentialResponse::Logout)
                }
            }
            // If a credential provider doesn't support a given operation, it should respond with `OperationNotSupported`.
            _ => Err(cargo_credential::Error::OperationNotSupported),
        }
    }
}

impl FileCredential {
    fn read() -> Result<HashMap<String, Secret<String>>, Error> {
        match File::open("cargo-credentials.json") {
            Ok(f) => Ok(serde_json::from_reader(f)?),
            Err(e) if e.kind() == ErrorKind::NotFound => Ok(HashMap::new()),
            Err(e) => Err(e)?,
        }
    }
    fn write(value: &HashMap<String, Secret<String>>) -> Result<(), Error> {
        let file = File::create("cargo-credentials.json")?;
        Ok(serde_json::to_writer_pretty(file, value)?)
    }
}

fn main() {
    cargo_credential::main(FileCredential);
}

Modules

Structs

Enums

Constants

  • 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.

Traits

Functions