Skip to content

Commit 27e73b3

Browse files
committed
Auto merge of #12461 - arlosi:cred-doc, r=epage
docs: add example for cargo-credential Add additional docs and example for `cargo-credential`. r? `@epage`
2 parents d432dec + af95711 commit 27e73b3

File tree

5 files changed

+164
-1
lines changed

5 files changed

+164
-1
lines changed

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

credential/cargo-credential/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ serde = { workspace = true, features = ["derive"] }
1212
serde_json.workspace = true
1313
thiserror.workspace = true
1414
time.workspace = true
15+
16+
[dev-dependencies]
17+
snapbox = { workspace = true, features = ["examples"] }
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//! Example credential provider that stores credentials in a JSON file.
2+
//! This is not secure
3+
4+
use cargo_credential::{
5+
Action, CacheControl, Credential, CredentialResponse, RegistryInfo, Secret,
6+
};
7+
use std::{collections::HashMap, fs::File, io::ErrorKind};
8+
type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
9+
10+
struct FileCredential;
11+
12+
impl Credential for FileCredential {
13+
fn perform(
14+
&self,
15+
registry: &RegistryInfo,
16+
action: &Action,
17+
_args: &[&str],
18+
) -> Result<CredentialResponse, cargo_credential::Error> {
19+
if registry.index_url != "https://github.com/rust-lang/crates.io-index" {
20+
// Restrict this provider to only work for crates.io. Cargo will skip it and attempt
21+
// another provider for any other registry.
22+
//
23+
// If a provider supports any registry, then this check should be omitted.
24+
return Err(cargo_credential::Error::UrlNotSupported);
25+
}
26+
27+
// `Error::Other` takes a boxed `std::error::Error` type that causes Cargo to show the error.
28+
let mut creds = FileCredential::read().map_err(cargo_credential::Error::Other)?;
29+
30+
match action {
31+
Action::Get(_) => {
32+
// Cargo requested a token, look it up.
33+
if let Some(token) = creds.get(registry.index_url) {
34+
Ok(CredentialResponse::Get {
35+
token: token.clone(),
36+
cache: CacheControl::Session,
37+
operation_independent: true,
38+
})
39+
} else {
40+
// Credential providers should respond with `NotFound` when a credential can not be
41+
// found, allowing Cargo to attempt another provider.
42+
Err(cargo_credential::Error::NotFound)
43+
}
44+
}
45+
Action::Login(login_options) => {
46+
// The token for `cargo login` can come from the `login_options` parameter or i
47+
// interactively reading from stdin.
48+
//
49+
// `cargo_credential::read_token` automatically handles this.
50+
let token = cargo_credential::read_token(login_options, registry)?;
51+
creds.insert(registry.index_url.to_string(), token);
52+
53+
FileCredential::write(&creds).map_err(cargo_credential::Error::Other)?;
54+
55+
// Credentials were successfully stored.
56+
Ok(CredentialResponse::Login)
57+
}
58+
Action::Logout => {
59+
if creds.remove(registry.index_url).is_none() {
60+
// If the user attempts to log out from a registry that has no credentials
61+
// stored, then NotFound is the appropriate error.
62+
Err(cargo_credential::Error::NotFound)
63+
} else {
64+
// Credentials were successfully erased.
65+
Ok(CredentialResponse::Logout)
66+
}
67+
}
68+
// If a credential provider doesn't support a given operation, it should respond with `OperationNotSupported`.
69+
_ => Err(cargo_credential::Error::OperationNotSupported),
70+
}
71+
}
72+
}
73+
74+
impl FileCredential {
75+
fn read() -> Result<HashMap<String, Secret<String>>, Error> {
76+
match File::open("cargo-credentials.json") {
77+
Ok(f) => Ok(serde_json::from_reader(f)?),
78+
Err(e) if e.kind() == ErrorKind::NotFound => Ok(HashMap::new()),
79+
Err(e) => Err(e)?,
80+
}
81+
}
82+
fn write(value: &HashMap<String, Secret<String>>) -> Result<(), Error> {
83+
let file = File::create("cargo-credentials.json")?;
84+
Ok(serde_json::to_writer_pretty(file, value)?)
85+
}
86+
}
87+
88+
fn main() {
89+
cargo_credential::main(FileCredential);
90+
}

credential/cargo-credential/src/lib.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Helper library for writing Cargo credential processes.
1+
//! Helper library for writing Cargo credential providers.
22
//!
33
//! A credential process should have a `struct` that implements the `Credential` trait.
44
//! The `main` function should be called with an instance of that struct, such as:
@@ -8,6 +8,34 @@
88
//! cargo_credential::main(MyCredential);
99
//! }
1010
//! ```
11+
//!
12+
//! While in the `perform` function, stdin and stdout will be re-attached to the
13+
//! active console. This allows credential providers to be interactive if necessary.
14+
//!
15+
//! ## Error handling
16+
//! ### [`Error::UrlNotSupported`]
17+
//! A credential provider may only support some registry URLs. If this is the case
18+
//! and an unsupported index URL is passed to the provider, it should respond with
19+
//! [`Error::UrlNotSupported`]. Other credential providers may be attempted by Cargo.
20+
//!
21+
//! ### [`Error::NotFound`]
22+
//! When attempting an [`Action::Get`] or [`Action::Logout`], if a credential can not
23+
//! be found, the provider should respond with [`Error::NotFound`]. Other credential
24+
//! providers may be attempted by Cargo.
25+
//!
26+
//! ### [`Error::OperationNotSupported`]
27+
//! A credential provider might not support all operations. For example if the provider
28+
//! only supports [`Action::Get`], [`Error::OperationNotSupported`] should be returned
29+
//! for all other requests.
30+
//!
31+
//! ### [`Error::Other`]
32+
//! All other errors go here. The error will be shown to the user in Cargo, including
33+
//! the full error chain using [`std::error::Error::source`].
34+
//!
35+
//! ## Example
36+
//! ```rust,ignore
37+
#![doc = include_str!("../examples/file-provider.rs")]
38+
//! ```
1139
1240
use serde::{Deserialize, Serialize};
1341
use std::{
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use std::path::Path;
2+
3+
use snapbox::cmd::Command;
4+
5+
#[test]
6+
fn file_provider() {
7+
let bin = snapbox::cmd::compile_example("file-provider", []).unwrap();
8+
9+
let hello = r#"{"v":[1]}"#;
10+
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": []}"#;
11+
let login_response = r#"{"Ok":{"kind":"login"}}"#;
12+
13+
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": []}"#;
14+
let get_response =
15+
r#"{"Ok":{"kind":"get","token":"s3krit","cache":"session","operation_independent":true}}"#;
16+
17+
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cargo-credential-tests");
18+
std::fs::create_dir(&dir).unwrap();
19+
Command::new(bin)
20+
.current_dir(&dir)
21+
.stdin(format!("{login_request}\n{get_request}\n"))
22+
.arg("--cargo-plugin")
23+
.assert()
24+
.stdout_eq(format!("{hello}\n{login_response}\n{get_response}\n"))
25+
.stderr_eq("")
26+
.success();
27+
std::fs::remove_dir_all(&dir).unwrap();
28+
}

0 commit comments

Comments
 (0)