mirror of https://github.com/rust-lang/cargo
Implement credential-process.
This commit is contained in:
parent
6bc510d33a
commit
cc6df1d7a5
|
@ -18,12 +18,12 @@ jobs:
|
|||
- run: rustup update stable && rustup default stable
|
||||
- run: rustup component add rustfmt
|
||||
- run: cargo fmt --all -- --check
|
||||
- run: cd crates/cargo-test-macro && cargo fmt --all -- --check
|
||||
- run: cd crates/cargo-test-support && cargo fmt --all -- --check
|
||||
- run: cd crates/crates-io && cargo fmt --all -- --check
|
||||
- run: cd crates/resolver-tests && cargo fmt --all -- --check
|
||||
- run: cd crates/cargo-platform && cargo fmt --all -- --check
|
||||
- run: cd crates/mdman && cargo fmt --all -- --check
|
||||
- run: |
|
||||
for manifest in `find crates -name Cargo.toml`
|
||||
do
|
||||
echo check fmt for $manifest
|
||||
cargo fmt --all --manifest-path $manifest -- --check
|
||||
done
|
||||
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
@ -58,7 +58,7 @@ jobs:
|
|||
- run: rustup target add ${{ matrix.other }}
|
||||
- run: rustup component add rustc-dev llvm-tools-preview rust-docs
|
||||
if: startsWith(matrix.rust, 'nightly')
|
||||
- run: sudo apt update -y && sudo apt install gcc-multilib -y
|
||||
- run: sudo apt update -y && sudo apt install gcc-multilib libsecret-1-0 libsecret-1-dev -y
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- run: rustup component add rustfmt || echo "rustfmt not available"
|
||||
|
||||
|
@ -67,6 +67,13 @@ jobs:
|
|||
- run: cargo test --features 'deny-warnings' -p cargo-test-support
|
||||
- run: cargo test -p cargo-platform
|
||||
- run: cargo test --manifest-path crates/mdman/Cargo.toml
|
||||
- run: cargo build --manifest-path crates/credential/cargo-credential-1password/Cargo.toml
|
||||
- run: cargo build --manifest-path crates/credential/cargo-credential-gnome-secret/Cargo.toml
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- run: cargo build --manifest-path crates/credential/cargo-credential-macos-keychain/Cargo.toml
|
||||
if: matrix.os == 'macos-latest'
|
||||
- run: cargo build --manifest-path crates/credential/cargo-credential-wincred/Cargo.toml
|
||||
if: matrix.os == 'windows-latest'
|
||||
|
||||
resolver:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
@ -1544,6 +1544,10 @@ fn substitute_macros(input: &str) -> String {
|
|||
("[INSTALLED]", " Installed"),
|
||||
("[REPLACED]", " Replaced"),
|
||||
("[BUILDING]", " Building"),
|
||||
("[LOGIN]", " Login"),
|
||||
("[LOGOUT]", " Logout"),
|
||||
("[YANK]", " Yank"),
|
||||
("[OWNER]", " Owner"),
|
||||
];
|
||||
let mut result = input.to_owned();
|
||||
for &(pat, subst) in ¯os {
|
||||
|
|
|
@ -110,14 +110,27 @@ pub trait CargoPathExt {
|
|||
}
|
||||
|
||||
impl CargoPathExt for Path {
|
||||
/* Technically there is a potential race condition, but we don't
|
||||
* care all that much for our tests
|
||||
*/
|
||||
fn rm_rf(&self) {
|
||||
if self.exists() {
|
||||
let meta = match self.symlink_metadata() {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
return;
|
||||
}
|
||||
panic!("failed to remove {:?}, could not read: {:?}", self, e);
|
||||
}
|
||||
};
|
||||
// There is a race condition between fetching the metadata and
|
||||
// actually performing the removal, but we don't care all that much
|
||||
// for our tests.
|
||||
if meta.is_dir() {
|
||||
if let Err(e) = remove_dir_all::remove_dir_all(self) {
|
||||
panic!("failed to remove {:?}: {:?}", self, e)
|
||||
}
|
||||
} else {
|
||||
if let Err(e) = fs::remove_file(self) {
|
||||
panic!("failed to remove {:?}: {:?}", self, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "cargo-credential-1password"
|
||||
version = "0.1.0"
|
||||
authors = ["The Rust Project Developers"]
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
cargo-credential = { path = "../cargo-credential" }
|
||||
serde = { version = "1.0.117", features = ["derive"] }
|
||||
serde_json = "1.0.59"
|
|
@ -0,0 +1,323 @@
|
|||
//! 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>,
|
||||
sign_in_address: Option<String>,
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
/// 1password Login item type, used for the JSON output of `op get item`.
|
||||
#[derive(Deserialize)]
|
||||
struct Login {
|
||||
details: Details,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Details {
|
||||
fields: Vec<Field>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Field {
|
||||
designation: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
/// 1password item from `op list items`.
|
||||
#[derive(Deserialize)]
|
||||
struct ListItem {
|
||||
uuid: String,
|
||||
overview: Overview,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Overview {
|
||||
title: 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;
|
||||
let mut sign_in_address = None;
|
||||
let mut email = 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")?);
|
||||
}
|
||||
"--sign-in-address" => {
|
||||
sign_in_address = Some(args.next().ok_or("--sign-in-address needs an arg")?);
|
||||
}
|
||||
"--email" => {
|
||||
email = Some(args.next().ok_or("--email 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if sign_in_address.is_none() && email.is_some() {
|
||||
return Err("--email requires --sign-in-address".into());
|
||||
}
|
||||
Ok(OnePasswordKeychain {
|
||||
account,
|
||||
vault,
|
||||
sign_in_address,
|
||||
email,
|
||||
})
|
||||
}
|
||||
|
||||
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.arg("signin");
|
||||
if let Some(addr) = &self.sign_in_address {
|
||||
cmd.arg(addr);
|
||||
if let Some(email) = &self.email {
|
||||
cmd.arg(email);
|
||||
}
|
||||
}
|
||||
cmd.arg("--raw");
|
||||
cmd.stdout(Stdio::piped());
|
||||
#[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);
|
||||
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());
|
||||
}
|
||||
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 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>,
|
||||
registry_name: &str,
|
||||
) -> Result<Option<String>, Error> {
|
||||
let cmd = self.make_cmd(
|
||||
session,
|
||||
&[
|
||||
"list",
|
||||
"items",
|
||||
"--categories",
|
||||
"Login",
|
||||
"--tags",
|
||||
CARGO_TAG,
|
||||
],
|
||||
);
|
||||
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.overview.title == registry_name);
|
||||
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 name {}, \
|
||||
consider deleting the excess entries",
|
||||
registry_name
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(Some(login.uuid))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn modify(&self, session: &Option<String>, uuid: &str, token: &str) -> Result<(), Error> {
|
||||
let cmd = self.make_cmd(
|
||||
session,
|
||||
&["edit", "item", uuid, &format!("password={}", token)],
|
||||
);
|
||||
self.run_cmd(cmd)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create(
|
||||
&self,
|
||||
session: &Option<String>,
|
||||
registry_name: &str,
|
||||
api_url: &str,
|
||||
token: &str,
|
||||
) -> Result<(), Error> {
|
||||
let cmd = self.make_cmd(
|
||||
session,
|
||||
&[
|
||||
"create",
|
||||
"item",
|
||||
"Login",
|
||||
&format!("password={}", token),
|
||||
&format!("url={}", api_url),
|
||||
"--title",
|
||||
registry_name,
|
||||
"--tags",
|
||||
CARGO_TAG,
|
||||
],
|
||||
);
|
||||
self.run_cmd(cmd)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_token(&self, session: &Option<String>, uuid: &str) -> Result<String, Error> {
|
||||
let cmd = self.make_cmd(session, &["get", "item", uuid]);
|
||||
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
|
||||
.details
|
||||
.fields
|
||||
.into_iter()
|
||||
.find(|item| item.designation == "password");
|
||||
match password {
|
||||
Some(password) => Ok(password.value),
|
||||
None => Err("could not find password field".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&self, session: &Option<String>, uuid: &str) -> Result<(), Error> {
|
||||
let cmd = self.make_cmd(session, &["delete", "item", uuid]);
|
||||
self.run_cmd(cmd)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Credential for OnePasswordKeychain {
|
||||
fn name(&self) -> &'static str {
|
||||
env!("CARGO_PKG_NAME")
|
||||
}
|
||||
|
||||
fn get(&self, registry_name: &str, _api_url: &str) -> Result<String, Error> {
|
||||
let session = self.signin()?;
|
||||
if let Some(uuid) = self.search(&session, registry_name)? {
|
||||
self.get_token(&session, &uuid)
|
||||
} else {
|
||||
return Err(format!(
|
||||
"no 1password entry found for registry `{}`, try `cargo login` to add a token",
|
||||
registry_name
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn store(&self, registry_name: &str, api_url: &str, token: &str) -> Result<(), Error> {
|
||||
let session = self.signin()?;
|
||||
// Check if an item already exists.
|
||||
if let Some(uuid) = self.search(&session, registry_name)? {
|
||||
self.modify(&session, &uuid, token)
|
||||
} else {
|
||||
self.create(&session, registry_name, api_url, token)
|
||||
}
|
||||
}
|
||||
|
||||
fn erase(&self, registry_name: &str, _api_url: &str) -> Result<(), Error> {
|
||||
let session = self.signin()?;
|
||||
// Check if an item already exists.
|
||||
if let Some(uuid) = self.search(&session, registry_name)? {
|
||||
self.delete(&session, &uuid)?;
|
||||
} else {
|
||||
eprintln!("not currently logged in to `{}`", registry_name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let op = match OnePasswordKeychain::new() {
|
||||
Ok(op) => op,
|
||||
Err(e) => {
|
||||
eprintln!("error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
cargo_credential::main(op);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "cargo-credential-gnome-secret"
|
||||
version = "0.1.0"
|
||||
authors = ["The Rust Project Developers"]
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
cargo-credential = { path = "../cargo-credential" }
|
||||
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3.19"
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
pkg_config::probe_library("libsecret-1").unwrap();
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
//! 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(registry_name: &str) -> CString {
|
||||
CString::new(format!("cargo-registry:{}", registry_name)).unwrap()
|
||||
}
|
||||
|
||||
fn schema() -> SecretSchema {
|
||||
let mut attributes = [SecretSchemaAttribute {
|
||||
name: null(),
|
||||
attr_type: SecretSchemaAttributeType::String,
|
||||
}; 32];
|
||||
attributes[0] = SecretSchemaAttribute {
|
||||
name: b"registry\0".as_ptr() as *const gchar,
|
||||
attr_type: SecretSchemaAttributeType::String,
|
||||
};
|
||||
attributes[1] = 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, registry_name: &str, api_url: &str) -> Result<String, Error> {
|
||||
let mut error: *mut GError = null_mut();
|
||||
let attr_registry = CString::new("registry").unwrap();
|
||||
let attr_url = CString::new("url").unwrap();
|
||||
let registry_name_c = CString::new(registry_name).unwrap();
|
||||
let api_url_c = CString::new(api_url).unwrap();
|
||||
let schema = schema();
|
||||
unsafe {
|
||||
let token_c = secret_password_lookup_sync(
|
||||
&schema,
|
||||
null_mut(),
|
||||
&mut error,
|
||||
attr_registry.as_ptr(),
|
||||
registry_name_c.as_ptr(),
|
||||
attr_url.as_ptr(),
|
||||
api_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 {}", registry_name).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, registry_name: &str, api_url: &str, token: &str) -> Result<(), Error> {
|
||||
let label = label(registry_name);
|
||||
let token = CString::new(token).unwrap();
|
||||
let mut error: *mut GError = null_mut();
|
||||
let attr_registry = CString::new("registry").unwrap();
|
||||
let attr_url = CString::new("url").unwrap();
|
||||
let registry_name_c = CString::new(registry_name).unwrap();
|
||||
let api_url_c = CString::new(api_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_registry.as_ptr(),
|
||||
registry_name_c.as_ptr(),
|
||||
attr_url.as_ptr(),
|
||||
api_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, registry_name: &str, api_url: &str) -> Result<(), Error> {
|
||||
let schema = schema();
|
||||
let mut error: *mut GError = null_mut();
|
||||
let attr_registry = CString::new("registry").unwrap();
|
||||
let attr_url = CString::new("url").unwrap();
|
||||
let registry_name_c = CString::new(registry_name).unwrap();
|
||||
let api_url_c = CString::new(api_url).unwrap();
|
||||
unsafe {
|
||||
secret_password_clear_sync(
|
||||
&schema,
|
||||
null_mut(),
|
||||
&mut error,
|
||||
attr_registry.as_ptr(),
|
||||
registry_name_c.as_ptr(),
|
||||
attr_url.as_ptr(),
|
||||
api_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);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "cargo-credential-macos-keychain"
|
||||
version = "0.1.0"
|
||||
authors = ["The Rust Project Developers"]
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
cargo-credential = { path = "../cargo-credential" }
|
||||
security-framework = "2.0.0"
|
|
@ -0,0 +1,50 @@
|
|||
//! Cargo registry macos keychain credential process.
|
||||
|
||||
use cargo_credential::{Credential, Error};
|
||||
use security_framework::os::macos::keychain::SecKeychain;
|
||||
|
||||
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, registry_name: &str, _api_url: &str) -> Result<String, Error> {
|
||||
let keychain = SecKeychain::default().unwrap();
|
||||
let service_name = registry(registry_name);
|
||||
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, registry_name: &str, _api_url: &str, token: &str) -> Result<(), Error> {
|
||||
let keychain = SecKeychain::default().unwrap();
|
||||
let service_name = registry(registry_name);
|
||||
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, registry_name: &str, _api_url: &str) -> Result<(), Error> {
|
||||
let keychain = SecKeychain::default().unwrap();
|
||||
let service_name = registry(registry_name);
|
||||
let (_pass, item) = keychain.find_generic_password(&service_name, ACCOUNT)?;
|
||||
item.delete();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
cargo_credential::main(MacKeychain);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "cargo-credential-wincred"
|
||||
version = "0.1.0"
|
||||
authors = ["The Rust Project Developers"]
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
cargo-credential = { path = "../cargo-credential" }
|
||||
winapi = { version = "0.3.9", features = ["wincred", "winerror", "impl-default"] }
|
|
@ -0,0 +1,93 @@
|
|||
//! Cargo registry windows credential process.
|
||||
|
||||
use cargo_credential::{Credential, Error};
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use winapi::shared::minwindef::{DWORD, FILETIME, LPBYTE, TRUE};
|
||||
use winapi::shared::winerror;
|
||||
use winapi::um::wincred;
|
||||
use winapi::um::winnt::LPWSTR;
|
||||
|
||||
struct WindowsCredential;
|
||||
|
||||
fn wstr(s: &str) -> Vec<u16> {
|
||||
OsStr::new(s).encode_wide().chain(Some(0)).collect()
|
||||
}
|
||||
|
||||
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, registry_name: &str, _api_url: &str) -> Result<String, Error> {
|
||||
let target_name = target_name(registry_name);
|
||||
let mut p_credential: wincred::PCREDENTIALW = std::ptr::null_mut();
|
||||
unsafe {
|
||||
if wincred::CredReadW(
|
||||
target_name.as_ptr(),
|
||||
wincred::CRED_TYPE_GENERIC,
|
||||
0,
|
||||
&mut p_credential,
|
||||
) != 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, registry_name: &str, _api_url: &str, token: &str) -> Result<(), Error> {
|
||||
let token = token.as_bytes();
|
||||
let target_name = target_name(registry_name);
|
||||
let comment = wstr("Cargo registry token");
|
||||
let mut credential = wincred::CREDENTIALW {
|
||||
Flags: 0,
|
||||
Type: wincred::CRED_TYPE_GENERIC,
|
||||
TargetName: target_name.as_ptr() as LPWSTR,
|
||||
Comment: comment.as_ptr() as LPWSTR,
|
||||
LastWritten: FILETIME::default(),
|
||||
CredentialBlobSize: token.len() as DWORD,
|
||||
CredentialBlob: token.as_ptr() as LPBYTE,
|
||||
Persist: wincred::CRED_PERSIST_LOCAL_MACHINE,
|
||||
AttributeCount: 0,
|
||||
Attributes: std::ptr::null_mut(),
|
||||
TargetAlias: std::ptr::null_mut(),
|
||||
UserName: std::ptr::null_mut(),
|
||||
};
|
||||
let result = unsafe { wincred::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, registry_name: &str, _api_url: &str) -> Result<(), Error> {
|
||||
let target_name = target_name(registry_name);
|
||||
let result =
|
||||
unsafe { wincred::CredDeleteW(target_name.as_ptr(), wincred::CRED_TYPE_GENERIC, 0) };
|
||||
if result != TRUE {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.raw_os_error() == Some(winerror::ERROR_NOT_FOUND as i32) {
|
||||
eprintln!("not currently logged in to `{}`", registry_name);
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!("failed to remove token: {}", err).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
cargo_credential::main(WindowsCredential);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "cargo-credential"
|
||||
version = "0.1.0"
|
||||
authors = ["The Rust Project Developers"]
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,81 @@
|
|||
//! 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);
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
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, registry_name: &str, api_url: &str) -> Result<String, Error>;
|
||||
|
||||
/// Stores the given token for the given registry.
|
||||
fn store(&self, registry_name: &str, api_url: &str, token: &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, registry_name: &str, api_url: &str) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// 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 registry_name = env("CARGO_REGISTRY_NAME")?;
|
||||
let api_url = env("CARGO_REGISTRY_API_URL")?;
|
||||
let result = match which.as_ref() {
|
||||
"get" => credential.get(®istry_name, &api_url).and_then(|token| {
|
||||
println!("{}", token);
|
||||
Ok(())
|
||||
}),
|
||||
"store" => {
|
||||
read_token().and_then(|token| credential.store(®istry_name, &api_url, &token))
|
||||
}
|
||||
"erase" => credential.erase(®istry_name, &api_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)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use crate::command_prelude::*;
|
||||
use anyhow::format_err;
|
||||
use cargo::core::features;
|
||||
use cargo::ops;
|
||||
|
||||
pub fn cli() -> App {
|
||||
subcommand("logout")
|
||||
.about("Remove an API token from the registry locally")
|
||||
.arg(opt("quiet", "No output printed to stdout").short("q"))
|
||||
.arg(opt("registry", "Registry to use").value_name("REGISTRY"))
|
||||
.after_help("Run `cargo help logout` for more detailed information.\n")
|
||||
}
|
||||
|
||||
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
|
||||
let unstable = config.cli_unstable();
|
||||
if !(unstable.credential_process || unstable.unstable_options) {
|
||||
const SEE: &str = "See https://github.com/rust-lang/cargo/issues/8933 for more \
|
||||
information about the `cargo logout` command.";
|
||||
if features::nightly_features_allowed() {
|
||||
return Err(format_err!(
|
||||
"the `cargo logout` command is unstable, pass `-Z unstable-options` to enable it\n\
|
||||
{}",
|
||||
SEE
|
||||
)
|
||||
.into());
|
||||
} else {
|
||||
return Err(format_err!(
|
||||
"the `cargo logout` command is unstable, and only available on the \
|
||||
nightly channel of Cargo, but this is the `{}` channel\n\
|
||||
{}\n\
|
||||
{}",
|
||||
features::channel(),
|
||||
features::SEE_CHANNELS,
|
||||
SEE
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
config.load_credentials()?;
|
||||
ops::registry_logout(config, args.value_of("registry").map(String::from))?;
|
||||
Ok(())
|
||||
}
|
|
@ -15,6 +15,7 @@ pub fn builtin() -> Vec<App> {
|
|||
install::cli(),
|
||||
locate_project::cli(),
|
||||
login::cli(),
|
||||
logout::cli(),
|
||||
metadata::cli(),
|
||||
new::cli(),
|
||||
owner::cli(),
|
||||
|
@ -52,6 +53,7 @@ pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches<'_>) -> Cli
|
|||
"install" => install::exec,
|
||||
"locate-project" => locate_project::exec,
|
||||
"login" => login::exec,
|
||||
"logout" => logout::exec,
|
||||
"metadata" => metadata::exec,
|
||||
"new" => new::exec,
|
||||
"owner" => owner::exec,
|
||||
|
@ -90,6 +92,7 @@ pub mod init;
|
|||
pub mod install;
|
||||
pub mod locate_project;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod metadata;
|
||||
pub mod new;
|
||||
pub mod owner;
|
||||
|
|
|
@ -360,6 +360,7 @@ pub struct CliUnstable {
|
|||
pub namespaced_features: bool,
|
||||
pub weak_dep_features: bool,
|
||||
pub extra_link_arg: bool,
|
||||
pub credential_process: bool,
|
||||
}
|
||||
|
||||
fn deserialize_build_std<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
|
||||
|
@ -468,6 +469,7 @@ impl CliUnstable {
|
|||
"namespaced-features" => self.namespaced_features = parse_empty(k, v)?,
|
||||
"weak-dep-features" => self.weak_dep_features = parse_empty(k, v)?,
|
||||
"extra-link-arg" => self.extra_link_arg = parse_empty(k, v)?,
|
||||
"credential-process" => self.credential_process = parse_empty(k, v)?,
|
||||
_ => bail!("unknown `-Z` flag specified: {}", k),
|
||||
}
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ pub use self::cargo_uninstall::uninstall;
|
|||
pub use self::fix::{fix, fix_maybe_exec_rustc, FixOptions};
|
||||
pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile};
|
||||
pub use self::registry::HttpTimeout;
|
||||
pub use self::registry::{configure_http_handle, http_handle_and_timeout};
|
||||
pub use self::registry::{http_handle, needs_custom_http_transport, registry_login, search};
|
||||
pub use self::registry::{configure_http_handle, http_handle, http_handle_and_timeout};
|
||||
pub use self::registry::{modify_owners, yank, OwnersOptions, PublishOpts};
|
||||
pub use self::registry::{needs_custom_http_transport, registry_login, registry_logout, search};
|
||||
pub use self::registry::{publish, registry_configuration, RegistryConfig};
|
||||
pub use self::resolve::{
|
||||
add_overrides, get_resolved_packages, resolve_with_previous, resolve_ws, resolve_ws_with_opts,
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashSet};
|
|||
use std::fs::File;
|
||||
use std::io::{self, BufRead};
|
||||
use std::iter::repeat;
|
||||
use std::path::PathBuf;
|
||||
use std::str;
|
||||
use std::time::Duration;
|
||||
use std::{cmp, env};
|
||||
|
@ -25,14 +26,19 @@ use crate::util::IntoUrl;
|
|||
use crate::util::{paths, validate_package_name};
|
||||
use crate::{drop_print, drop_println, version};
|
||||
|
||||
mod auth;
|
||||
|
||||
/// Registry settings loaded from config files.
|
||||
///
|
||||
/// This is loaded based on the `--registry` flag and the config settings.
|
||||
#[derive(Debug)]
|
||||
pub struct RegistryConfig {
|
||||
/// The index URL. If `None`, use crates.io.
|
||||
pub index: Option<String>,
|
||||
/// The authentication token.
|
||||
pub token: Option<String>,
|
||||
/// Process used for fetching a token.
|
||||
pub credential_process: Option<(PathBuf, Vec<String>)>,
|
||||
}
|
||||
|
||||
pub struct PublishOpts<'cfg> {
|
||||
|
@ -83,7 +89,7 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
|
|||
}
|
||||
}
|
||||
|
||||
let (mut registry, reg_id) = registry(
|
||||
let (mut registry, _reg_cfg, reg_id) = registry(
|
||||
opts.config,
|
||||
opts.token.clone(),
|
||||
opts.index.clone(),
|
||||
|
@ -346,27 +352,64 @@ fn transmit(
|
|||
/// `None`, `index` is set to `None` to indicate it should use crates.io.
|
||||
pub fn registry_configuration(
|
||||
config: &Config,
|
||||
registry: Option<String>,
|
||||
registry: Option<&str>,
|
||||
) -> CargoResult<RegistryConfig> {
|
||||
// `registry.default` is handled in command-line parsing.
|
||||
let (index, token) = match registry {
|
||||
let (index, token, process, token_key, proc_key) = match registry {
|
||||
Some(registry) => {
|
||||
validate_package_name(®istry, "registry name", "")?;
|
||||
(
|
||||
Some(config.get_registry_index(®istry)?.to_string()),
|
||||
config
|
||||
.get_string(&format!("registries.{}.token", registry))?
|
||||
.map(|p| p.val),
|
||||
)
|
||||
let index = Some(config.get_registry_index(®istry)?.to_string());
|
||||
let token_key = format!("registries.{}.token", registry);
|
||||
let token = config.get_string(&token_key)?.map(|p| p.val);
|
||||
let mut proc_key = format!("registries.{}.credential-process", registry);
|
||||
let mut process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
|
||||
if process.is_none() {
|
||||
proc_key = String::from("registry.credential-process");
|
||||
process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
|
||||
}
|
||||
(index, token, process, token_key, proc_key)
|
||||
}
|
||||
None => {
|
||||
// Use crates.io default.
|
||||
config.check_registry_index_not_set()?;
|
||||
(None, config.get_string("registry.token")?.map(|p| p.val))
|
||||
let token = config.get_string("registry.token")?.map(|p| p.val);
|
||||
let process =
|
||||
config.get::<Option<config::PathAndArgs>>("registry.credential-process")?;
|
||||
(
|
||||
None,
|
||||
token,
|
||||
process,
|
||||
String::from("registry.token"),
|
||||
String::from("registry.credential-process"),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(RegistryConfig { index, token })
|
||||
let process = if config.cli_unstable().credential_process {
|
||||
if token.is_some() && process.is_some() {
|
||||
config.shell().warn(format!(
|
||||
"both `{TOKEN_KEY}` and `{PROC_KEY}` \
|
||||
were specified in the config, only `{TOKEN_KEY}` will be used\n\
|
||||
Specify only one value to silence this warning.",
|
||||
TOKEN_KEY = token_key,
|
||||
PROC_KEY = proc_key,
|
||||
))?;
|
||||
None
|
||||
} else {
|
||||
process
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let credential_process =
|
||||
process.map(|process| (process.path.resolve_program(config), process.args));
|
||||
|
||||
Ok(RegistryConfig {
|
||||
index,
|
||||
token,
|
||||
credential_process,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the `Registry` and `Source` based on command-line and config settings.
|
||||
|
@ -387,17 +430,14 @@ fn registry(
|
|||
registry: Option<String>,
|
||||
force_update: bool,
|
||||
validate_token: bool,
|
||||
) -> CargoResult<(Registry, SourceId)> {
|
||||
) -> CargoResult<(Registry, RegistryConfig, SourceId)> {
|
||||
if index.is_some() && registry.is_some() {
|
||||
// Otherwise we would silently ignore one or the other.
|
||||
bail!("both `--index` and `--registry` should not be set at the same time");
|
||||
}
|
||||
// Parse all configuration options
|
||||
let RegistryConfig {
|
||||
token: token_config,
|
||||
index: index_config,
|
||||
} = registry_configuration(config, registry.clone())?;
|
||||
let opt_index = index_config.as_ref().or_else(|| index.as_ref());
|
||||
let reg_cfg = registry_configuration(config, registry.as_deref())?;
|
||||
let opt_index = reg_cfg.index.as_ref().or_else(|| index.as_ref());
|
||||
let sid = get_source_id(config, opt_index, registry.as_ref())?;
|
||||
if !sid.is_remote_registry() {
|
||||
bail!(
|
||||
|
@ -426,52 +466,49 @@ fn registry(
|
|||
cfg.and_then(|cfg| cfg.api)
|
||||
.ok_or_else(|| format_err!("{} does not support API commands", sid))?
|
||||
};
|
||||
let token = match (&index, &token, &token_config) {
|
||||
// No token.
|
||||
(None, None, None) => {
|
||||
if validate_token {
|
||||
bail!("no upload token found, please run `cargo login` or pass `--token`");
|
||||
let token = if validate_token {
|
||||
if index.is_some() {
|
||||
if !token.is_some() {
|
||||
bail!("command-line argument --index requires --token to be specified");
|
||||
}
|
||||
None
|
||||
}
|
||||
// Token on command-line.
|
||||
(_, Some(_), _) => token,
|
||||
// Token in config, no --index, loading from config is OK for crates.io.
|
||||
(None, None, Some(_)) => {
|
||||
token
|
||||
} else {
|
||||
// Check `is_default_registry` so that the crates.io index can
|
||||
// change config.json's "api" value, and this won't affect most
|
||||
// people. It will affect those using source replacement, but
|
||||
// hopefully that's a relatively small set of users.
|
||||
if registry.is_none()
|
||||
if token.is_none()
|
||||
&& reg_cfg.token.is_some()
|
||||
&& registry.is_none()
|
||||
&& !sid.is_default_registry()
|
||||
&& !crates_io::is_url_crates_io(&api_host)
|
||||
{
|
||||
if validate_token {
|
||||
config.shell().warn(
|
||||
"using `registry.token` config value with source \
|
||||
config.shell().warn(
|
||||
"using `registry.token` config value with source \
|
||||
replacement is deprecated\n\
|
||||
This may become a hard error in the future; \
|
||||
see <https://github.com/rust-lang/cargo/issues/xxx>.\n\
|
||||
Use the --token command-line flag to remove this warning.",
|
||||
)?;
|
||||
token_config
|
||||
} else {
|
||||
None
|
||||
}
|
||||
)?;
|
||||
reg_cfg.token.clone()
|
||||
} else {
|
||||
token_config
|
||||
let token = auth::auth_token(
|
||||
config,
|
||||
token.as_deref(),
|
||||
reg_cfg.token.as_deref(),
|
||||
reg_cfg.credential_process.as_ref(),
|
||||
registry.as_deref(),
|
||||
&api_host,
|
||||
)?;
|
||||
log::debug!("found token {:?}", token);
|
||||
Some(token)
|
||||
}
|
||||
}
|
||||
// --index, no --token
|
||||
(Some(_), None, _) => {
|
||||
if validate_token {
|
||||
bail!("command-line argument --index requires --token to be specified")
|
||||
}
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let handle = http_handle(config)?;
|
||||
Ok((Registry::new_handle(api_host, token, handle), sid))
|
||||
Ok((Registry::new_handle(api_host, token, handle), reg_cfg, sid))
|
||||
}
|
||||
|
||||
/// Creates a new HTTP handle with appropriate global configuration for cargo.
|
||||
|
@ -674,7 +711,7 @@ pub fn registry_login(
|
|||
token: Option<String>,
|
||||
reg: Option<String>,
|
||||
) -> CargoResult<()> {
|
||||
let (registry, _) = registry(config, token.clone(), None, reg.clone(), false, false)?;
|
||||
let (registry, reg_cfg, _) = registry(config, token.clone(), None, reg.clone(), false, false)?;
|
||||
|
||||
let token = match token {
|
||||
Some(token) => token,
|
||||
|
@ -696,18 +733,21 @@ pub fn registry_login(
|
|||
}
|
||||
};
|
||||
|
||||
let RegistryConfig {
|
||||
token: old_token, ..
|
||||
} = registry_configuration(config, reg.clone())?;
|
||||
|
||||
if let Some(old_token) = old_token {
|
||||
if old_token == token {
|
||||
if let Some(old_token) = ®_cfg.token {
|
||||
if old_token == &token {
|
||||
config.shell().status("Login", "already logged in")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
config::save_credentials(config, token, reg.clone())?;
|
||||
auth::login(
|
||||
config,
|
||||
token,
|
||||
reg_cfg.credential_process.as_ref(),
|
||||
reg.as_deref(),
|
||||
registry.host(),
|
||||
)?;
|
||||
|
||||
config.shell().status(
|
||||
"Login",
|
||||
format!(
|
||||
|
@ -718,6 +758,32 @@ pub fn registry_login(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn registry_logout(config: &Config, reg: Option<String>) -> CargoResult<()> {
|
||||
let (registry, reg_cfg, _) = registry(config, None, None, reg.clone(), false, false)?;
|
||||
let reg_name = reg.as_deref().unwrap_or("crates.io");
|
||||
if reg_cfg.credential_process.is_none() && reg_cfg.token.is_none() {
|
||||
config.shell().status(
|
||||
"Logout",
|
||||
format!("not currently logged in to `{}`", reg_name),
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
auth::logout(
|
||||
config,
|
||||
reg_cfg.credential_process.as_ref(),
|
||||
reg.as_deref(),
|
||||
registry.host(),
|
||||
)?;
|
||||
config.shell().status(
|
||||
"Logout",
|
||||
format!(
|
||||
"token for `{}` has been removed from local storage",
|
||||
reg_name
|
||||
),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct OwnersOptions {
|
||||
pub krate: Option<String>,
|
||||
pub token: Option<String>,
|
||||
|
@ -738,7 +804,7 @@ pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> {
|
|||
}
|
||||
};
|
||||
|
||||
let (mut registry, _) = registry(
|
||||
let (mut registry, _, _) = registry(
|
||||
config,
|
||||
opts.token.clone(),
|
||||
opts.index.clone(),
|
||||
|
@ -805,7 +871,7 @@ pub fn yank(
|
|||
None => bail!("a version must be specified to yank"),
|
||||
};
|
||||
|
||||
let (mut registry, _) = registry(config, token, index, reg, true, true)?;
|
||||
let (mut registry, _, _) = registry(config, token, index, reg, true, true)?;
|
||||
|
||||
if undo {
|
||||
config
|
||||
|
@ -865,7 +931,7 @@ pub fn search(
|
|||
prefix
|
||||
}
|
||||
|
||||
let (mut registry, source_id) = registry(config, None, index, reg, false, false)?;
|
||||
let (mut registry, _, source_id) = registry(config, None, index, reg, false, false)?;
|
||||
let (crates, total_crates) = registry
|
||||
.search(query, limit)
|
||||
.chain_err(|| "failed to retrieve search results from the registry")?;
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
//! Registry authentication support.
|
||||
|
||||
use crate::sources::CRATES_IO_REGISTRY;
|
||||
use crate::util::{config, process_error, CargoResult, CargoResultExt, Config};
|
||||
use anyhow::bail;
|
||||
use anyhow::format_err;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
enum Action {
|
||||
Get,
|
||||
Store(String),
|
||||
Erase,
|
||||
}
|
||||
|
||||
/// Returns the token to use for the given registry.
|
||||
pub(super) fn auth_token(
|
||||
config: &Config,
|
||||
cli_token: Option<&str>,
|
||||
config_token: Option<&str>,
|
||||
credential_process: Option<&(PathBuf, Vec<String>)>,
|
||||
registry_name: Option<&str>,
|
||||
api_url: &str,
|
||||
) -> CargoResult<String> {
|
||||
let token = match (cli_token, config_token, credential_process) {
|
||||
(None, None, None) => {
|
||||
bail!("no upload token found, please run `cargo login` or pass `--token`");
|
||||
}
|
||||
(Some(cli_token), _, _) => cli_token.to_string(),
|
||||
(None, Some(config_token), _) => config_token.to_string(),
|
||||
(None, None, Some(process)) => {
|
||||
let registry_name = registry_name.unwrap_or(CRATES_IO_REGISTRY);
|
||||
run_command(config, process, registry_name, api_url, Action::Get)?.unwrap()
|
||||
}
|
||||
};
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Saves the given token.
|
||||
pub(super) fn login(
|
||||
config: &Config,
|
||||
token: String,
|
||||
credential_process: Option<&(PathBuf, Vec<String>)>,
|
||||
registry_name: Option<&str>,
|
||||
api_url: &str,
|
||||
) -> CargoResult<()> {
|
||||
if let Some(process) = credential_process {
|
||||
let registry_name = registry_name.unwrap_or(CRATES_IO_REGISTRY);
|
||||
run_command(
|
||||
config,
|
||||
process,
|
||||
registry_name,
|
||||
api_url,
|
||||
Action::Store(token),
|
||||
)?;
|
||||
} else {
|
||||
config::save_credentials(config, Some(token), registry_name)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the token for the given registry.
|
||||
pub(super) fn logout(
|
||||
config: &Config,
|
||||
credential_process: Option<&(PathBuf, Vec<String>)>,
|
||||
registry_name: Option<&str>,
|
||||
api_url: &str,
|
||||
) -> CargoResult<()> {
|
||||
if let Some(process) = credential_process {
|
||||
let registry_name = registry_name.unwrap_or(CRATES_IO_REGISTRY);
|
||||
run_command(config, process, registry_name, api_url, Action::Erase)?;
|
||||
} else {
|
||||
config::save_credentials(config, None, registry_name)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_command(
|
||||
config: &Config,
|
||||
process: &(PathBuf, Vec<String>),
|
||||
name: &str,
|
||||
api_url: &str,
|
||||
action: Action,
|
||||
) -> CargoResult<Option<String>> {
|
||||
let cred_proc;
|
||||
let (exe, args) = if process.0.to_str().unwrap_or("").starts_with("cargo:") {
|
||||
cred_proc = sysroot_credential(config, process)?;
|
||||
&cred_proc
|
||||
} else {
|
||||
process
|
||||
};
|
||||
if !args.iter().any(|arg| arg.contains("{action}")) {
|
||||
let msg = |which| {
|
||||
format!(
|
||||
"credential process `{}` cannot be used to {}, \
|
||||
the credential-process configuration value must pass the \
|
||||
`{{action}}` argument in the config to support this command",
|
||||
exe.display(),
|
||||
which
|
||||
)
|
||||
};
|
||||
match action {
|
||||
Action::Get => {}
|
||||
Action::Store(_) => bail!(msg("log in")),
|
||||
Action::Erase => bail!(msg("log out")),
|
||||
}
|
||||
}
|
||||
let action_str = match action {
|
||||
Action::Get => "get",
|
||||
Action::Store(_) => "store",
|
||||
Action::Erase => "erase",
|
||||
};
|
||||
let args: Vec<_> = args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
arg.replace("{action}", action_str)
|
||||
.replace("{name}", name)
|
||||
.replace("{api_url}", api_url)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut cmd = Command::new(&exe);
|
||||
cmd.args(args)
|
||||
.env("CARGO", config.cargo_exe()?)
|
||||
.env("CARGO_REGISTRY_NAME", name)
|
||||
.env("CARGO_REGISTRY_API_URL", api_url);
|
||||
match action {
|
||||
Action::Get => {
|
||||
cmd.stdout(Stdio::piped());
|
||||
}
|
||||
Action::Store(_) => {
|
||||
cmd.stdin(Stdio::piped());
|
||||
}
|
||||
Action::Erase => {}
|
||||
}
|
||||
let mut child = cmd.spawn().chain_err(|| {
|
||||
let verb = match action {
|
||||
Action::Get => "fetch",
|
||||
Action::Store(_) => "store",
|
||||
Action::Erase => "erase",
|
||||
};
|
||||
format!(
|
||||
"failed to execute `{}` to {} authentication token for registry `{}`",
|
||||
exe.display(),
|
||||
verb,
|
||||
name
|
||||
)
|
||||
})?;
|
||||
let mut token = None;
|
||||
match &action {
|
||||
Action::Get => {
|
||||
let mut buffer = String::new();
|
||||
log::debug!("reading into buffer");
|
||||
child
|
||||
.stdout
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.read_to_string(&mut buffer)
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"failed to read token from registry credential process `{}`",
|
||||
exe.display()
|
||||
)
|
||||
})?;
|
||||
if let Some(end) = buffer.find('\n') {
|
||||
buffer.truncate(end);
|
||||
}
|
||||
token = Some(buffer);
|
||||
}
|
||||
Action::Store(token) => {
|
||||
writeln!(child.stdin.as_ref().unwrap(), "{}", token).chain_err(|| {
|
||||
format!(
|
||||
"failed to send token to registry credential process `{}`",
|
||||
exe.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Action::Erase => {}
|
||||
}
|
||||
let status = child.wait().chain_err(|| {
|
||||
format!(
|
||||
"registry credential process `{}` exit failure",
|
||||
exe.display()
|
||||
)
|
||||
})?;
|
||||
if !status.success() {
|
||||
let msg = match action {
|
||||
Action::Get => "failed to authenticate to registry",
|
||||
Action::Store(_) => "failed to store token to registry",
|
||||
Action::Erase => "failed to erase token from registry",
|
||||
};
|
||||
return Err(process_error(
|
||||
&format!(
|
||||
"registry credential process `{}` {} `{}`",
|
||||
exe.display(),
|
||||
msg,
|
||||
name
|
||||
),
|
||||
Some(status),
|
||||
None,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Gets the path to the libexec processes in the sysroot.
|
||||
fn sysroot_credential(
|
||||
config: &Config,
|
||||
process: &(PathBuf, Vec<String>),
|
||||
) -> CargoResult<(PathBuf, Vec<String>)> {
|
||||
let cred_name = process.0.to_str().unwrap().strip_prefix("cargo:").unwrap();
|
||||
let cargo = config.cargo_exe()?;
|
||||
let root = cargo
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.ok_or_else(|| format_err!("expected cargo path {}", cargo.display()))?;
|
||||
let exe = root.join("libexec").join(format!(
|
||||
"cargo-credential-{}{}",
|
||||
cred_name,
|
||||
std::env::consts::EXE_SUFFIX
|
||||
));
|
||||
let mut args = process.1.clone();
|
||||
if !args.iter().any(|arg| arg == "{action}") {
|
||||
args.push("{action}".to_string());
|
||||
}
|
||||
Ok((exe, args))
|
||||
}
|
|
@ -63,7 +63,7 @@ use std::str::FromStr;
|
|||
use std::sync::Once;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use anyhow::{anyhow, bail, format_err};
|
||||
use curl::easy::Easy;
|
||||
use lazycell::LazyCell;
|
||||
use serde::Deserialize;
|
||||
|
@ -1620,7 +1620,11 @@ pub fn homedir(cwd: &Path) -> Option<PathBuf> {
|
|||
::home::cargo_home_with_cwd(cwd).ok()
|
||||
}
|
||||
|
||||
pub fn save_credentials(cfg: &Config, token: String, registry: Option<String>) -> CargoResult<()> {
|
||||
pub fn save_credentials(
|
||||
cfg: &Config,
|
||||
token: Option<String>,
|
||||
registry: Option<&str>,
|
||||
) -> CargoResult<()> {
|
||||
// If 'credentials.toml' exists, we should write to that, otherwise
|
||||
// use the legacy 'credentials'. There's no need to print the warning
|
||||
// here, because it would already be printed at load time.
|
||||
|
@ -1639,25 +1643,6 @@ pub fn save_credentials(cfg: &Config, token: String, registry: Option<String>) -
|
|||
.open_rw(filename, cfg, "credentials' config file")?
|
||||
};
|
||||
|
||||
let (key, mut value) = {
|
||||
let key = "token".to_string();
|
||||
let value = ConfigValue::String(token, Definition::Path(file.path().to_path_buf()));
|
||||
let mut map = HashMap::new();
|
||||
map.insert(key, value);
|
||||
let table = CV::Table(map, Definition::Path(file.path().to_path_buf()));
|
||||
|
||||
if let Some(registry) = registry.clone() {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(registry, table);
|
||||
(
|
||||
"registries".into(),
|
||||
CV::Table(map, Definition::Path(file.path().to_path_buf())),
|
||||
)
|
||||
} else {
|
||||
("registry".into(), table)
|
||||
}
|
||||
};
|
||||
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).chain_err(|| {
|
||||
format!(
|
||||
|
@ -1677,13 +1662,55 @@ pub fn save_credentials(cfg: &Config, token: String, registry: Option<String>) -
|
|||
.insert("registry".into(), map.into());
|
||||
}
|
||||
|
||||
if registry.is_some() {
|
||||
if let Some(table) = toml.as_table_mut().unwrap().remove("registries") {
|
||||
let v = CV::from_toml(Definition::Path(file.path().to_path_buf()), table)?;
|
||||
value.merge(v, false)?;
|
||||
if let Some(token) = token {
|
||||
// login
|
||||
let (key, mut value) = {
|
||||
let key = "token".to_string();
|
||||
let value = ConfigValue::String(token, Definition::Path(file.path().to_path_buf()));
|
||||
let mut map = HashMap::new();
|
||||
map.insert(key, value);
|
||||
let table = CV::Table(map, Definition::Path(file.path().to_path_buf()));
|
||||
|
||||
if let Some(registry) = registry {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(registry.to_string(), table);
|
||||
(
|
||||
"registries".into(),
|
||||
CV::Table(map, Definition::Path(file.path().to_path_buf())),
|
||||
)
|
||||
} else {
|
||||
("registry".into(), table)
|
||||
}
|
||||
};
|
||||
|
||||
if registry.is_some() {
|
||||
if let Some(table) = toml.as_table_mut().unwrap().remove("registries") {
|
||||
let v = CV::from_toml(Definition::Path(file.path().to_path_buf()), table)?;
|
||||
value.merge(v, false)?;
|
||||
}
|
||||
}
|
||||
toml.as_table_mut().unwrap().insert(key, value.into_toml());
|
||||
} else {
|
||||
// logout
|
||||
let table = toml.as_table_mut().unwrap();
|
||||
if let Some(registry) = registry {
|
||||
if let Some(registries) = table.get_mut("registries") {
|
||||
if let Some(reg) = registries.get_mut(registry) {
|
||||
let rtable = reg.as_table_mut().ok_or_else(|| {
|
||||
format_err!("expected `[registries.{}]` to be a table", registry)
|
||||
})?;
|
||||
rtable.remove("token");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(registry) = table.get_mut("registry") {
|
||||
let reg_table = registry
|
||||
.as_table_mut()
|
||||
.ok_or_else(|| format_err!("expected `[registry]` to be a table"))?;
|
||||
reg_table.remove("token");
|
||||
}
|
||||
}
|
||||
}
|
||||
toml.as_table_mut().unwrap().insert(key, value.into_toml());
|
||||
|
||||
let contents = toml.to_string();
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
|
|
|
@ -961,3 +961,168 @@ std = ["serde?/std"]
|
|||
In this example, the `std` feature enables the `std` feature on the `serde`
|
||||
dependency. However, unlike the normal `serde/std` syntax, it will not enable
|
||||
the optional dependency `serde` unless something else has included it.
|
||||
|
||||
### credential-process
|
||||
* Tracking Issue: [#XXXX](https://github.com/rust-lang/cargo/issues/XXXX)
|
||||
* RFC: [#2730](https://github.com/rust-lang/rfcs/pull/2730)
|
||||
|
||||
The `credential-process` feature adds a config setting to fetch registry
|
||||
authentication tokens by calling an external process.
|
||||
|
||||
Token authentication is used by the [`cargo login`], [`cargo publish`],
|
||||
[`cargo owner`], and [`cargo yank`] commands. Additionally, this feature adds
|
||||
a new `cargo logout` command.
|
||||
|
||||
To use this feature, you must pass the `-Z credential-process` flag on the
|
||||
command-line. Additionally, you must remove any current tokens currently saved
|
||||
in the [`credentials` file] (which can be done with the new `logout` command).
|
||||
|
||||
#### `credential-process` Configuration
|
||||
|
||||
To configure which process to run to fetch the token, specify the process in
|
||||
the `registry` table in a [config file]:
|
||||
|
||||
```toml
|
||||
[registry]
|
||||
credential-process = "/usr/bin/cargo-creds"
|
||||
```
|
||||
|
||||
If you want to use a different process for a specific registry, it can be
|
||||
specified in the `registries` table:
|
||||
|
||||
```toml
|
||||
[registries.my-registry]
|
||||
credential-process = "/usr/bin/cargo-creds"
|
||||
```
|
||||
|
||||
The value can be a string with spaces separating arguments or it can be a TOML
|
||||
array of strings.
|
||||
|
||||
Command-line arguments allow special placeholders which will be replaced with
|
||||
the corresponding value:
|
||||
|
||||
* `{name}` — The name of the registry.
|
||||
* `{api_url}` — The base URL of the registry API endpoints.
|
||||
* `{action}` — The authentication action (described below).
|
||||
|
||||
Process names with the prefix `cargo:` are loaded from the `libexec` directory
|
||||
next to cargo. Several experimental credential wrappers are included with
|
||||
Cargo, and this provides convenient access to them:
|
||||
|
||||
```toml
|
||||
[registry]
|
||||
credential-process = "cargo:macos-keychain"
|
||||
```
|
||||
|
||||
The current wrappers are:
|
||||
|
||||
* `cargo:macos-keychain`: Uses the macOS Keychain to store the token.
|
||||
* `cargo:gnome-secret`: Uses
|
||||
[libsecret](https://wiki.gnome.org/Projects/Libsecret) to store the token on
|
||||
Linux systems running GNOME.
|
||||
* `cargo:wincred`: Uses the Windows Credential Manager to store the token.
|
||||
* `cargo:1password`: Uses the 1password `op` CLI to store the token. You must
|
||||
install the `op` CLI from the [1password
|
||||
website](https://1password.com/downloads/command-line/). You must run `op
|
||||
signin` at least once with the appropriate arguments (such as `op signin
|
||||
my.1password.com user@example.com`), unless you provide the sign-in-address
|
||||
and email arguments. The master password will be required on each request
|
||||
unless the appropriate `OP_SESSION` environment variable is set. It supports
|
||||
the following command-line arguments:
|
||||
* `--account`: The account shorthand name to use.
|
||||
* `--vault`: The vault name to use.
|
||||
* `--sign-in-address`: The sign-in-address, which is a web address such as `my.1password.com`.
|
||||
* `--email`: The email address to sign in with.
|
||||
|
||||
#### `credential-process` Interface
|
||||
|
||||
There are two different kinds of token processes that Cargo supports. The
|
||||
simple "basic" kind will only be called by Cargo when it needs a token. This
|
||||
is intended for simple and easy integration with password managers, that can
|
||||
often use pre-existing tooling. The more advanced "Cargo" kind supports
|
||||
different actions passed as a command-line argument. This is intended for more
|
||||
pleasant integration experience, at the expense of requiring a Cargo-specific
|
||||
process to glue to the password manager. Cargo will determine which kind is
|
||||
supported by the `credential-process` definition. If it contains the
|
||||
`{action}` argument, then it uses the advanced style, otherwise it assumes it
|
||||
only supports the "basic" kind.
|
||||
|
||||
##### Basic authenticator
|
||||
|
||||
A basic authenticator is a process that returns a token on stdout. Newlines
|
||||
will be trimmed. The process inherits the user's stdin and stderr. It should
|
||||
exit 0 on success, and nonzero on error.
|
||||
|
||||
With this form, [`cargo login`] and `cargo logout` are not supported and
|
||||
return an error if used.
|
||||
|
||||
##### Cargo authenticator
|
||||
|
||||
The protocol between the Cargo and the process is very basic, intended to
|
||||
ensure the credential process is kept as simple as possible. Cargo will
|
||||
execute the process with the `{action}` argument indicating which action to
|
||||
perform:
|
||||
|
||||
* `store` — Store the given token in secure storage.
|
||||
* `get` — Get a token from storage.
|
||||
* `erase` — Remove a token from storage.
|
||||
|
||||
The `cargo login` command uses `store` to save a token. Commands that require
|
||||
authentication, like `cargo publish`, uses `get` to retrieve a token. `cargo
|
||||
logout` uses the `erase` command to remove a token.
|
||||
|
||||
The process inherits the user's stderr, so the process can display messages.
|
||||
Some values are passed in via environment variables (see below). The expected
|
||||
interactions are:
|
||||
|
||||
* `store` — The token is sent to the process's stdin, terminated by a newline.
|
||||
The process should store the token keyed off the registry name. If the
|
||||
process fails, it should exit with a nonzero exit status.
|
||||
|
||||
* `get` — The process should send the token to its stdout (trailing newline
|
||||
will be trimmed). The process inherits the user's stdin, should it need to
|
||||
receive input.
|
||||
|
||||
If the process is unable to fulfill the request, it should exit with a
|
||||
nonzero exit code.
|
||||
|
||||
* `erase` — The process should remove the token associated with the registry
|
||||
name. If the token is not found, the process should exit with a 0 exit
|
||||
status.
|
||||
|
||||
##### Environment
|
||||
|
||||
The following environment variables will be provided to the executed command:
|
||||
|
||||
* `CARGO` — Path to the `cargo` binary executing the command.
|
||||
* `CARGO_REGISTRY_NAME` — Name of the registry the authentication token is for.
|
||||
* `CARGO_REGISTRY_API_URL` — The URL of the registry API.
|
||||
|
||||
#### `cargo logout`
|
||||
|
||||
A new `cargo logout` command has been added to make it easier to remove a
|
||||
token from storage. This supports both [`credentials` file] tokens and
|
||||
`credential-process` tokens.
|
||||
|
||||
When used with `credentials` file tokens, it needs the `-Z unstable-options`
|
||||
command-line option:
|
||||
|
||||
```console
|
||||
cargo logout -Z unstable-options`
|
||||
```
|
||||
|
||||
When used with the `credential-process` config, use the `-Z
|
||||
credential-process` command-line option:
|
||||
|
||||
|
||||
```console
|
||||
cargo logout -Z credential-process`
|
||||
```
|
||||
|
||||
[`cargo login`]: ../commands/cargo-login.md
|
||||
[`cargo publish`]: ../commands/cargo-publish.md
|
||||
[`cargo owner`]: ../commands/cargo-owner.md
|
||||
[`cargo yank`]: ../commands/cargo-yank.md
|
||||
[`credentials` file]: config.md#credentials
|
||||
[crates.io]: https://crates.io/
|
||||
[config file]: config.md
|
||||
|
|
|
@ -0,0 +1,450 @@
|
|||
//! Tests for credential-process.
|
||||
|
||||
use cargo_test_support::paths::CargoPathExt;
|
||||
use cargo_test_support::{basic_manifest, cargo_process, paths, project, registry, Project};
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::thread;
|
||||
use url::Url;
|
||||
|
||||
fn toml_bin(proj: &Project, name: &str) -> String {
|
||||
proj.bin(name).display().to_string().replace('\\', "\\\\")
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn gated() {
|
||||
registry::init();
|
||||
|
||||
paths::home().join(".cargo/credentials").rm_rf();
|
||||
|
||||
let p = project()
|
||||
.file(
|
||||
".cargo/config",
|
||||
r#"
|
||||
[registry]
|
||||
credential-process = "false"
|
||||
"#,
|
||||
)
|
||||
.file("Cargo.toml", &basic_manifest("foo", "1.0.0"))
|
||||
.file("src/lib.rs", "")
|
||||
.build();
|
||||
|
||||
p.cargo("publish --no-verify")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_status(101)
|
||||
.with_stderr(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[ERROR] no upload token found, please run `cargo login` or pass `--token`
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
||||
p.change_file(
|
||||
".cargo/config",
|
||||
r#"
|
||||
[registry.alternative]
|
||||
credential-process = "false"
|
||||
"#,
|
||||
);
|
||||
|
||||
p.cargo("publish --no-verify --registry alternative")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_status(101)
|
||||
.with_stderr(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[ERROR] no upload token found, please run `cargo login` or pass `--token`
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn warn_both_token_and_process() {
|
||||
// Specifying both credential-process and a token in config should issue a warning.
|
||||
registry::init();
|
||||
paths::home().join(".cargo/credentials").rm_rf();
|
||||
let p = project()
|
||||
.file(
|
||||
".cargo/config",
|
||||
r#"
|
||||
[registries.alternative]
|
||||
token = "sekrit"
|
||||
credential-process = "false"
|
||||
"#,
|
||||
)
|
||||
.file(
|
||||
"Cargo.toml",
|
||||
r#"
|
||||
[package]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
description = "foo"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
homepage = "https://example.com/"
|
||||
"#,
|
||||
)
|
||||
.file("src/lib.rs", "")
|
||||
.build();
|
||||
|
||||
p.cargo("publish --no-verify --registry alternative -Z credential-process")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_stderr(
|
||||
"\
|
||||
[WARNING] both `registries.alternative.token` and `registries.alternative.credential-process` \
|
||||
were specified in the config, only `registries.alternative.token` will be used
|
||||
Specify only one value to silence this warning.
|
||||
[UPDATING] [..]
|
||||
[PACKAGING] foo v0.1.0 [..]
|
||||
[UPLOADING] foo v0.1.0 [..]
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
||||
// Try with global credential-process, and registry-specific `token`.
|
||||
p.change_file(
|
||||
".cargo/config",
|
||||
r#"
|
||||
[registry]
|
||||
credential-process = "false"
|
||||
|
||||
[registries.alternative]
|
||||
token = "sekrit"
|
||||
"#,
|
||||
);
|
||||
p.cargo("publish --no-verify --registry alternative -Z credential-process")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_stderr(
|
||||
"\
|
||||
[WARNING] both `registries.alternative.token` and `registry.credential-process` \
|
||||
were specified in the config, only `registries.alternative.token` will be used
|
||||
Specify only one value to silence this warning.
|
||||
[UPDATING] [..]
|
||||
[PACKAGING] foo v0.1.0 [..]
|
||||
[UPLOADING] foo v0.1.0 [..]
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
/// Setup for a test that will issue a command that needs to fetch a token.
|
||||
///
|
||||
/// This does the following:
|
||||
///
|
||||
/// * Spawn a thread that will act as an API server.
|
||||
/// * Create a simple credential-process that will generate a fake token.
|
||||
/// * Create a simple `foo` project to run the test against.
|
||||
/// * Configure the credential-process config.
|
||||
///
|
||||
/// Returns a thread handle for the API server, the test should join it when
|
||||
/// finished. Also returns the simple `foo` project to test against.
|
||||
fn get_token_test() -> (Project, thread::JoinHandle<()>) {
|
||||
let server = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = server.local_addr().unwrap();
|
||||
let api_url = format!("http://{}", addr);
|
||||
|
||||
registry::init_registry(
|
||||
registry::alt_registry_path(),
|
||||
registry::alt_dl_url(),
|
||||
Url::parse(&api_url).unwrap(),
|
||||
registry::alt_api_path(),
|
||||
);
|
||||
|
||||
// API server that checks that the token is included correctly.
|
||||
let t = thread::spawn(move || {
|
||||
let mut conn = BufReader::new(server.accept().unwrap().0);
|
||||
let headers: Vec<_> = (&mut conn)
|
||||
.lines()
|
||||
.map(|s| s.unwrap())
|
||||
.take_while(|s| s.len() > 2)
|
||||
.map(|s| s.trim().to_string())
|
||||
.collect();
|
||||
assert!(headers
|
||||
.iter()
|
||||
.any(|header| header == "Authorization: sekrit"));
|
||||
conn.get_mut()
|
||||
.write_all(
|
||||
b"HTTP/1.1 200\r\n\
|
||||
Content-Length: 33\r\n\
|
||||
\r\n\
|
||||
{\"ok\": true, \"msg\": \"completed!\"}\r\n",
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// The credential process to use.
|
||||
let cred_proj = project()
|
||||
.at("cred_proj")
|
||||
.file("Cargo.toml", &basic_manifest("test-cred", "1.0.0"))
|
||||
.file("src/main.rs", r#"fn main() { println!("sekrit"); } "#)
|
||||
.build();
|
||||
cred_proj.cargo("build").run();
|
||||
|
||||
let p = project()
|
||||
.file(
|
||||
".cargo/config",
|
||||
&format!(
|
||||
r#"
|
||||
[registries.alternative]
|
||||
index = "{}"
|
||||
credential-process = ["{}"]
|
||||
"#,
|
||||
registry::alt_registry_url(),
|
||||
toml_bin(&cred_proj, "test-cred")
|
||||
),
|
||||
)
|
||||
.file(
|
||||
"Cargo.toml",
|
||||
r#"
|
||||
[package]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
description = "foo"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
homepage = "https://example.com/"
|
||||
"#,
|
||||
)
|
||||
.file("src/lib.rs", "")
|
||||
.build();
|
||||
(p, t)
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn publish() {
|
||||
// Checks that credential-process is used for `cargo publish`.
|
||||
let (p, t) = get_token_test();
|
||||
|
||||
p.cargo("publish --no-verify --registry alternative -Z credential-process")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_stderr(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[PACKAGING] foo v0.1.0 [..]
|
||||
[UPLOADING] foo v0.1.0 [..]
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
||||
t.join().ok().unwrap();
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn basic_unsupported() {
|
||||
// Non-action commands don't support login/logout.
|
||||
registry::init();
|
||||
// If both `credential-process` and `token` are specified, it will ignore
|
||||
// `credential-process`, so remove the default tokens.
|
||||
paths::home().join(".cargo/credentials").rm_rf();
|
||||
cargo::util::paths::append(
|
||||
&paths::home().join(".cargo/config"),
|
||||
br#"
|
||||
[registry]
|
||||
credential-process = "false"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cargo_process("login -Z credential-process abcdefg")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_status(101)
|
||||
.with_stderr(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[ERROR] credential process `false` cannot be used to log in, \
|
||||
the credential-process configuration value must pass the \
|
||||
`{action}` argument in the config to support this command
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
||||
cargo_process("logout -Z credential-process")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_status(101)
|
||||
.with_stderr(
|
||||
"\
|
||||
[ERROR] credential process `false` cannot be used to log out, \
|
||||
the credential-process configuration value must pass the \
|
||||
`{action}` argument in the config to support this command
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn login() {
|
||||
registry::init();
|
||||
// The credential process to use.
|
||||
let cred_proj = project()
|
||||
.at("cred_proj")
|
||||
.file("Cargo.toml", &basic_manifest("test-cred", "1.0.0"))
|
||||
.file(
|
||||
"src/main.rs",
|
||||
&r#"
|
||||
use std::io::Read;
|
||||
|
||||
fn main() {
|
||||
assert_eq!(std::env::var("CARGO_REGISTRY_NAME").unwrap(), "crates-io");
|
||||
assert_eq!(std::env::var("CARGO_REGISTRY_API_URL").unwrap(), "__API__");
|
||||
assert_eq!(std::env::args().skip(1).next().unwrap(), "store");
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin().read_to_string(&mut buffer).unwrap();
|
||||
assert_eq!(buffer, "abcdefg\n");
|
||||
std::fs::write("token-store", buffer).unwrap();
|
||||
}
|
||||
"#
|
||||
.replace("__API__", ®istry::api_url().to_string()),
|
||||
)
|
||||
.build();
|
||||
cred_proj.cargo("build").run();
|
||||
|
||||
cargo::util::paths::append(
|
||||
&paths::home().join(".cargo/config"),
|
||||
format!(
|
||||
r#"
|
||||
[registry]
|
||||
credential-process = ["{}", "{{action}}"]
|
||||
"#,
|
||||
toml_bin(&cred_proj, "test-cred")
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cargo_process("login -Z credential-process abcdefg")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_stderr(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[LOGIN] token for `crates.io` saved
|
||||
",
|
||||
)
|
||||
.run();
|
||||
assert_eq!(
|
||||
fs::read_to_string(paths::root().join("token-store")).unwrap(),
|
||||
"abcdefg\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn logout() {
|
||||
registry::init();
|
||||
// If both `credential-process` and `token` are specified, it will ignore
|
||||
// `credential-process`, so remove the default tokens.
|
||||
paths::home().join(".cargo/credentials").rm_rf();
|
||||
// The credential process to use.
|
||||
let cred_proj = project()
|
||||
.at("cred_proj")
|
||||
.file("Cargo.toml", &basic_manifest("test-cred", "1.0.0"))
|
||||
.file(
|
||||
"src/main.rs",
|
||||
r#"
|
||||
use std::io::Read;
|
||||
|
||||
fn main() {
|
||||
assert_eq!(std::env::var("CARGO_REGISTRY_NAME").unwrap(), "crates-io");
|
||||
assert_eq!(std::env::args().skip(1).next().unwrap(), "erase");
|
||||
std::fs::write("token-store", "").unwrap();
|
||||
eprintln!("token for `{}` has been erased!",
|
||||
std::env::var("CARGO_REGISTRY_NAME").unwrap());
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
cred_proj.cargo("build").run();
|
||||
|
||||
cargo::util::paths::append(
|
||||
&paths::home().join(".cargo/config"),
|
||||
format!(
|
||||
r#"
|
||||
[registry]
|
||||
credential-process = ["{}", "{{action}}"]
|
||||
"#,
|
||||
toml_bin(&cred_proj, "test-cred")
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cargo_process("logout -Z credential-process")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_stderr(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
token for `crates-io` has been erased!
|
||||
[LOGOUT] token for `crates.io` has been removed from local storage
|
||||
",
|
||||
)
|
||||
.run();
|
||||
assert_eq!(
|
||||
fs::read_to_string(paths::root().join("token-store")).unwrap(),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn yank() {
|
||||
let (p, t) = get_token_test();
|
||||
|
||||
p.cargo("yank --vers 0.1.0 --registry alternative -Z credential-process")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_stderr(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[YANK] foo:0.1.0
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
||||
t.join().ok().unwrap();
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn owner() {
|
||||
let (p, t) = get_token_test();
|
||||
|
||||
p.cargo("owner --add username --registry alternative -Z credential-process")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_stderr(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[OWNER] completed!
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
||||
t.join().ok().unwrap();
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn libexec_path() {
|
||||
// cargo: prefixed names use the sysroot
|
||||
registry::init();
|
||||
|
||||
paths::home().join(".cargo/credentials").rm_rf();
|
||||
cargo::util::paths::append(
|
||||
&paths::home().join(".cargo/config"),
|
||||
br#"
|
||||
[registry]
|
||||
credential-process = "cargo:doesnotexist"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cargo_process("login -Z credential-process abcdefg")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_status(101)
|
||||
.with_stderr(
|
||||
&format!("\
|
||||
[UPDATING] [..]
|
||||
[ERROR] failed to execute `[..]libexec/cargo-credential-doesnotexist[EXE]` to store authentication token for registry `crates-io`
|
||||
|
||||
Caused by:
|
||||
{}
|
||||
", cargo_test_support::no_such_file_err_msg()),
|
||||
)
|
||||
.run();
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
//! Tests for the `cargo logout` command.
|
||||
|
||||
use cargo_test_support::install::cargo_home;
|
||||
use cargo_test_support::{cargo_process, registry};
|
||||
use std::fs;
|
||||
|
||||
#[cargo_test]
|
||||
fn gated() {
|
||||
registry::init();
|
||||
cargo_process("logout")
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_status(101)
|
||||
.with_stderr(
|
||||
"\
|
||||
[ERROR] the `cargo logout` command is unstable, pass `-Z unstable-options` to enable it
|
||||
See https://github.com/rust-lang/cargo/issues/8933 for more information about \
|
||||
the `cargo logout` command.
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
/// Checks whether or not the token is set for the given token.
|
||||
fn check_config_token(registry: Option<&str>, should_be_set: bool) {
|
||||
let credentials = cargo_home().join("credentials");
|
||||
let contents = fs::read_to_string(&credentials).unwrap();
|
||||
let toml: toml::Value = contents.parse().unwrap();
|
||||
if let Some(registry) = registry {
|
||||
assert_eq!(
|
||||
toml.get("registries")
|
||||
.and_then(|registries| registries.get(registry))
|
||||
.and_then(|registry| registry.get("token"))
|
||||
.is_some(),
|
||||
should_be_set
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
toml.get("registry")
|
||||
.and_then(|registry| registry.get("token"))
|
||||
.is_some(),
|
||||
should_be_set
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn simple_logout_test(reg: Option<&str>, flag: &str) {
|
||||
registry::init();
|
||||
let msg = reg.unwrap_or("crates.io");
|
||||
check_config_token(reg, true);
|
||||
cargo_process(&format!("logout -Z unstable-options {}", flag))
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_stderr(&format!(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[LOGOUT] token for `{}` has been removed from local storage
|
||||
",
|
||||
msg
|
||||
))
|
||||
.run();
|
||||
check_config_token(reg, false);
|
||||
|
||||
cargo_process(&format!("logout -Z unstable-options {}", flag))
|
||||
.masquerade_as_nightly_cargo()
|
||||
.with_stderr(&format!(
|
||||
"\
|
||||
[LOGOUT] not currently logged in to `{}`
|
||||
",
|
||||
msg
|
||||
))
|
||||
.run();
|
||||
check_config_token(reg, false);
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn default_registry() {
|
||||
simple_logout_test(None, "");
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn other_registry() {
|
||||
simple_logout_test(Some("alternative"), "--registry alternative");
|
||||
}
|
|
@ -35,6 +35,7 @@ mod config;
|
|||
mod config_cli;
|
||||
mod config_include;
|
||||
mod corrupt_git;
|
||||
mod credential_process;
|
||||
mod cross_compile;
|
||||
mod cross_publish;
|
||||
mod custom_target;
|
||||
|
@ -65,6 +66,7 @@ mod local_registry;
|
|||
mod locate_project;
|
||||
mod lockfile_compat;
|
||||
mod login;
|
||||
mod logout;
|
||||
mod lto;
|
||||
mod member_discovery;
|
||||
mod member_errors;
|
||||
|
|
Loading…
Reference in New Issue