Skip to content

Commit

Permalink
Add more docs and example for cargo-credential
Browse files Browse the repository at this point in the history
  • Loading branch information
arlosi committed Aug 7, 2023
1 parent abc1159 commit 5847b42
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 1 deletion.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions credential/cargo-credential/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
thiserror.workspace = true
time.workspace = true

[dev-dependencies]
snapbox = { workspace = true, features = ["examples"] }
tempfile.workspace = true
87 changes: 87 additions & 0 deletions credential/cargo-credential/examples/file-provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! 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};
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> {
let mut creds = FileCredential::read().unwrap_or_default();
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);
}

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 could 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);

// `Error::Other` takes a boxed `std::error::Error` type that causes Cargo to
// show the error.
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> {
let file = File::open("cargo-credentials.json")?;
Ok(serde_json::from_reader(file)?)
}
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);
}
30 changes: 29 additions & 1 deletion credential/cargo-credential/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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::{
Expand Down
23 changes: 23 additions & 0 deletions credential/cargo-credential/tests/examples.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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}}"#;

Command::new(bin)
.current_dir(env!("CARGO_TARGET_TMPDIR"))
.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();
}

0 comments on commit 5847b42

Please sign in to comment.