Add more docs and example for cargo-credential

This commit is contained in:
Arlo Siemsen 2023-08-02 17:25:30 -05:00
parent abc115972e
commit af95711ae5
5 changed files with 164 additions and 1 deletions

14
Cargo.lock generated
View File

@ -339,6 +339,7 @@ dependencies = [
"anyhow",
"serde",
"serde_json",
"snapbox",
"thiserror",
"time",
]
@ -867,6 +868,18 @@ dependencies = [
"libc",
]
[[package]]
name = "escargot"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "768064bd3a0e2bedcba91dc87ace90beea91acc41b6a01a3ca8e9aa8827461bf"
dependencies = [
"log",
"once_cell",
"serde",
"serde_json",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@ -3003,6 +3016,7 @@ dependencies = [
"anstyle",
"content_inspector",
"dunce",
"escargot",
"filetime",
"normalize-line-endings",
"similar",

View File

@ -12,3 +12,6 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
thiserror.workspace = true
time.workspace = true
[dev-dependencies]
snapbox = { workspace = true, features = ["examples"] }

View File

@ -0,0 +1,90 @@
//! 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);
}

View File

@ -1,4 +1,4 @@
//! Helper library for writing Cargo credential processes.
//! 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:
@ -8,6 +8,34 @@
//! 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::{

View File

@ -0,0 +1,28 @@
use std::path::Path;
use snapbox::cmd::Command;
#[test]
fn file_provider() {
let bin = snapbox::cmd::compile_example("file-provider", []).unwrap();
let hello = r#"{"v":[1]}"#;
let login_request = r#"{"v": 1,"registry": {"index-url":"https://github.com/rust-lang/crates.io-index","name":"crates-io"},"kind": "login","token": "s3krit","args": []}"#;
let login_response = r#"{"Ok":{"kind":"login"}}"#;
let get_request = r#"{"v": 1,"registry": {"index-url":"https://github.com/rust-lang/crates.io-index","name":"crates-io"},"kind": "get","operation": "read","args": []}"#;
let get_response =
r#"{"Ok":{"kind":"get","token":"s3krit","cache":"session","operation_independent":true}}"#;
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cargo-credential-tests");
std::fs::create_dir(&dir).unwrap();
Command::new(bin)
.current_dir(&dir)
.stdin(format!("{login_request}\n{get_request}\n"))
.arg("--cargo-plugin")
.assert()
.stdout_eq(format!("{hello}\n{login_response}\n{get_response}\n"))
.stderr_eq("")
.success();
std::fs::remove_dir_all(&dir).unwrap();
}