diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-18 02:49:50 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-18 02:49:50 +0000 |
commit | 9835e2ae736235810b4ea1c162ca5e65c547e770 (patch) | |
tree | 3fcebf40ed70e581d776a8a4c65923e8ec20e026 /src/tools/cargo/credential | |
parent | Releasing progress-linux version 1.70.0+dfsg2-1~progress7.99u1. (diff) | |
download | rustc-9835e2ae736235810b4ea1c162ca5e65c547e770.tar.xz rustc-9835e2ae736235810b4ea1c162ca5e65c547e770.zip |
Merging upstream version 1.71.1+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/cargo/credential')
13 files changed, 907 insertions, 0 deletions
diff --git a/src/tools/cargo/credential/README.md b/src/tools/cargo/credential/README.md new file mode 100644 index 000000000..168cc71c3 --- /dev/null +++ b/src/tools/cargo/credential/README.md @@ -0,0 +1,8 @@ +# Cargo Credential Packages + +This directory contains Cargo packages for handling storage of tokens in a +secure manner. + +`cargo-credential` is a generic library to assist writing a credential +process. The other directories contain implementations that integrate with +specific credential systems. diff --git a/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml b/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml new file mode 100644 index 000000000..8db40e577 --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cargo-credential-1password" +version = "0.2.0" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-lang/cargo" +description = "A Cargo credential process that stores tokens in a 1password vault." + +[dependencies] +cargo-credential.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true diff --git a/src/tools/cargo/credential/cargo-credential-1password/src/main.rs b/src/tools/cargo/credential/cargo-credential-1password/src/main.rs new file mode 100644 index 000000000..4f512b717 --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential-1password/src/main.rs @@ -0,0 +1,314 @@ +//! Cargo registry 1password credential process. + +use cargo_credential::{Credential, Error}; +use serde::Deserialize; +use std::io::Read; +use std::process::{Command, Stdio}; + +const CARGO_TAG: &str = "cargo-registry"; + +/// Implementation of 1password keychain access for Cargo registries. +struct OnePasswordKeychain { + account: Option<String>, + vault: Option<String>, +} + +/// 1password Login item type, used for the JSON output of `op item get`. +#[derive(Deserialize)] +struct Login { + fields: Vec<Field>, +} + +#[derive(Deserialize)] +struct Field { + id: String, + value: Option<String>, +} + +/// 1password item from `op items list`. +#[derive(Deserialize)] +struct ListItem { + id: String, + urls: Vec<Url>, +} + +#[derive(Deserialize)] +struct Url { + href: String, +} + +impl OnePasswordKeychain { + fn new() -> Result<OnePasswordKeychain, Error> { + let mut args = std::env::args().skip(1); + let mut action = false; + let mut account = None; + let mut vault = None; + while let Some(arg) = args.next() { + match arg.as_str() { + "--account" => { + account = Some(args.next().ok_or("--account needs an arg")?); + } + "--vault" => { + vault = Some(args.next().ok_or("--vault needs an arg")?); + } + s if s.starts_with('-') => { + return Err(format!("unknown option {}", s).into()); + } + _ => { + if action { + return Err("too many arguments".into()); + } else { + action = true; + } + } + } + } + Ok(OnePasswordKeychain { account, vault }) + } + + fn signin(&self) -> Result<Option<String>, Error> { + // If there are any session env vars, we'll assume that this is the + // correct account, and that the user knows what they are doing. + if std::env::vars().any(|(name, _)| name.starts_with("OP_SESSION_")) { + return Ok(None); + } + let mut cmd = Command::new("op"); + cmd.args(&["signin", "--raw"]); + cmd.stdout(Stdio::piped()); + self.with_tty(&mut cmd)?; + let mut child = cmd + .spawn() + .map_err(|e| format!("failed to spawn `op`: {}", e))?; + let mut buffer = String::new(); + child + .stdout + .as_mut() + .unwrap() + .read_to_string(&mut buffer) + .map_err(|e| format!("failed to get session from `op`: {}", e))?; + if let Some(end) = buffer.find('\n') { + buffer.truncate(end); + } + let status = child + .wait() + .map_err(|e| format!("failed to wait for `op`: {}", e))?; + if !status.success() { + return Err(format!("failed to run `op signin`: {}", status).into()); + } + if buffer.is_empty() { + // When using CLI integration, `op signin` returns no output, + // so there is no need to set the session. + return Ok(None); + } + Ok(Some(buffer)) + } + + fn make_cmd(&self, session: &Option<String>, args: &[&str]) -> Command { + let mut cmd = Command::new("op"); + cmd.args(args); + if let Some(account) = &self.account { + cmd.arg("--account"); + cmd.arg(account); + } + if let Some(vault) = &self.vault { + cmd.arg("--vault"); + cmd.arg(vault); + } + if let Some(session) = session { + cmd.arg("--session"); + cmd.arg(session); + } + cmd + } + + fn with_tty(&self, cmd: &mut Command) -> Result<(), Error> { + #[cfg(unix)] + const IN_DEVICE: &str = "/dev/tty"; + #[cfg(windows)] + const IN_DEVICE: &str = "CONIN$"; + let stdin = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(IN_DEVICE)?; + cmd.stdin(stdin); + Ok(()) + } + + fn run_cmd(&self, mut cmd: Command) -> Result<String, Error> { + cmd.stdout(Stdio::piped()); + let mut child = cmd + .spawn() + .map_err(|e| format!("failed to spawn `op`: {}", e))?; + let mut buffer = String::new(); + child + .stdout + .as_mut() + .unwrap() + .read_to_string(&mut buffer) + .map_err(|e| format!("failed to read `op` output: {}", e))?; + let status = child + .wait() + .map_err(|e| format!("failed to wait for `op`: {}", e))?; + if !status.success() { + return Err(format!("`op` command exit error: {}", status).into()); + } + Ok(buffer) + } + + fn search(&self, session: &Option<String>, index_url: &str) -> Result<Option<String>, Error> { + let cmd = self.make_cmd( + session, + &[ + "items", + "list", + "--categories", + "Login", + "--tags", + CARGO_TAG, + "--format", + "json", + ], + ); + let buffer = self.run_cmd(cmd)?; + let items: Vec<ListItem> = serde_json::from_str(&buffer) + .map_err(|e| format!("failed to deserialize JSON from 1password list: {}", e))?; + let mut matches = items + .into_iter() + .filter(|item| item.urls.iter().any(|url| url.href == index_url)); + match matches.next() { + Some(login) => { + // Should this maybe just sort on `updatedAt` and return the newest one? + if matches.next().is_some() { + return Err(format!( + "too many 1password logins match registry `{}`, \ + consider deleting the excess entries", + index_url + ) + .into()); + } + Ok(Some(login.id)) + } + None => Ok(None), + } + } + + fn modify( + &self, + session: &Option<String>, + id: &str, + token: &str, + _name: Option<&str>, + ) -> Result<(), Error> { + let cmd = self.make_cmd( + session, + &["item", "edit", id, &format!("password={}", token)], + ); + self.run_cmd(cmd)?; + Ok(()) + } + + fn create( + &self, + session: &Option<String>, + index_url: &str, + token: &str, + name: Option<&str>, + ) -> Result<(), Error> { + let title = match name { + Some(name) => format!("Cargo registry token for {}", name), + None => "Cargo registry token".to_string(), + }; + let mut cmd = self.make_cmd( + session, + &[ + "item", + "create", + "--category", + "Login", + &format!("password={}", token), + &format!("url={}", index_url), + "--title", + &title, + "--tags", + CARGO_TAG, + ], + ); + // 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. + self.with_tty(&mut cmd)?; + self.run_cmd(cmd)?; + Ok(()) + } + + fn get_token(&self, session: &Option<String>, id: &str) -> Result<String, Error> { + let cmd = self.make_cmd(session, &["item", "get", "--format=json", id]); + let buffer = self.run_cmd(cmd)?; + let item: Login = serde_json::from_str(&buffer) + .map_err(|e| format!("failed to deserialize JSON from 1password get: {}", e))?; + let password = item.fields.into_iter().find(|item| item.id == "password"); + match password { + Some(password) => password + .value + .ok_or_else(|| format!("missing password value for entry").into()), + None => Err("could not find password field".into()), + } + } + + fn delete(&self, session: &Option<String>, id: &str) -> Result<(), Error> { + let cmd = self.make_cmd(session, &["item", "delete", id]); + self.run_cmd(cmd)?; + Ok(()) + } +} + +impl Credential for OnePasswordKeychain { + fn name(&self) -> &'static str { + env!("CARGO_PKG_NAME") + } + + fn get(&self, index_url: &str) -> Result<String, Error> { + let session = self.signin()?; + if let Some(id) = self.search(&session, index_url)? { + self.get_token(&session, &id) + } else { + return Err(format!( + "no 1password entry found for registry `{}`, try `cargo login` to add a token", + index_url + ) + .into()); + } + } + + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> { + let session = self.signin()?; + // Check if an item already exists. + if let Some(id) = self.search(&session, index_url)? { + self.modify(&session, &id, token, name) + } else { + self.create(&session, index_url, token, name) + } + } + + fn erase(&self, index_url: &str) -> Result<(), Error> { + let session = self.signin()?; + // Check if an item already exists. + if let Some(id) = self.search(&session, index_url)? { + self.delete(&session, &id)?; + } else { + eprintln!("not currently logged in to `{}`", index_url); + } + Ok(()) + } +} + +fn main() { + let op = match OnePasswordKeychain::new() { + Ok(op) => op, + Err(e) => { + eprintln!("error: {}", e); + std::process::exit(1); + } + }; + cargo_credential::main(op); +} diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml b/src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml new file mode 100644 index 000000000..63b3e95cc --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cargo-credential-gnome-secret" +version = "0.2.0" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-lang/cargo" +description = "A Cargo credential process that stores tokens with GNOME libsecret." + +[dependencies] +cargo-credential.workspace = true + +[build-dependencies] +pkg-config.workspace = true diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs b/src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs new file mode 100644 index 000000000..9283535af --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs @@ -0,0 +1,3 @@ +fn main() { + pkg_config::probe_library("libsecret-1").unwrap(); +} diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs b/src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs new file mode 100644 index 000000000..40972b05d --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs @@ -0,0 +1,194 @@ +//! Cargo registry gnome libsecret credential process. + +use cargo_credential::{Credential, Error}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int}; +use std::ptr::{null, null_mut}; + +#[allow(non_camel_case_types)] +type gchar = c_char; + +#[allow(non_camel_case_types)] +type gboolean = c_int; + +type GQuark = u32; + +#[repr(C)] +struct GError { + domain: GQuark, + code: c_int, + message: *mut gchar, +} + +#[repr(C)] +struct GCancellable { + _private: [u8; 0], +} + +#[repr(C)] +struct SecretSchema { + name: *const gchar, + flags: SecretSchemaFlags, + attributes: [SecretSchemaAttribute; 32], +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct SecretSchemaAttribute { + name: *const gchar, + attr_type: SecretSchemaAttributeType, +} + +#[repr(C)] +enum SecretSchemaFlags { + None = 0, +} + +#[repr(C)] +#[derive(Copy, Clone)] +enum SecretSchemaAttributeType { + String = 0, +} + +extern "C" { + fn secret_password_store_sync( + schema: *const SecretSchema, + collection: *const gchar, + label: *const gchar, + password: *const gchar, + cancellable: *mut GCancellable, + error: *mut *mut GError, + ... + ) -> gboolean; + fn secret_password_clear_sync( + schema: *const SecretSchema, + cancellable: *mut GCancellable, + error: *mut *mut GError, + ... + ) -> gboolean; + fn secret_password_lookup_sync( + schema: *const SecretSchema, + cancellable: *mut GCancellable, + error: *mut *mut GError, + ... + ) -> *mut gchar; +} + +struct GnomeSecret; + +fn label(index_url: &str) -> CString { + CString::new(format!("cargo-registry:{}", index_url)).unwrap() +} + +fn schema() -> SecretSchema { + let mut attributes = [SecretSchemaAttribute { + name: null(), + attr_type: SecretSchemaAttributeType::String, + }; 32]; + attributes[0] = SecretSchemaAttribute { + name: b"url\0".as_ptr() as *const gchar, + attr_type: SecretSchemaAttributeType::String, + }; + SecretSchema { + name: b"org.rust-lang.cargo.registry\0".as_ptr() as *const gchar, + flags: SecretSchemaFlags::None, + attributes, + } +} + +impl Credential for GnomeSecret { + fn name(&self) -> &'static str { + env!("CARGO_PKG_NAME") + } + + fn get(&self, index_url: &str) -> Result<String, Error> { + let mut error: *mut GError = null_mut(); + let attr_url = CString::new("url").unwrap(); + let index_url_c = CString::new(index_url).unwrap(); + let schema = schema(); + unsafe { + let token_c = secret_password_lookup_sync( + &schema, + null_mut(), + &mut error, + attr_url.as_ptr(), + index_url_c.as_ptr(), + null() as *const gchar, + ); + if !error.is_null() { + return Err(format!( + "failed to get token: {}", + CStr::from_ptr((*error).message).to_str()? + ) + .into()); + } + if token_c.is_null() { + return Err(format!("cannot find token for {}", index_url).into()); + } + let token = CStr::from_ptr(token_c) + .to_str() + .map_err(|e| format!("expected utf8 token: {}", e))? + .to_string(); + Ok(token) + } + } + + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> { + let label = label(name.unwrap_or(index_url)); + let token = CString::new(token).unwrap(); + let mut error: *mut GError = null_mut(); + let attr_url = CString::new("url").unwrap(); + let index_url_c = CString::new(index_url).unwrap(); + let schema = schema(); + unsafe { + secret_password_store_sync( + &schema, + b"default\0".as_ptr() as *const gchar, + label.as_ptr(), + token.as_ptr(), + null_mut(), + &mut error, + attr_url.as_ptr(), + index_url_c.as_ptr(), + null() as *const gchar, + ); + if !error.is_null() { + return Err(format!( + "failed to store token: {}", + CStr::from_ptr((*error).message).to_str()? + ) + .into()); + } + } + Ok(()) + } + + fn erase(&self, index_url: &str) -> Result<(), Error> { + let schema = schema(); + let mut error: *mut GError = null_mut(); + let attr_url = CString::new("url").unwrap(); + let index_url_c = CString::new(index_url).unwrap(); + unsafe { + secret_password_clear_sync( + &schema, + null_mut(), + &mut error, + attr_url.as_ptr(), + index_url_c.as_ptr(), + null() as *const gchar, + ); + if !error.is_null() { + return Err(format!( + "failed to erase token: {}", + CStr::from_ptr((*error).message).to_str()? + ) + .into()); + } + } + Ok(()) + } +} + +fn main() { + cargo_credential::main(GnomeSecret); +} diff --git a/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml b/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml new file mode 100644 index 000000000..6311b71de --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cargo-credential-macos-keychain" +version = "0.2.0" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-lang/cargo" +description = "A Cargo credential process that stores tokens in a macOS keychain." + +[dependencies] +cargo-credential.workspace = true + +[target.'cfg(target_os = "macos")'.dependencies] +security-framework.workspace = true diff --git a/src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs b/src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs new file mode 100644 index 000000000..4d6ea96d0 --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs @@ -0,0 +1,58 @@ +//! Cargo registry macos keychain credential process. + +#[cfg(target_os = "macos")] +mod macos { + use cargo_credential::{Credential, Error}; + use security_framework::os::macos::keychain::SecKeychain; + + pub(crate) struct MacKeychain; + + /// The account name is not used. + const ACCOUNT: &'static str = ""; + + fn registry(registry_name: &str) -> String { + format!("cargo-registry:{}", registry_name) + } + + impl Credential for MacKeychain { + fn name(&self) -> &'static str { + env!("CARGO_PKG_NAME") + } + + fn get(&self, index_url: &str) -> Result<String, Error> { + let keychain = SecKeychain::default().unwrap(); + let service_name = registry(index_url); + let (pass, _item) = keychain.find_generic_password(&service_name, ACCOUNT)?; + String::from_utf8(pass.as_ref().to_vec()) + .map_err(|_| "failed to convert token to UTF8".into()) + } + + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> { + let keychain = SecKeychain::default().unwrap(); + let service_name = registry(name.unwrap_or(index_url)); + if let Ok((_pass, mut item)) = keychain.find_generic_password(&service_name, ACCOUNT) { + item.set_password(token.as_bytes())?; + } else { + keychain.add_generic_password(&service_name, ACCOUNT, token.as_bytes())?; + } + Ok(()) + } + + fn erase(&self, index_url: &str) -> Result<(), Error> { + let keychain = SecKeychain::default().unwrap(); + let service_name = registry(index_url); + let (_pass, item) = keychain.find_generic_password(&service_name, ACCOUNT)?; + item.delete(); + Ok(()) + } + } +} + +#[cfg(not(target_os = "macos"))] +use cargo_credential::UnsupportedCredential as MacKeychain; +#[cfg(target_os = "macos")] +use macos::MacKeychain; + +fn main() { + cargo_credential::main(MacKeychain); +} diff --git a/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml b/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml new file mode 100644 index 000000000..cd168a8a3 --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "cargo-credential-wincred" +version = "0.2.0" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-lang/cargo" +description = "A Cargo credential process that stores tokens with Windows Credential Manager." + +[dependencies] +cargo-credential.workspace = true + +[target.'cfg(windows)'.dependencies.windows-sys] +features = ["Win32_Foundation", "Win32_Security_Credentials"] +workspace = true diff --git a/src/tools/cargo/credential/cargo-credential-wincred/src/main.rs b/src/tools/cargo/credential/cargo-credential-wincred/src/main.rs new file mode 100644 index 000000000..4377172e8 --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential-wincred/src/main.rs @@ -0,0 +1,122 @@ +//! Cargo registry windows credential process. + +#[cfg(windows)] +mod win { + use cargo_credential::{Credential, Error}; + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + + use windows_sys::core::PWSTR; + use windows_sys::Win32::Foundation::ERROR_NOT_FOUND; + use windows_sys::Win32::Foundation::FILETIME; + use windows_sys::Win32::Foundation::TRUE; + use windows_sys::Win32::Security::Credentials::CredDeleteW; + use windows_sys::Win32::Security::Credentials::CredReadW; + use windows_sys::Win32::Security::Credentials::CredWriteW; + use windows_sys::Win32::Security::Credentials::CREDENTIALW; + use windows_sys::Win32::Security::Credentials::CRED_PERSIST_LOCAL_MACHINE; + use windows_sys::Win32::Security::Credentials::CRED_TYPE_GENERIC; + + pub(crate) struct WindowsCredential; + + /// Converts a string to a nul-terminated wide UTF-16 byte sequence. + fn wstr(s: &str) -> Vec<u16> { + let mut wide: Vec<u16> = OsStr::new(s).encode_wide().collect(); + if wide.iter().any(|b| *b == 0) { + panic!("nul byte in wide string"); + } + wide.push(0); + wide + } + + fn target_name(registry_name: &str) -> Vec<u16> { + wstr(&format!("cargo-registry:{}", registry_name)) + } + + impl Credential for WindowsCredential { + fn name(&self) -> &'static str { + env!("CARGO_PKG_NAME") + } + + fn get(&self, index_url: &str) -> Result<String, Error> { + let target_name = target_name(index_url); + let p_credential: *mut CREDENTIALW = std::ptr::null_mut() as *mut _; + unsafe { + if CredReadW( + target_name.as_ptr(), + CRED_TYPE_GENERIC, + 0, + p_credential as *mut _ as *mut _, + ) != TRUE + { + return Err(format!( + "failed to fetch token: {}", + std::io::Error::last_os_error() + ) + .into()); + } + let bytes = std::slice::from_raw_parts( + (*p_credential).CredentialBlob, + (*p_credential).CredentialBlobSize as usize, + ); + String::from_utf8(bytes.to_vec()) + .map_err(|_| "failed to convert token to UTF8".into()) + } + } + + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> { + let token = token.as_bytes(); + let target_name = target_name(index_url); + let comment = match name { + Some(name) => wstr(&format!("Cargo registry token for {}", name)), + None => wstr("Cargo registry token"), + }; + let mut credential = CREDENTIALW { + Flags: 0, + Type: CRED_TYPE_GENERIC, + TargetName: target_name.as_ptr() as PWSTR, + Comment: comment.as_ptr() as PWSTR, + LastWritten: FILETIME { + dwLowDateTime: 0, + dwHighDateTime: 0, + }, + CredentialBlobSize: token.len() as u32, + CredentialBlob: token.as_ptr() as *mut u8, + Persist: CRED_PERSIST_LOCAL_MACHINE, + AttributeCount: 0, + Attributes: std::ptr::null_mut(), + TargetAlias: std::ptr::null_mut(), + UserName: std::ptr::null_mut(), + }; + let result = unsafe { CredWriteW(&mut credential, 0) }; + if result != TRUE { + let err = std::io::Error::last_os_error(); + return Err(format!("failed to store token: {}", err).into()); + } + Ok(()) + } + + fn erase(&self, index_url: &str) -> Result<(), Error> { + let target_name = target_name(index_url); + let result = unsafe { CredDeleteW(target_name.as_ptr(), CRED_TYPE_GENERIC, 0) }; + if result != TRUE { + let err = std::io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_NOT_FOUND as i32) { + eprintln!("not currently logged in to `{}`", index_url); + return Ok(()); + } + return Err(format!("failed to remove token: {}", err).into()); + } + Ok(()) + } + } +} + +#[cfg(not(windows))] +use cargo_credential::UnsupportedCredential as WindowsCredential; +#[cfg(windows)] +use win::WindowsCredential; + +fn main() { + cargo_credential::main(WindowsCredential); +} diff --git a/src/tools/cargo/credential/cargo-credential/Cargo.toml b/src/tools/cargo/credential/cargo-credential/Cargo.toml new file mode 100644 index 000000000..2addaf5af --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cargo-credential" +version = "0.2.0" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-lang/cargo" +description = "A library to assist writing Cargo credential helpers." + +[dependencies] diff --git a/src/tools/cargo/credential/cargo-credential/README.md b/src/tools/cargo/credential/cargo-credential/README.md new file mode 100644 index 000000000..53dc8e6b7 --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential/README.md @@ -0,0 +1,41 @@ +# cargo-credential + +This package is a library to assist writing a Cargo credential helper, which +provides an interface to store tokens for authorizing access to a registry +such as https://crates.io/. + +Documentation about credential processes may be found at +https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#credential-process + +Example implementations may be found at +https://github.com/rust-lang/cargo/tree/master/credential + +## Usage + +Create a Cargo project with this as a dependency: + +```toml +# Add this to your Cargo.toml: + +[dependencies] +cargo-credential = "0.1" +``` + +And then include a `main.rs` binary which implements the `Credential` trait, and calls +the `main` function which will call the appropriate method of the trait: + +```rust +// src/main.rs + +use cargo_credential::{Credential, Error}; + +struct MyCredential; + +impl Credential for MyCredential { + /// implement trait methods here... +} + +fn main() { + cargo_credential::main(MyCredential); +} +``` diff --git a/src/tools/cargo/credential/cargo-credential/src/lib.rs b/src/tools/cargo/credential/cargo-credential/src/lib.rs new file mode 100644 index 000000000..c75172242 --- /dev/null +++ b/src/tools/cargo/credential/cargo-credential/src/lib.rs @@ -0,0 +1,106 @@ +//! Helper library for writing Cargo credential processes. +//! +//! 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: +//! +//! ```rust,ignore +//! fn main() { +//! cargo_credential::main(MyCredential); +//! } +//! ``` +//! +//! This will determine the action to perform (get/store/erase) by looking at +//! the CLI arguments for the first argument that does not start with `-`. It +//! will then call the corresponding method of the trait to perform the +//! requested action. + +pub type Error = Box<dyn std::error::Error>; + +pub trait Credential { + /// Returns the name of this credential process. + fn name(&self) -> &'static str; + + /// Retrieves a token for the given registry. + fn get(&self, index_url: &str) -> Result<String, Error>; + + /// Stores the given token for the given registry. + fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error>; + + /// Removes the token for the given registry. + /// + /// If the user is not logged in, this should print a message to stderr if + /// possible indicating that the user is not currently logged in, and + /// return `Ok`. + fn erase(&self, index_url: &str) -> Result<(), Error>; +} + +pub struct UnsupportedCredential; + +impl Credential for UnsupportedCredential { + fn name(&self) -> &'static str { + "unsupported" + } + + fn get(&self, _index_url: &str) -> Result<String, Error> { + Err("unsupported".into()) + } + + fn store(&self, _index_url: &str, _token: &str, _name: Option<&str>) -> Result<(), Error> { + Err("unsupported".into()) + } + + fn erase(&self, _index_url: &str) -> Result<(), Error> { + Err("unsupported".into()) + } +} + +/// Runs the credential interaction by processing the command-line and +/// environment variables. +pub fn main(credential: impl Credential) { + let name = credential.name(); + if let Err(e) = doit(credential) { + eprintln!("{} error: {}", name, e); + std::process::exit(1); + } +} + +fn env(name: &str) -> Result<String, Error> { + std::env::var(name).map_err(|_| format!("environment variable `{}` is not set", name).into()) +} + +fn doit(credential: impl Credential) -> Result<(), Error> { + let which = std::env::args() + .skip(1) + .skip_while(|arg| arg.starts_with('-')) + .next() + .ok_or_else(|| "first argument must be the {action}")?; + let index_url = env("CARGO_REGISTRY_INDEX_URL")?; + let name = std::env::var("CARGO_REGISTRY_NAME_OPT").ok(); + let result = match which.as_ref() { + "get" => credential.get(&index_url).and_then(|token| { + println!("{}", token); + Ok(()) + }), + "store" => { + read_token().and_then(|token| credential.store(&index_url, &token, name.as_deref())) + } + "erase" => credential.erase(&index_url), + _ => { + return Err(format!( + "unexpected command-line argument `{}`, expected get/store/erase", + which + ) + .into()) + } + }; + result.map_err(|e| format!("failed to `{}` token: {}", which, e).into()) +} + +fn read_token() -> Result<String, Error> { + let mut buffer = String::new(); + std::io::stdin().read_line(&mut buffer)?; + if buffer.ends_with('\n') { + buffer.pop(); + } + Ok(buffer) +} |