mirror of
https://github.com/rust-lang/cargo
synced 2024-09-13 21:11:44 +00:00
Auto merge of #12424 - arlosi:credential-thiserror, r=arlosi
Use thiserror for credential provider errors ### What does this PR try to resolve? Errors from credential providers currently must a single string. This leads to a lot of `.map_err(|e|cargo_credential::Error::Other(e.to_string())`, which loses the `source()` of these errors. This changes the `cargo_credential::Error` to use `thiserror` and adds a custom serialization for `std::error::Error` that preserves the source error chain across serialization / deserialization. A unit test is added to verify serialization / deserialization.
This commit is contained in:
commit
29a6f2fab2
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -347,8 +347,10 @@ dependencies = [
|
|||
name = "cargo-credential"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"time",
|
||||
]
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ impl OnePasswordKeychain {
|
|||
let mut cmd = Command::new("op");
|
||||
cmd.args(["signin", "--raw"]);
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stdin(cargo_credential::tty()?);
|
||||
cmd.stdin(cargo_credential::tty().map_err(Box::new)?);
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("failed to spawn `op`: {}", e))?;
|
||||
|
@ -228,7 +228,7 @@ impl OnePasswordKeychain {
|
|||
// For unknown reasons, `op item create` seems to not be happy if
|
||||
// stdin is not a tty. Otherwise it returns with a 0 exit code without
|
||||
// doing anything.
|
||||
cmd.stdin(cargo_credential::tty()?);
|
||||
cmd.stdin(cargo_credential::tty().map_err(Box::new)?);
|
||||
self.run_cmd(cmd)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -243,7 +243,7 @@ impl OnePasswordKeychain {
|
|||
Some(password) => password
|
||||
.value
|
||||
.map(Secret::from)
|
||||
.ok_or_else(|| format!("missing password value for entry").into()),
|
||||
.ok_or("missing password value for entry".into()),
|
||||
None => Err("could not find password field".into()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,6 @@ mod macos {
|
|||
format!("cargo-registry:{}", index_url)
|
||||
}
|
||||
|
||||
fn to_credential_error(e: security_framework::base::Error) -> Error {
|
||||
Error::Other(format!("security framework ({}): {e}", e.code()))
|
||||
}
|
||||
|
||||
impl Credential for MacKeychain {
|
||||
fn perform(
|
||||
&self,
|
||||
|
@ -34,11 +30,9 @@ mod macos {
|
|||
match action {
|
||||
Action::Get(_) => match keychain.find_generic_password(&service_name, ACCOUNT) {
|
||||
Err(e) if e.code() == not_found => Err(Error::NotFound),
|
||||
Err(e) => Err(to_credential_error(e)),
|
||||
Err(e) => Err(Box::new(e).into()),
|
||||
Ok((pass, _)) => {
|
||||
let token = String::from_utf8(pass.as_ref().to_vec()).map_err(|_| {
|
||||
Error::Other("failed to convert token to UTF8".to_string())
|
||||
})?;
|
||||
let token = String::from_utf8(pass.as_ref().to_vec()).map_err(Box::new)?;
|
||||
Ok(CredentialResponse::Get {
|
||||
token: token.into(),
|
||||
cache: CacheControl::Session,
|
||||
|
@ -57,19 +51,19 @@ mod macos {
|
|||
ACCOUNT,
|
||||
token.expose().as_bytes(),
|
||||
)
|
||||
.map_err(to_credential_error)?;
|
||||
.map_err(Box::new)?;
|
||||
}
|
||||
}
|
||||
Ok((_, mut item)) => {
|
||||
item.set_password(token.expose().as_bytes())
|
||||
.map_err(to_credential_error)?;
|
||||
.map_err(Box::new)?;
|
||||
}
|
||||
}
|
||||
Ok(CredentialResponse::Login)
|
||||
}
|
||||
Action::Logout => match keychain.find_generic_password(&service_name, ACCOUNT) {
|
||||
Err(e) if e.code() == not_found => Err(Error::NotFound),
|
||||
Err(e) => Err(to_credential_error(e)),
|
||||
Err(e) => Err(Box::new(e).into()),
|
||||
Ok((_, item)) => {
|
||||
item.delete();
|
||||
Ok(CredentialResponse::Logout)
|
||||
|
|
|
@ -58,23 +58,20 @@ mod win {
|
|||
if err.raw_os_error() == Some(ERROR_NOT_FOUND as i32) {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
return Err(err.into());
|
||||
return Err(Box::new(err).into());
|
||||
}
|
||||
std::slice::from_raw_parts(
|
||||
(*p_credential).CredentialBlob,
|
||||
(*p_credential).CredentialBlobSize as usize,
|
||||
)
|
||||
};
|
||||
let result = match String::from_utf8(bytes.to_vec()) {
|
||||
Err(_) => Err("failed to convert token to UTF8".into()),
|
||||
Ok(token) => Ok(CredentialResponse::Get {
|
||||
token: token.into(),
|
||||
cache: CacheControl::Session,
|
||||
operation_independent: true,
|
||||
}),
|
||||
};
|
||||
let _ = unsafe { CredFree(p_credential as *mut _) };
|
||||
result
|
||||
let token = String::from_utf8(bytes.to_vec()).map_err(Box::new);
|
||||
unsafe { CredFree(p_credential as *mut _) };
|
||||
Ok(CredentialResponse::Get {
|
||||
token: token?.into(),
|
||||
cache: CacheControl::Session,
|
||||
operation_independent: true,
|
||||
})
|
||||
}
|
||||
Action::Login(options) => {
|
||||
let token = read_token(options, registry)?.expose();
|
||||
|
@ -100,7 +97,7 @@ mod win {
|
|||
let result = unsafe { CredWriteW(&credential, 0) };
|
||||
if result != TRUE {
|
||||
let err = std::io::Error::last_os_error();
|
||||
return Err(err.into());
|
||||
return Err(Box::new(err).into());
|
||||
}
|
||||
Ok(CredentialResponse::Login)
|
||||
}
|
||||
|
@ -112,7 +109,7 @@ mod win {
|
|||
if err.raw_os_error() == Some(ERROR_NOT_FOUND as i32) {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
return Err(err.into());
|
||||
return Err(Box::new(err).into());
|
||||
}
|
||||
Ok(CredentialResponse::Logout)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ repository = "https://github.com/rust-lang/cargo"
|
|||
description = "A library to assist writing Cargo credential helpers."
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
|
|
206
credential/cargo-credential/src/error.rs
Normal file
206
credential/cargo-credential/src/error.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error as StdError;
|
||||
use thiserror::Error as ThisError;
|
||||
|
||||
/// Credential provider error type.
|
||||
///
|
||||
/// `UrlNotSupported` and `NotFound` errors both cause Cargo
|
||||
/// to attempt another provider, if one is available. The other
|
||||
/// variants are fatal.
|
||||
///
|
||||
/// Note: Do not add a tuple variant, as it cannot be serialized.
|
||||
#[derive(Serialize, Deserialize, ThisError, Debug)]
|
||||
#[serde(rename_all = "kebab-case", tag = "kind")]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Registry URL is not supported. This should be used if
|
||||
/// the provider only works for some registries. Cargo will
|
||||
/// try another provider, if available
|
||||
#[error("registry not supported")]
|
||||
UrlNotSupported,
|
||||
|
||||
/// Credentials could not be found. Cargo will try another
|
||||
/// provider, if available
|
||||
#[error("credential not found")]
|
||||
NotFound,
|
||||
|
||||
/// The provider doesn't support this operation, such as
|
||||
/// a provider that can't support 'login' / 'logout'
|
||||
#[error("requested operation not supported")]
|
||||
OperationNotSupported,
|
||||
|
||||
/// The provider failed to perform the operation. Other
|
||||
/// providers will not be attempted
|
||||
#[error(transparent)]
|
||||
#[serde(with = "error_serialize")]
|
||||
Other(Box<dyn StdError + Sync + Send>),
|
||||
|
||||
/// A new variant was added to this enum since Cargo was built
|
||||
#[error("unknown error kind; try updating Cargo?")]
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(err: String) -> Self {
|
||||
Box::new(StringTypedError {
|
||||
message: err.to_string(),
|
||||
source: None,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
fn from(err: &str) -> Self {
|
||||
err.to_string().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
let mut prev = None;
|
||||
for e in value.chain().rev() {
|
||||
prev = Some(Box::new(StringTypedError {
|
||||
message: e.to_string(),
|
||||
source: prev,
|
||||
}));
|
||||
}
|
||||
Error::Other(prev.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: StdError + Send + Sync + 'static> From<Box<T>> for Error {
|
||||
fn from(value: Box<T>) -> Self {
|
||||
Error::Other(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// String-based error type with an optional source
|
||||
#[derive(Debug)]
|
||||
struct StringTypedError {
|
||||
message: String,
|
||||
source: Option<Box<StringTypedError>>,
|
||||
}
|
||||
|
||||
impl StdError for StringTypedError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
self.source.as_ref().map(|err| err as &dyn StdError)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StringTypedError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.message.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializer / deserializer for any boxed error.
|
||||
/// The string representation of the error, and its `source` chain can roundtrip across
|
||||
/// the serialization. The actual types are lost (downcast will not work).
|
||||
mod error_serialize {
|
||||
use std::error::Error as StdError;
|
||||
use std::ops::Deref;
|
||||
|
||||
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serializer};
|
||||
|
||||
use crate::error::StringTypedError;
|
||||
|
||||
pub fn serialize<S>(
|
||||
e: &Box<dyn StdError + Send + Sync>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("StringTypedError", 2)?;
|
||||
state.serialize_field("message", &format!("{}", e))?;
|
||||
|
||||
// Serialize the source error chain recursively
|
||||
let mut current_source: &dyn StdError = e.deref();
|
||||
let mut sources = Vec::new();
|
||||
while let Some(err) = current_source.source() {
|
||||
sources.push(err.to_string());
|
||||
current_source = err;
|
||||
}
|
||||
state.serialize_field("caused-by", &sources)?;
|
||||
state.end()
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<dyn StdError + Sync + Send>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct ErrorData {
|
||||
message: String,
|
||||
caused_by: Option<Vec<String>>,
|
||||
}
|
||||
let data = ErrorData::deserialize(deserializer)?;
|
||||
let mut prev = None;
|
||||
if let Some(source) = data.caused_by {
|
||||
for e in source.into_iter().rev() {
|
||||
prev = Some(Box::new(StringTypedError {
|
||||
message: e,
|
||||
source: prev,
|
||||
}));
|
||||
}
|
||||
}
|
||||
let e = Box::new(StringTypedError {
|
||||
message: data.message,
|
||||
source: prev,
|
||||
});
|
||||
Ok(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Error;
|
||||
|
||||
#[test]
|
||||
pub fn unknown_kind() {
|
||||
let json = r#"{
|
||||
"kind": "unexpected-kind",
|
||||
"unexpected-content": "test"
|
||||
}"#;
|
||||
let e: Error = serde_json::from_str(&json).unwrap();
|
||||
assert!(matches!(e, Error::Unknown));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn roundtrip() {
|
||||
// Construct an error with context
|
||||
let e = anyhow::anyhow!("E1").context("E2").context("E3");
|
||||
// Convert to a string with contexts.
|
||||
let s1 = format!("{:?}", e);
|
||||
// Convert the error into an `Error`
|
||||
let e: Error = e.into();
|
||||
// Convert that error into JSON
|
||||
let json = serde_json::to_string_pretty(&e).unwrap();
|
||||
// Convert that error back to anyhow
|
||||
let e: anyhow::Error = e.into();
|
||||
let s2 = format!("{:?}", e);
|
||||
assert_eq!(s1, s2);
|
||||
|
||||
// Convert the error back from JSON
|
||||
let e: Error = serde_json::from_str(&json).unwrap();
|
||||
// Convert to back to anyhow
|
||||
let e: anyhow::Error = e.into();
|
||||
let s3 = format!("{:?}", e);
|
||||
assert_eq!(s2, s3);
|
||||
|
||||
assert_eq!(
|
||||
r#"{
|
||||
"kind": "other",
|
||||
"message": "E3",
|
||||
"caused-by": [
|
||||
"E2",
|
||||
"E1"
|
||||
]
|
||||
}"#,
|
||||
json
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,9 @@ use std::{
|
|||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
mod error;
|
||||
mod secret;
|
||||
pub use error::Error;
|
||||
pub use secret::Secret;
|
||||
|
||||
/// Message sent by the credential helper on startup
|
||||
|
@ -63,7 +65,7 @@ pub struct RegistryInfo<'a> {
|
|||
/// 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")]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub headers: Vec<String>,
|
||||
}
|
||||
|
||||
|
@ -75,6 +77,8 @@ pub enum Action<'a> {
|
|||
Get(Operation<'a>),
|
||||
Login(LoginOptions<'a>),
|
||||
Logout,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl<'a> Display for Action<'a> {
|
||||
|
@ -83,6 +87,7 @@ impl<'a> Display for Action<'a> {
|
|||
Action::Get(_) => f.write_str("get"),
|
||||
Action::Login(_) => f.write_str("login"),
|
||||
Action::Logout => f.write_str("logout"),
|
||||
Action::Unknown => f.write_str("<unknown>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,6 +136,8 @@ pub enum Operation<'a> {
|
|||
/// The name of the crate
|
||||
name: &'a str,
|
||||
},
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Message sent by the credential helper
|
||||
|
@ -145,6 +152,8 @@ pub enum CredentialResponse {
|
|||
},
|
||||
Login,
|
||||
Logout,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
|
@ -157,76 +166,14 @@ pub enum CacheControl {
|
|||
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;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case", tag = "kind", content = "detail")]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
UrlNotSupported,
|
||||
ProtocolNotSupported(u32),
|
||||
Subprocess(String),
|
||||
Io(String),
|
||||
Serde(String),
|
||||
Other(String),
|
||||
OperationNotSupported,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
Error::Serde(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Error::Io(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(err: String) -> Self {
|
||||
Error::Other(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
fn from(err: &str) -> Self {
|
||||
Error::Other(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::UrlNotSupported => {
|
||||
write!(f, "credential provider does not support this registry")
|
||||
}
|
||||
Error::ProtocolNotSupported(v) => write!(
|
||||
f,
|
||||
"credential provider does not support protocol version {v}"
|
||||
),
|
||||
Error::Io(msg) => write!(f, "i/o error: {msg}"),
|
||||
Error::Serde(msg) => write!(f, "serialization error: {msg}"),
|
||||
Error::Other(msg) => write!(f, "error: {msg}"),
|
||||
Error::Subprocess(msg) => write!(f, "subprocess failed: {msg}"),
|
||||
Error::OperationNotSupported => write!(
|
||||
f,
|
||||
"credential provider does not support the requested operation"
|
||||
),
|
||||
Error::NotFound => write!(f, "credential not found"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Credential {
|
||||
/// Retrieves a token for the given registry.
|
||||
fn perform(
|
||||
|
@ -239,7 +186,7 @@ pub trait Credential {
|
|||
|
||||
/// Runs the credential interaction
|
||||
pub fn main(credential: impl Credential) {
|
||||
let result = doit(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");
|
||||
|
@ -247,7 +194,9 @@ pub fn main(credential: impl Credential) {
|
|||
}
|
||||
}
|
||||
|
||||
fn doit(credential: impl Credential) -> Result<(), Error> {
|
||||
fn doit(
|
||||
credential: impl Credential,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let hello = CredentialHello {
|
||||
v: vec![PROTOCOL_VERSION_1],
|
||||
};
|
||||
|
@ -262,7 +211,7 @@ fn doit(credential: impl Credential) -> Result<(), Error> {
|
|||
}
|
||||
let request: CredentialRequest = serde_json::from_str(&buffer)?;
|
||||
if request.v != PROTOCOL_VERSION_1 {
|
||||
return Err(Error::ProtocolNotSupported(request.v));
|
||||
return Err(format!("unsupported protocol version {}", request.v).into());
|
||||
}
|
||||
serde_json::to_writer(
|
||||
std::io::stdout(),
|
||||
|
@ -310,5 +259,5 @@ pub fn read_token(
|
|||
eprintln!("please paste the token for {} below", registry.index_url);
|
||||
}
|
||||
|
||||
Ok(Secret::from(read_line()?))
|
||||
Ok(Secret::from(read_line().map_err(Box::new)?))
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::{
|
|||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use cargo_credential::{
|
||||
Action, CacheControl, Credential, CredentialResponse, RegistryInfo, Secret,
|
||||
};
|
||||
|
@ -22,7 +23,7 @@ impl Credential for BasicProcessCredential {
|
|||
Action::Get(_) => {
|
||||
let mut args = args.iter();
|
||||
let exe = args.next()
|
||||
.ok_or_else(||cargo_credential::Error::Other(format!("The first argument to the `cargo:basic` adaptor must be the path to the credential provider executable.")))?;
|
||||
.ok_or("The first argument to the `cargo:basic` adaptor must be the path to the credential provider executable.")?;
|
||||
let args = args.map(|arg| arg.replace("{index_url}", registry.index_url));
|
||||
|
||||
let mut cmd = Command::new(exe);
|
||||
|
@ -32,32 +33,28 @@ impl Credential for BasicProcessCredential {
|
|||
cmd.env("CARGO_REGISTRY_NAME_OPT", name);
|
||||
}
|
||||
cmd.stdout(Stdio::piped());
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| cargo_credential::Error::Subprocess(e.to_string()))?;
|
||||
let mut child = cmd.spawn().context("failed to spawn credential process")?;
|
||||
let mut buffer = String::new();
|
||||
child
|
||||
.stdout
|
||||
.take()
|
||||
.unwrap()
|
||||
.read_to_string(&mut buffer)
|
||||
.map_err(|e| cargo_credential::Error::Subprocess(e.to_string()))?;
|
||||
.context("failed to read from credential provider")?;
|
||||
if let Some(end) = buffer.find('\n') {
|
||||
if buffer.len() > end + 1 {
|
||||
return Err(cargo_credential::Error::Other(format!(
|
||||
return Err(format!(
|
||||
"process `{}` returned more than one line of output; \
|
||||
expected a single token",
|
||||
exe
|
||||
)));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
buffer.truncate(end);
|
||||
}
|
||||
let status = child.wait().expect("process was started");
|
||||
let status = child.wait().context("credential process never started")?;
|
||||
if !status.success() {
|
||||
return Err(cargo_credential::Error::Subprocess(format!(
|
||||
"process `{}` failed with status `{status}`",
|
||||
exe
|
||||
)));
|
||||
return Err(format!("process `{}` failed with status `{status}`", exe).into());
|
||||
}
|
||||
Ok(CredentialResponse::Get {
|
||||
token: Secret::from(buffer),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Credential provider that implements PASETO asymmetric tokens stored in Cargo's config.
|
||||
|
||||
use anyhow::Context;
|
||||
use cargo_credential::{
|
||||
Action, CacheControl, Credential, CredentialResponse, Error, Operation, RegistryInfo, Secret,
|
||||
};
|
||||
|
@ -61,16 +62,14 @@ impl<'a> Credential for PasetoCredential<'a> {
|
|||
action: &Action<'_>,
|
||||
_args: &[&str],
|
||||
) -> Result<CredentialResponse, Error> {
|
||||
let index_url = Url::parse(registry.index_url).map_err(|e| e.to_string())?;
|
||||
let index_url = Url::parse(registry.index_url).context("parsing index url")?;
|
||||
let sid = if let Some(name) = registry.name {
|
||||
SourceId::for_alt_registry(&index_url, name)
|
||||
} else {
|
||||
SourceId::for_registry(&index_url)
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
}?;
|
||||
|
||||
let reg_cfg = registry_credential_config_raw(self.config, &sid)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
let reg_cfg = registry_credential_config_raw(self.config, &sid)?;
|
||||
|
||||
match action {
|
||||
Action::Get(operation) => {
|
||||
|
@ -87,14 +86,12 @@ impl<'a> Credential for PasetoCredential<'a> {
|
|||
.as_ref()
|
||||
.map(|key| key.as_str().try_into())
|
||||
.transpose()
|
||||
.map_err(|e| Error::Other(format!("failed to load private key: {e}")))?;
|
||||
.context("failed to load private key")?;
|
||||
let public: AsymmetricPublicKey<pasetors::version3::V3> = secret
|
||||
.as_ref()
|
||||
.map(|key| key.try_into())
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
Error::Other(format!("failed to load public key from private key: {e}"))
|
||||
})?
|
||||
.context("failed to load public key from private key")?
|
||||
.expose();
|
||||
let kip: pasetors::paserk::Id = (&public).into();
|
||||
|
||||
|
@ -157,7 +154,7 @@ impl<'a> Credential for PasetoCredential<'a> {
|
|||
)
|
||||
})
|
||||
.transpose()
|
||||
.map_err(|e| Error::Other(format!("failed to sign request: {e}")))?;
|
||||
.context("failed to sign request")?;
|
||||
|
||||
Ok(CredentialResponse::Get {
|
||||
token,
|
||||
|
@ -181,18 +178,14 @@ impl<'a> Credential for PasetoCredential<'a> {
|
|||
if let Some(p) = paserk_public_from_paserk_secret(secret_key.as_deref()) {
|
||||
eprintln!("{}", &p);
|
||||
} else {
|
||||
return Err(Error::Other(
|
||||
"not a validly formatted PASERK secret key".to_string(),
|
||||
));
|
||||
return Err("not a validly formatted PASERK secret key".into());
|
||||
}
|
||||
new_token = RegistryCredentialConfig::AsymmetricKey((secret_key, None));
|
||||
config::save_credentials(self.config, Some(new_token), &sid)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
config::save_credentials(self.config, Some(new_token), &sid)?;
|
||||
Ok(CredentialResponse::Login)
|
||||
}
|
||||
Action::Logout => {
|
||||
config::save_credentials(self.config, None, &sid)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
config::save_credentials(self.config, None, &sid)?;
|
||||
Ok(CredentialResponse::Logout)
|
||||
}
|
||||
_ => Err(Error::OperationNotSupported),
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::{
|
|||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use cargo_credential::{
|
||||
Action, Credential, CredentialHello, CredentialRequest, CredentialResponse, RegistryInfo,
|
||||
};
|
||||
|
@ -35,17 +36,15 @@ impl<'a> Credential for CredentialProcessCredential {
|
|||
cmd.stdin(Stdio::piped());
|
||||
cmd.arg("--cargo-plugin");
|
||||
log::debug!("credential-process: {cmd:?}");
|
||||
let mut child = cmd.spawn().map_err(|e| {
|
||||
cargo_credential::Error::Subprocess(format!(
|
||||
"failed to spawn credential process `{}`: {e}",
|
||||
self.path.display()
|
||||
))
|
||||
})?;
|
||||
let mut child = cmd.spawn().context("failed to spawn credential process")?;
|
||||
let mut output_from_child = BufReader::new(child.stdout.take().unwrap());
|
||||
let mut input_to_child = child.stdin.take().unwrap();
|
||||
let mut buffer = String::new();
|
||||
output_from_child.read_line(&mut buffer)?;
|
||||
let credential_hello: CredentialHello = serde_json::from_str(&buffer)?;
|
||||
output_from_child
|
||||
.read_line(&mut buffer)
|
||||
.context("failed to read hello from credential provider")?;
|
||||
let credential_hello: CredentialHello =
|
||||
serde_json::from_str(&buffer).context("failed to deserialize hello")?;
|
||||
log::debug!("credential-process > {credential_hello:?}");
|
||||
|
||||
let req = CredentialRequest {
|
||||
|
@ -54,23 +53,25 @@ impl<'a> Credential for CredentialProcessCredential {
|
|||
registry: registry.clone(),
|
||||
args: args.to_vec(),
|
||||
};
|
||||
let request = serde_json::to_string(&req)?;
|
||||
let request = serde_json::to_string(&req).context("failed to serialize request")?;
|
||||
log::debug!("credential-process < {req:?}");
|
||||
writeln!(input_to_child, "{request}")?;
|
||||
writeln!(input_to_child, "{request}").context("failed to write to credential provider")?;
|
||||
|
||||
buffer.clear();
|
||||
output_from_child.read_line(&mut buffer)?;
|
||||
output_from_child
|
||||
.read_line(&mut buffer)
|
||||
.context("failed to read response from credential provider")?;
|
||||
let response: Result<CredentialResponse, cargo_credential::Error> =
|
||||
serde_json::from_str(&buffer)?;
|
||||
serde_json::from_str(&buffer).context("failed to deserialize response")?;
|
||||
log::debug!("credential-process > {response:?}");
|
||||
drop(input_to_child);
|
||||
let status = child.wait().expect("credential process never started");
|
||||
let status = child.wait().context("credential process never started")?;
|
||||
if !status.success() {
|
||||
return Err(cargo_credential::Error::Subprocess(format!(
|
||||
return Err(anyhow::anyhow!(
|
||||
"credential process `{}` failed with status {}`",
|
||||
self.path.display(),
|
||||
status
|
||||
))
|
||||
)
|
||||
.into());
|
||||
}
|
||||
log::trace!("credential process exited successfully");
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Credential provider that uses plaintext tokens in Cargo's config.
|
||||
|
||||
use anyhow::Context;
|
||||
use cargo_credential::{Action, CacheControl, Credential, CredentialResponse, Error, RegistryInfo};
|
||||
use url::Url;
|
||||
|
||||
|
@ -27,16 +28,14 @@ impl<'a> Credential for TokenCredential<'a> {
|
|||
action: &Action<'_>,
|
||||
_args: &[&str],
|
||||
) -> Result<CredentialResponse, Error> {
|
||||
let index_url = Url::parse(registry.index_url).map_err(|e| e.to_string())?;
|
||||
let index_url = Url::parse(registry.index_url).context("parsing index url")?;
|
||||
let sid = if let Some(name) = registry.name {
|
||||
SourceId::for_alt_registry(&index_url, name)
|
||||
} else {
|
||||
SourceId::for_registry(&index_url)
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
let previous_token = registry_credential_config_raw(self.config, &sid)
|
||||
.map_err(|e| Error::Other(e.to_string()))?
|
||||
.and_then(|c| c.token);
|
||||
}?;
|
||||
let previous_token =
|
||||
registry_credential_config_raw(self.config, &sid)?.and_then(|c| c.token);
|
||||
|
||||
match action {
|
||||
Action::Get(_) => {
|
||||
|
@ -53,14 +52,12 @@ impl<'a> Credential for TokenCredential<'a> {
|
|||
let new_token = cargo_credential::read_token(options, registry)?
|
||||
.map(|line| line.replace("cargo login", "").trim().to_string());
|
||||
|
||||
crates_io::check_token(new_token.as_ref().expose())
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
crates_io::check_token(new_token.as_ref().expose()).map_err(Box::new)?;
|
||||
config::save_credentials(
|
||||
self.config,
|
||||
Some(RegistryCredentialConfig::Token(new_token)),
|
||||
&sid,
|
||||
)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
)?;
|
||||
let _ = self.config.shell().status(
|
||||
"Login",
|
||||
format!("token for `{}` saved", sid.display_registry_name()),
|
||||
|
@ -72,8 +69,7 @@ impl<'a> Credential for TokenCredential<'a> {
|
|||
return Err(Error::NotFound);
|
||||
}
|
||||
let reg_name = sid.display_registry_name();
|
||||
config::save_credentials(self.config, None, &sid)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
config::save_credentials(self.config, None, &sid)?;
|
||||
let _ = self.config.shell().status(
|
||||
"Logout",
|
||||
format!("token for `{reg_name}` has been removed from local storage"),
|
||||
|
|
|
@ -161,7 +161,7 @@ fn basic_unsupported() {
|
|||
[ERROR] credential provider `cargo:basic false` failed action `login`
|
||||
|
||||
Caused by:
|
||||
credential provider does not support the requested operation
|
||||
requested operation not supported
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
@ -175,7 +175,7 @@ Caused by:
|
|||
[ERROR] credential provider `cargo:basic false` failed action `logout`
|
||||
|
||||
Caused by:
|
||||
credential provider does not support the requested operation
|
||||
requested operation not supported
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
@ -280,7 +280,7 @@ fn invalid_token_output() {
|
|||
[ERROR] credential provider `[..]test-cred[EXE]` failed action `get`
|
||||
|
||||
Caused by:
|
||||
error: process `[..]` returned more than one line of output; expected a single token
|
||||
process `[..]` returned more than one line of output; expected a single token
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
|
|
@ -116,7 +116,7 @@ fn empty_login_token() {
|
|||
[ERROR] credential provider `cargo:token` failed action `login`
|
||||
|
||||
Caused by:
|
||||
[ERROR] please provide a non-empty token
|
||||
please provide a non-empty token
|
||||
",
|
||||
)
|
||||
.with_status(101)
|
||||
|
@ -130,7 +130,7 @@ Caused by:
|
|||
[ERROR] credential provider `cargo:token` failed action `login`
|
||||
|
||||
Caused by:
|
||||
[ERROR] please provide a non-empty token
|
||||
please provide a non-empty token
|
||||
",
|
||||
)
|
||||
.with_status(101)
|
||||
|
@ -160,7 +160,7 @@ fn invalid_login_token() {
|
|||
"[ERROR] credential provider `cargo:token` failed action `login`
|
||||
|
||||
Caused by:
|
||||
[ERROR] token contains invalid characters.
|
||||
token contains invalid characters.
|
||||
Only printable ISO-8859-1 characters are allowed as it is sent in a HTTPS header.",
|
||||
101,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue