diff --git a/.config/nextest.toml b/.config/nextest.toml index cc1a18dbe..a6c344d22 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -2,3 +2,10 @@ # Mark tests that take longer than 10s as slow. # Terminate after 120s as a stop-gap measure to terminate on deadlock. slow-timeout = { period = "10s", terminate-after = 12 } + +[test-groups] +serial = { max-threads = 1 } + +[[profile.default.overrides]] +filter = 'test(native_keyring)' +test-group = 'serial' diff --git a/Cargo.lock b/Cargo.lock index 9daf3d3ec..0a572895c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5089,9 +5089,11 @@ dependencies = [ "serde", "tempfile", "test-log", + "thiserror 2.0.16", "tokio", "tracing", "url", + "uv-keyring", "uv-once-map", "uv-redacted", "uv-small-str", @@ -5407,6 +5409,7 @@ dependencies = [ "uv-pep440", "uv-pep508", "uv-platform-tags", + "uv-redacted", "uv-static", ] diff --git a/Cargo.toml b/Cargo.toml index c56753d4a..96c0b38d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ uv-git-types = { path = "crates/uv-git-types" } uv-globfilter = { path = "crates/uv-globfilter" } uv-install-wheel = { path = "crates/uv-install-wheel", default-features = false } uv-installer = { path = "crates/uv-installer" } +uv-keyring = { path = "crates/uv-keyring" } uv-logging = { path = "crates/uv-logging" } uv-macros = { path = "crates/uv-macros" } uv-metadata = { path = "crates/uv-metadata" } diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml index cbe4d4787..d3f288ab9 100644 --- a/crates/uv-auth/Cargo.toml +++ b/crates/uv-auth/Cargo.toml @@ -10,6 +10,7 @@ doctest = false workspace = true [dependencies] +uv-keyring = { workspace = true, features = ["apple-native", "secret-service", "windows-native"] } uv-once-map = { workspace = true } uv-redacted = { workspace = true } uv-small-str = { workspace = true } @@ -28,6 +29,7 @@ rust-netrc = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } url = { workspace = true } diff --git a/crates/uv-auth/src/keyring.rs b/crates/uv-auth/src/keyring.rs index 41b92114a..4ee9e0ba5 100644 --- a/crates/uv-auth/src/keyring.rs +++ b/crates/uv-auth/src/keyring.rs @@ -1,11 +1,14 @@ use std::{io::Write, process::Stdio}; use tokio::process::Command; -use tracing::{instrument, trace, warn}; +use tracing::{debug, instrument, trace, warn}; use uv_redacted::DisplaySafeUrl; use uv_warnings::warn_user_once; use crate::credentials::Credentials; +/// Service name prefix for storing credentials in a keyring. +static UV_SERVICE_PREFIX: &str = "uv:"; + /// A backend for retrieving credentials from a keyring. /// /// See pip's implementation for reference @@ -15,15 +18,47 @@ pub struct KeyringProvider { backend: KeyringProviderBackend, } -#[derive(Debug)] -pub(crate) enum KeyringProviderBackend { - /// Use the `keyring` command to fetch credentials. +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Keyring(#[from] uv_keyring::Error), + + #[error("The '{0}' keyring provider does not support storing credentials")] + StoreUnsupported(KeyringProviderBackend), + + #[error("The '{0}' keyring provider does not support removing credentials")] + RemoveUnsupported(KeyringProviderBackend), +} + +#[derive(Debug, Clone)] +pub enum KeyringProviderBackend { + /// Use a native system keyring integration for credentials. + Native, + /// Use the external `keyring` command for credentials. Subprocess, #[cfg(test)] Dummy(Vec<(String, &'static str, &'static str)>), } +impl std::fmt::Display for KeyringProviderBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Native => write!(f, "native"), + Self::Subprocess => write!(f, "subprocess"), + #[cfg(test)] + Self::Dummy(_) => write!(f, "dummy"), + } + } +} + impl KeyringProvider { + /// Create a new [`KeyringProvider::Native`]. + pub fn native() -> Self { + Self { + backend: KeyringProviderBackend::Native, + } + } + /// Create a new [`KeyringProvider::Subprocess`]. pub fn subprocess() -> Self { Self { @@ -31,6 +66,84 @@ impl KeyringProvider { } } + /// Store credentials for the given [`DisplaySafeUrl`] to the keyring. + /// + /// Only [`KeyringProviderBackend::Native`] is supported at this time. + #[instrument(skip_all, fields(url = % url.to_string(), username))] + pub async fn store( + &self, + url: &DisplaySafeUrl, + credentials: &Credentials, + ) -> Result { + let Some(username) = credentials.username() else { + trace!("Unable to store credentials in keyring for {url} due to missing username"); + return Ok(false); + }; + let Some(password) = credentials.password() else { + trace!("Unable to store credentials in keyring for {url} due to missing password"); + return Ok(false); + }; + + match &self.backend { + KeyringProviderBackend::Native => { + self.store_native(url.as_str(), username, password).await?; + Ok(true) + } + KeyringProviderBackend::Subprocess => { + Err(Error::StoreUnsupported(self.backend.clone())) + } + #[cfg(test)] + KeyringProviderBackend::Dummy(_) => Err(Error::StoreUnsupported(self.backend.clone())), + } + } + + /// Store credentials to the system keyring. + #[instrument(skip(self))] + async fn store_native( + &self, + service: &str, + username: &str, + password: &str, + ) -> Result<(), Error> { + let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}"); + let entry = uv_keyring::Entry::new(&prefixed_service, username)?; + entry.set_password(password).await?; + Ok(()) + } + + /// Remove credentials for the given [`DisplaySafeUrl`] and username from the keyring. + /// + /// Only [`KeyringProviderBackend::Native`] is supported at this time. + #[instrument(skip_all, fields(url = % url.to_string(), username))] + pub async fn remove(&self, url: &DisplaySafeUrl, username: &str) -> Result<(), Error> { + match &self.backend { + KeyringProviderBackend::Native => { + self.remove_native(url.as_str(), username).await?; + Ok(()) + } + KeyringProviderBackend::Subprocess => { + Err(Error::RemoveUnsupported(self.backend.clone())) + } + #[cfg(test)] + KeyringProviderBackend::Dummy(_) => Err(Error::RemoveUnsupported(self.backend.clone())), + } + } + + /// Remove credentials from the system keyring for the given `service_name`/`username` + /// pair. + #[instrument(skip(self))] + async fn remove_native( + &self, + service_name: &str, + username: &str, + ) -> Result<(), uv_keyring::Error> { + let prefixed_service = format!("{UV_SERVICE_PREFIX}{service_name}"); + let entry = uv_keyring::Entry::new(&prefixed_service, username)?; + entry.delete_credential().await?; + trace!("Removed credentials for {username}@{service_name} from system keyring"); + Ok(()) + } + /// Fetch credentials for the given [`Url`] from the keyring. /// /// Returns [`None`] if no password was found for the username or if any errors @@ -55,6 +168,7 @@ impl KeyringProvider { // trace!("Checking keyring for URL {url}"); let mut credentials = match self.backend { + KeyringProviderBackend::Native => self.fetch_native(url.as_str(), username).await, KeyringProviderBackend::Subprocess => { self.fetch_subprocess(url.as_str(), username).await } @@ -72,6 +186,7 @@ impl KeyringProvider { }; trace!("Checking keyring for host {host}"); credentials = match self.backend { + KeyringProviderBackend::Native => self.fetch_native(&host, username).await, KeyringProviderBackend::Subprocess => self.fetch_subprocess(&host, username).await, #[cfg(test)] KeyringProviderBackend::Dummy(ref store) => { @@ -175,6 +290,32 @@ impl KeyringProvider { } } + #[instrument(skip(self))] + async fn fetch_native( + &self, + service: &str, + username: Option<&str>, + ) -> Option<(String, String)> { + let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}"); + let username = username?; + let Ok(entry) = uv_keyring::Entry::new(&prefixed_service, username) else { + return None; + }; + match entry.get_password().await { + Ok(password) => return Some((username.to_string(), password)), + Err(uv_keyring::Error::NoEntry) => { + debug!("No entry found in system keyring for {service}"); + } + Err(err) => { + warn_user_once!( + "Unable to fetch credentials for {service} from system keyring: {}", + err + ); + } + } + None + } + #[cfg(test)] fn fetch_dummy( store: &Vec<(String, &'static str, &'static str)>, diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 28bbfb07a..40812f3ea 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -375,6 +375,7 @@ impl AuthMiddleware { .as_ref() .is_ok_and(|response| response.error_for_status_ref().is_ok()) { + // TODO(zanieb): Consider also updating the system keyring after successful use trace!("Updating cached credentials for {url} to {credentials:?}"); self.cache().insert(&url, credentials); } diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index b14b2a62a..7a5e9ac5b 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -11,7 +11,7 @@ use clap::{Args, Parser, Subcommand}; use uv_cache::CacheArgs; use uv_configuration::{ ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, ProjectBuildBackend, - TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, + Service, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, }; use uv_distribution_types::{ ConfigSettingEntry, ConfigSettingPackageEntry, Index, IndexUrl, Origin, PipExtraIndex, @@ -399,6 +399,13 @@ impl From for anstream::ColorChoice { #[derive(Subcommand)] #[allow(clippy::large_enum_variant)] pub enum Commands { + /// Manage authentication. + #[command( + after_help = "Use `uv help auth` for more details.", + after_long_help = "" + )] + Auth(AuthNamespace), + /// Manage Python projects. #[command(flatten)] Project(Box), @@ -4386,6 +4393,22 @@ pub struct FormatArgs { pub extra_args: Vec, } +#[derive(Args)] +pub struct AuthNamespace { + #[command(subcommand)] + pub command: AuthCommand, +} + +#[derive(Subcommand)] +pub enum AuthCommand { + /// Login to a service + Login(AuthLoginArgs), + /// Logout of a service + Logout(AuthLogoutArgs), + /// Show the authentication token for a service + Token(AuthTokenArgs), +} + #[derive(Args)] pub struct ToolNamespace { #[command(subcommand)] @@ -5501,6 +5524,76 @@ pub struct PythonPinArgs { pub rm: bool, } +#[derive(Args)] +pub struct AuthLogoutArgs { + /// The service to logout of. + pub service: Service, + + /// The username to logout. + #[arg(long, short)] + pub username: Option, + + /// The keyring provider to use for storage of credentials. + /// + /// Only `--keyring-provider native` is supported for `logout`, which uses the system keyring + /// via an integration built into uv. + #[arg( + long, + value_enum, + env = EnvVars::UV_KEYRING_PROVIDER, + )] + pub keyring_provider: Option, +} + +#[derive(Args)] +pub struct AuthLoginArgs { + /// The service to login to. + pub service: Service, + + /// The username to use for the service. + #[arg(long, short, conflicts_with = "token")] + pub username: Option, + + /// The password to use for the service. + #[arg(long, conflicts_with = "token")] + pub password: Option, + + /// The token to use for the service. + /// + /// The username will be set to `__token__`. + #[arg(long, short, conflicts_with = "username", conflicts_with = "password")] + pub token: Option, + + /// The keyring provider to use for storage of credentials. + /// + /// Only `--keyring-provider native` is supported for `login`, which uses the system keyring via + /// an integration built into uv. + #[arg( + long, + value_enum, + env = EnvVars::UV_KEYRING_PROVIDER, + )] + pub keyring_provider: Option, +} + +#[derive(Args)] +pub struct AuthTokenArgs { + /// The service to lookup. + pub service: Service, + + /// The username to lookup. + #[arg(long, short)] + pub username: Option, + + /// The keyring provider to use for reading credentials. + #[arg( + long, + value_enum, + env = EnvVars::UV_KEYRING_PROVIDER, + )] + pub keyring_provider: Option, +} + #[derive(Args)] pub struct GenerateShellCompletionArgs { /// The shell to generate the completion script for diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 55712470f..641d88498 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -28,8 +28,7 @@ use tracing::{debug, trace}; use url::ParseError; use url::Url; -use uv_auth::Credentials; -use uv_auth::{AuthMiddleware, Indexes}; +use uv_auth::{AuthMiddleware, Credentials, Indexes}; use uv_configuration::{KeyringProviderType, TrustedHost}; use uv_fs::Simplified; use uv_pep508::MarkerEnvironment; diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index 655a115a9..3776ab8c1 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -25,6 +25,7 @@ uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true, features = ["schemars"] } uv-platform-tags = { workspace = true } +uv-redacted = { workspace = true } uv-static = { workspace = true } clap = { workspace = true, features = ["derive"], optional = true } either = { workspace = true } diff --git a/crates/uv-configuration/src/authentication.rs b/crates/uv-configuration/src/authentication.rs index a6773c81f..dee874dae 100644 --- a/crates/uv-configuration/src/authentication.rs +++ b/crates/uv-configuration/src/authentication.rs @@ -1,4 +1,6 @@ +use std::str::FromStr; use uv_auth::{self, KeyringProvider}; +use uv_redacted::DisplaySafeUrl; /// Keyring provider type to use for credential lookup. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -9,6 +11,8 @@ pub enum KeyringProviderType { /// Do not use keyring for credential lookup. #[default] Disabled, + /// Use a native integration with the system keychain for credential lookup. + Native, /// Use the `keyring` command for credential lookup. Subprocess, // /// Not yet implemented @@ -22,7 +26,60 @@ impl KeyringProviderType { pub fn to_provider(&self) -> Option { match self { Self::Disabled => None, + Self::Native => Some(KeyringProvider::native()), Self::Subprocess => Some(KeyringProvider::subprocess()), } } } + +impl std::fmt::Display for KeyringProviderType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Disabled => write!(f, "disabled"), + Self::Native => write!(f, "native"), + Self::Subprocess => write!(f, "subprocess"), + } + } +} + +/// A service URL that wraps [`DisplaySafeUrl`] for CLI usage. +/// +/// This type provides automatic URL parsing and validation when used as a CLI argument, +/// eliminating the need for manual parsing in command functions. +#[derive(Debug, Clone)] +pub struct Service(DisplaySafeUrl); + +impl Service { + /// Get the underlying [`DisplaySafeUrl`]. + pub fn url(&self) -> &DisplaySafeUrl { + &self.0 + } + + /// Convert into the underlying [`DisplaySafeUrl`]. + pub fn into_url(self) -> DisplaySafeUrl { + self.0 + } +} + +impl FromStr for Service { + type Err = url::ParseError; + + fn from_str(s: &str) -> Result { + // First try parsing as-is + match DisplaySafeUrl::parse(s) { + Ok(url) => Ok(Self(url)), + Err(url::ParseError::RelativeUrlWithoutBase) => { + // If it's a relative URL, try prepending https:// + let with_https = format!("https://{s}"); + DisplaySafeUrl::parse(&with_https).map(Service) + } + Err(e) => Err(e), + } + } +} + +impl std::fmt::Display for Service { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} diff --git a/crates/uv-console/src/lib.rs b/crates/uv-console/src/lib.rs index f90d45c1f..d1476f5c7 100644 --- a/crates/uv-console/src/lib.rs +++ b/crates/uv-console/src/lib.rs @@ -112,6 +112,19 @@ pub fn password(prompt: &str, term: &Term) -> std::io::Result { Ok(input) } +/// Prompt the user for username in the given [`Term`]. +pub fn username(prompt: &str, term: &Term) -> std::io::Result { + term.write_str(prompt)?; + term.show_cursor()?; + term.flush()?; + + let input = term.read_line()?; + + term.clear_line()?; + + Ok(input) +} + /// Prompt the user for input text in the given [`Term`]. /// /// This is a slimmed-down version of `dialoguer::Input`. diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 59c680264..ad30a4c71 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -18,6 +18,7 @@ bitflags::bitflags! { const EXTRA_BUILD_DEPENDENCIES = 1 << 6; const DETECT_MODULE_CONFLICTS = 1 << 7; const FORMAT = 1 << 8; + const NATIVE_KEYRING = 1 << 9; } } @@ -36,6 +37,7 @@ impl PreviewFeatures { Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies", Self::DETECT_MODULE_CONFLICTS => "detect-module-conflicts", Self::FORMAT => "format", + Self::NATIVE_KEYRING => "native-keyring", _ => panic!("`flag_as_str` can only be used for exactly one feature flag"), } } @@ -82,6 +84,7 @@ impl FromStr for PreviewFeatures { "extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES, "detect-module-conflicts" => Self::DETECT_MODULE_CONFLICTS, "format" => Self::FORMAT, + "native-keyring" => Self::NATIVE_KEYRING, _ => { warn_user_once!("Unknown preview feature: `{part}`"); continue; diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 48cd8a93a..e38a07ef7 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -152,6 +152,7 @@ ignored = [ [features] default = ["performance", "uv-distribution/static", "default-tests"] +keyring-tests = [] # Use better memory allocators, etc. performance = ["performance-memory-allocator"] performance-memory-allocator = ["dep:uv-performance-memory-allocator"] diff --git a/crates/uv/src/commands/auth/login.rs b/crates/uv/src/commands/auth/login.rs new file mode 100644 index 000000000..fb67e3fbc --- /dev/null +++ b/crates/uv/src/commands/auth/login.rs @@ -0,0 +1,80 @@ +use anyhow::{Result, bail}; +use std::fmt::Write; + +use console::Term; +use uv_auth::Credentials; +use uv_configuration::{KeyringProviderType, Service}; + +use crate::{commands::ExitStatus, printer::Printer}; + +/// Login to a service. +pub(crate) async fn login( + service: Service, + username: Option, + password: Option, + token: Option, + keyring_provider: Option, + printer: Printer, +) -> Result { + let url = service.url(); + let display_url = username + .as_ref() + .map(|username| format!("{username}@{url}")) + .unwrap_or_else(|| url.to_string()); + + let username = if let Some(username) = username { + username + } else if token.is_some() { + String::from("__token__") + } else { + let term = Term::stderr(); + if term.is_term() { + let prompt = "username: "; + uv_console::username(prompt, &term)? + } else { + bail!("No username provided; did you mean to provide `--username` or `--token`?"); + } + }; + + // Be helpful about incompatible `keyring-provider` settings + let Some(keyring_provider) = &keyring_provider else { + bail!( + "Logging in requires setting `keyring-provider = {}` for credentials to be retrieved in subsequent commands", + KeyringProviderType::Native + ); + }; + let provider = match keyring_provider { + KeyringProviderType::Native => keyring_provider.to_provider().unwrap(), + KeyringProviderType::Disabled | KeyringProviderType::Subprocess => { + bail!( + "Cannot login with `keyring-provider = {keyring_provider}`, use `keyring-provider = {}` instead", + KeyringProviderType::Native + ); + } + }; + + // FIXME: It would be preferable to accept the value of --password or --token + // from stdin, perhaps checking here for `-` as an indicator to read stdin. We + // could then warn if the password is provided as a plaintext argument. + let password = if let Some(password) = password { + password + } else if let Some(token) = token { + token + } else { + let term = Term::stderr(); + if term.is_term() { + let prompt = "password: "; + uv_console::password(prompt, &term)? + } else { + bail!("No password provided; did you mean to provide `--password` or `--token`?"); + } + }; + + // TODO(zanieb): Add support for other authentication schemes here, e.g., `Credentials::Bearer` + let credentials = Credentials::basic(Some(username), Some(password)); + provider.store(url, &credentials).await?; + + writeln!(printer.stderr(), "Logged in to {display_url}")?; + + Ok(ExitStatus::Success) +} diff --git a/crates/uv/src/commands/auth/logout.rs b/crates/uv/src/commands/auth/logout.rs new file mode 100644 index 000000000..270dfdade --- /dev/null +++ b/crates/uv/src/commands/auth/logout.rs @@ -0,0 +1,48 @@ +use anyhow::{Context, Result, bail}; +use std::{borrow::Cow, fmt::Write}; +use uv_configuration::{KeyringProviderType, Service}; + +use crate::{commands::ExitStatus, printer::Printer}; + +/// Logout from a service. +/// +/// If no username is provided, defaults to `__token__`. +pub(crate) async fn logout( + service: Service, + username: Option, + keyring_provider: Option, + printer: Printer, +) -> Result { + let url = service.url(); + let display_url = username + .as_ref() + .map(|username| format!("{username}@{url}")) + .unwrap_or_else(|| url.to_string()); + let username = username + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed("__token__")); + + // Unlike login, we'll default to the native provider if none is requested since it's the only + // valid option and it doesn't matter if the credentials are available in subsequent commands. + let keyring_provider = keyring_provider.unwrap_or(KeyringProviderType::Native); + + // Be helpful about incompatible `keyring-provider` settings + let provider = match keyring_provider { + KeyringProviderType::Native => keyring_provider.to_provider().unwrap(), + KeyringProviderType::Disabled | KeyringProviderType::Subprocess => { + bail!( + "Cannot logout with `keyring-provider = {keyring_provider}`, use `keyring-provider = {}` instead", + KeyringProviderType::Native + ); + } + }; + + provider + .remove(url, &username) + .await + .with_context(|| format!("Unable to remove credentials for {display_url}"))?; + + writeln!(printer.stderr(), "Logged out of {display_url}")?; + + Ok(ExitStatus::Success) +} diff --git a/crates/uv/src/commands/auth/mod.rs b/crates/uv/src/commands/auth/mod.rs new file mode 100644 index 000000000..8904696da --- /dev/null +++ b/crates/uv/src/commands/auth/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod login; +pub(crate) mod logout; +pub(crate) mod token; diff --git a/crates/uv/src/commands/auth/token.rs b/crates/uv/src/commands/auth/token.rs new file mode 100644 index 000000000..9cd2db86e --- /dev/null +++ b/crates/uv/src/commands/auth/token.rs @@ -0,0 +1,48 @@ +use std::fmt::Write; + +use anyhow::{Context, Result, bail}; + +use uv_configuration::{KeyringProviderType, Service}; + +use crate::{Printer, commands::ExitStatus}; + +/// Show the token that will be used for a service. +pub(crate) async fn token( + service: Service, + username: Option, + keyring_provider: Option, + printer: Printer, +) -> Result { + // Determine the keyring provider to use + let Some(keyring_provider) = &keyring_provider else { + bail!("Retrieving credentials requires setting a `keyring-provider`"); + }; + let Some(provider) = keyring_provider.to_provider() else { + bail!("Cannot retrieve credentials with `keyring-provider = {keyring_provider}`"); + }; + + let url = service.url(); + let display_url = username + .as_ref() + .map(|username| format!("{username}@{url}")) + .unwrap_or_else(|| url.to_string()); + + let credentials = provider + .fetch(url, Some(username.as_deref().unwrap_or("__token__"))) + .await + .with_context(|| format!("Failed to fetch credentials for {display_url}"))?; + + let Some(password) = credentials.password() else { + bail!( + "No {} found for {display_url}", + if username.is_some() { + "password" + } else { + "token" + } + ); + }; + + writeln!(printer.stdout(), "{password}")?; + Ok(ExitStatus::Success) +} diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 42038976d..c0a9a8ce7 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -9,6 +9,9 @@ use anyhow::Context; use owo_colors::OwoColorize; use tracing::debug; +pub(crate) use auth::login::login as auth_login; +pub(crate) use auth::logout::logout as auth_logout; +pub(crate) use auth::token::token as auth_token; pub(crate) use build_frontend::build_frontend; pub(crate) use cache_clean::cache_clean; pub(crate) use cache_dir::cache_dir; @@ -65,6 +68,7 @@ pub(crate) use venv::venv; use crate::printer::Printer; +mod auth; pub(crate) mod build_backend; mod build_frontend; mod cache_clean; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 812ad9ada..ab866d57b 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -24,9 +24,9 @@ use uv_cache_info::Timestamp; #[cfg(feature = "self-update")] use uv_cli::SelfUpdateArgs; use uv_cli::{ - BuildBackendCommand, CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace, - ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, SelfNamespace, ToolCommand, - ToolNamespace, TopLevelArgs, compat::CompatArgs, + AuthCommand, AuthNamespace, BuildBackendCommand, CacheCommand, CacheNamespace, Cli, Commands, + PipCommand, PipNamespace, ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, + SelfNamespace, ToolCommand, ToolNamespace, TopLevelArgs, compat::CompatArgs, }; use uv_client::BaseClientBuilder; use uv_configuration::min_stack_size; @@ -439,6 +439,41 @@ async fn run(mut cli: Cli) -> Result { .retries_from_env()?; match *cli.command { + Commands::Auth(AuthNamespace { + command: AuthCommand::Login(args), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::AuthLoginSettings::resolve(args, filesystem); + show_settings!(args); + + commands::auth_login( + args.service, + args.username, + args.password, + args.token, + args.keyring_provider, + printer, + ) + .await + } + Commands::Auth(AuthNamespace { + command: AuthCommand::Logout(args), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::AuthLogoutSettings::resolve(args, filesystem); + show_settings!(args); + + commands::auth_logout(args.service, args.username, args.keyring_provider, printer).await + } + Commands::Auth(AuthNamespace { + command: AuthCommand::Token(args), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::AuthTokenSettings::resolve(args, filesystem); + show_settings!(args); + + commands::auth_token(args.service, args.username, args.keyring_provider, printer).await + } Commands::Help(args) => commands::help( args.command.unwrap_or_default().as_slice(), printer, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 7a44aa6e0..cf939f4e3 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -7,12 +7,13 @@ use std::str::FromStr; use uv_cache::{CacheArgs, Refresh}; use uv_cli::comma::CommaSeparatedRequirements; use uv_cli::{ - AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, - PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, - PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs, - PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs, - SyncArgs, SyncFormat, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, - ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs, VersionBump, VersionFormat, + AddArgs, AuthLoginArgs, AuthLogoutArgs, AuthTokenArgs, ColorChoice, ExternalCommand, + GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, + PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs, PipUninstallArgs, + PythonFindArgs, PythonInstallArgs, PythonListArgs, PythonListFormat, PythonPinArgs, + PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs, SyncArgs, SyncFormat, ToolDirArgs, + ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs, + VersionBump, VersionFormat, }; use uv_cli::{ AuthorFrom, BuildArgs, ExportArgs, FormatArgs, PublishArgs, PythonDirArgs, @@ -24,7 +25,8 @@ use uv_configuration::{ BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, NoBinary, NoBuild, ProjectBuildBackend, Reinstall, RequiredVersion, - SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, + Service, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, + VersionControlSystem, }; use uv_distribution_types::{ ConfigSettings, DependencyMetadata, ExtraBuildVariables, Index, IndexLocations, IndexUrl, @@ -3483,6 +3485,101 @@ impl PublishSettings { } } +/// The resolved settings to use for an invocation of the `uv auth logout` CLI. +#[derive(Debug, Clone)] +pub(crate) struct AuthLogoutSettings { + pub(crate) service: Service, + pub(crate) username: Option, + + // Both CLI and configuration. + pub(crate) keyring_provider: Option, +} + +impl AuthLogoutSettings { + /// Resolve the [`AuthLogoutSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: AuthLogoutArgs, filesystem: Option) -> Self { + let Options { top_level, .. } = filesystem + .map(FilesystemOptions::into_options) + .unwrap_or_default(); + + let ResolverInstallerSchema { + keyring_provider, .. + } = top_level; + + Self { + service: args.service, + username: args.username, + keyring_provider, + } + } +} + +/// The resolved settings to use for an invocation of the `uv auth token` CLI. +#[derive(Debug, Clone)] +pub(crate) struct AuthTokenSettings { + pub(crate) service: Service, + pub(crate) username: Option, + + // Both CLI and configuration. + pub(crate) keyring_provider: Option, +} + +impl AuthTokenSettings { + /// Resolve the [`AuthTokenSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: AuthTokenArgs, filesystem: Option) -> Self { + let Options { top_level, .. } = filesystem + .map(FilesystemOptions::into_options) + .unwrap_or_default(); + + let ResolverInstallerSchema { + keyring_provider, .. + } = top_level; + + let keyring_provider = args.keyring_provider.combine(keyring_provider); + + Self { + service: args.service, + username: args.username, + keyring_provider, + } + } +} + +/// The resolved settings to use for an invocation of the `uv auth set` CLI. +#[derive(Debug, Clone)] +pub(crate) struct AuthLoginSettings { + pub(crate) service: Service, + pub(crate) username: Option, + pub(crate) password: Option, + pub(crate) token: Option, + + // Both CLI and configuration. + pub(crate) keyring_provider: Option, +} + +impl AuthLoginSettings { + /// Resolve the [`AuthLoginSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: AuthLoginArgs, filesystem: Option) -> Self { + let Options { top_level, .. } = filesystem + .map(FilesystemOptions::into_options) + .unwrap_or_default(); + + let ResolverInstallerSchema { + keyring_provider, .. + } = top_level; + + let keyring_provider = args.keyring_provider.combine(keyring_provider); + + Self { + service: args.service, + username: args.username, + password: args.password, + token: args.token, + keyring_provider, + } + } +} + // Environment variables that are not exposed as CLI arguments. mod env { use uv_static::EnvVars; diff --git a/crates/uv/tests/it/auth.rs b/crates/uv/tests/it/auth.rs new file mode 100644 index 000000000..0532c2dad --- /dev/null +++ b/crates/uv/tests/it/auth.rs @@ -0,0 +1,733 @@ +use anyhow::Result; +use assert_cmd::assert::OutputAssertExt; +use assert_fs::{fixture::PathChild, prelude::FileWriteStr}; +use uv_static::EnvVars; + +use crate::common::{TestContext, uv_snapshot, venv_bin_path}; + +#[test] +fn add_package_native_keyring() -> Result<()> { + let context = TestContext::new("3.12").with_real_home(); + + // Clear state before the test + context + .auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .status()?; + + // Configure `pyproject.toml` with native keyring provider. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc::indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.11, <4" + dependencies = [] + + [tool.uv] + keyring-provider = "native" + "# + })?; + + // Try to add a package without credentials. + uv_snapshot!(context.add().arg("anyio").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because anyio was not found in the package registry and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable. + + hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized). + help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing. + " + ); + + // Login to the index + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .arg("--password") + .arg("heron"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to public@https://pypi-proxy.fly.dev/basic-auth/simple + " + ); + + // Try to add the original package without credentials again. This should use + // credentials storied in the system keyring. + uv_snapshot!(context.add().arg("anyio").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + " + ); + + // Logout of the index + uv_snapshot!(context.auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged out of public@https://pypi-proxy.fly.dev/basic-auth/simple + " + ); + + // Authentication should fail again + uv_snapshot!(context.add().arg("iniconfig").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable. + + hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized). + help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing. + " + ); + + Ok(()) +} + +#[test] +fn token_native_keyring() -> Result<()> { + let context = TestContext::new_with_versions(&[]).with_real_home(); + + // Clear state before the test + context + .auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .status()?; + + // Without a service name + uv_snapshot!(context.auth_token(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the following required arguments were not provided: + + + Usage: uv auth token --cache-dir [CACHE_DIR] + + For more information, try '--help'. + "); + + // Without a keyring provider... + uv_snapshot!(context.auth_token() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Retrieving credentials requires setting a `keyring-provider` + "); + + // Without persisted credentials + uv_snapshot!(context.auth_token() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to fetch credentials for https://pypi-proxy.fly.dev/basic-auth/simple + "); + + // Without persisted credentials (with a username in the request) + uv_snapshot!(context.auth_token() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to fetch credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple + "); + + // Login to the index + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .arg("--password") + .arg("heron") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to public@https://pypi-proxy.fly.dev/basic-auth/simple + " + ); + + // Show the credentials + uv_snapshot!(context.auth_token() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + heron + + ----- stderr ----- + "); + + // Without the username + // TODO(zanieb): Add a hint here if we can? + uv_snapshot!(context.auth_token() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to fetch credentials for https://pypi-proxy.fly.dev/basic-auth/simple + "); + + // With a mismatched username + // TODO(zanieb): Add a hint here if we can? + uv_snapshot!(context.auth_token() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("private") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to fetch credentials for private@https://pypi-proxy.fly.dev/basic-auth/simple + "); + + // Login to the index with a token + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--token") + .arg("heron") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to https://pypi-proxy.fly.dev/basic-auth/simple + " + ); + + // Retrieve the token without a username + uv_snapshot!(context.auth_token() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + heron + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn token_subprocess_keyring() { + let context = TestContext::new("3.12"); + + // Without a keyring on the PATH + uv_snapshot!(context.auth_token() + .arg("https://public@pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("subprocess"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to fetch credentials for https://****@pypi-proxy.fly.dev/basic-auth/simple + " + ); + + // Install our keyring plugin + context + .pip_install() + .arg( + context + .workspace_root + .join("scripts") + .join("packages") + .join("keyring_test_plugin"), + ) + .assert() + .success(); + + // Without credentials available + uv_snapshot!(context.auth_token() + .arg("https://public@pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("subprocess") + .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Keyring request for __token__@https://public@pypi-proxy.fly.dev/basic-auth/simple + Keyring request for __token__@pypi-proxy.fly.dev + error: Failed to fetch credentials for https://****@pypi-proxy.fly.dev/basic-auth/simple + " + ); + + // Without a username + // TODO(zanieb): Add a hint here if we can? + uv_snapshot!(context.auth_token() + .arg("https://public@pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("subprocess") + .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#) + .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Keyring request for __token__@https://public@pypi-proxy.fly.dev/basic-auth/simple + Keyring request for __token__@pypi-proxy.fly.dev + error: Failed to fetch credentials for https://****@pypi-proxy.fly.dev/basic-auth/simple + " + ); + + // With the correct username + uv_snapshot!(context.auth_token() + .arg("https://public@pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("subprocess") + .arg("--username") + .arg("public") + .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#) + .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r" + success: true + exit_code: 0 + ----- stdout ----- + heron + + ----- stderr ----- + Keyring request for public@https://public@pypi-proxy.fly.dev/basic-auth/simple + Keyring request for public@pypi-proxy.fly.dev + " + ); +} + +#[test] +fn login_native_keyring() -> Result<()> { + let context = TestContext::new_with_versions(&[]).with_real_home(); + + // Clear state before the test + context + .auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .status()?; + + // Without a service name + uv_snapshot!(context.auth_login(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the following required arguments were not provided: + + + Usage: uv auth login --cache-dir [CACHE_DIR] + + For more information, try '--help'. + "); + + // Without a username (or token) + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No username provided; did you mean to provide `--username` or `--token`? + "); + + // Without a password + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No password provided; did you mean to provide `--password` or `--token`? + "); + + // Without a keyring provider + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--token") + .arg("foo"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Logging in requires setting `keyring-provider = native` for credentials to be retrieved in subsequent commands + "); + + // Successful + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .arg("--password") + .arg("heron") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to public@https://pypi-proxy.fly.dev/basic-auth/simple + " + ); + + Ok(()) +} + +#[test] +fn login_token_native_keyring() -> Result<()> { + let context = TestContext::new_with_versions(&[]).with_real_home(); + + // Clear state before the test + context + .auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("__token__") + .status()?; + + // Successful with token + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--token") + .arg("test-token") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to https://pypi-proxy.fly.dev/basic-auth/simple + " + ); + + Ok(()) +} + +#[test] +fn logout_native_keyring() -> Result<()> { + let context = TestContext::new_with_versions(&[]).with_real_home(); + + // Clear state before the test + context + .auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .status()?; + + // Without a service name + uv_snapshot!(context.auth_logout(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the following required arguments were not provided: + + + Usage: uv auth logout --cache-dir [CACHE_DIR] + + For more information, try '--help'. + "); + + // Logout without a keyring provider + uv_snapshot!(context.auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged out of https://pypi-proxy.fly.dev/basic-auth/simple + "); + + // Logout before logging in (without a username) + uv_snapshot!(context.auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Unable to remove credentials for https://pypi-proxy.fly.dev/basic-auth/simple + Caused by: No matching entry found in secure storage + "); + + // Logout before logging in (with a username) + uv_snapshot!(context.auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Unable to remove credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple + Caused by: No matching entry found in secure storage + "); + + // Login with a username + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .arg("--password") + .arg("heron") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to public@https://pypi-proxy.fly.dev/basic-auth/simple + " + ); + + // Logout without a username + // TODO(zanieb): Add a hint here if we can? + uv_snapshot!(context.auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Unable to remove credentials for https://pypi-proxy.fly.dev/basic-auth/simple + Caused by: No matching entry found in secure storage + "); + + // Logout with a username + uv_snapshot!(context.auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--username") + .arg("public") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged out of public@https://pypi-proxy.fly.dev/basic-auth/simple + "); + + Ok(()) +} + +#[test] +fn logout_token_native_keyring() -> Result<()> { + let context = TestContext::new_with_versions(&[]).with_real_home(); + + // Clear state before the test + context + .auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .status()?; + + // Login with a token + uv_snapshot!(context.auth_login() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--token") + .arg("test-token") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to https://pypi-proxy.fly.dev/basic-auth/simple + " + ); + + // Logout without a username + uv_snapshot!(context.auth_logout() + .arg("https://pypi-proxy.fly.dev/basic-auth/simple") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged out of https://pypi-proxy.fly.dev/basic-auth/simple + "); + + Ok(()) +} + +#[test] +fn login_url_parsing() { + let context = TestContext::new_with_versions(&[]).with_real_home(); + + // A domain-only service name gets https:// prepended + uv_snapshot!(context.auth_login() + .arg("example.com") + .arg("--username") + .arg("test") + .arg("--password") + .arg("test") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to test@https://example.com/ + "); + + // When including a protocol explicitly, it is retained + uv_snapshot!(context.auth_login() + .arg("http://example.com") + .arg("--username") + .arg("test") + .arg("--password") + .arg("test") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to test@http://example.com/ + "); + + uv_snapshot!(context.auth_login() + .arg("https://example.com") + .arg("--username") + .arg("test") + .arg("--password") + .arg("test") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to test@https://example.com/ + "); + + // A domain-only service with a path also gets https:// prepended + uv_snapshot!(context.auth_login() + .arg("example.com/simple") + .arg("--username") + .arg("test") + .arg("--password") + .arg("test") + .arg("--keyring-provider") + .arg("native"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Logged in to test@https://example.com/simple + "); + + // An invalid URL is rejected + uv_snapshot!(context.auth_login() + .arg("not a valid url") + .arg("--username") + .arg("test") + .arg("--password") + .arg("test") + .arg("--keyring-provider") + .arg("native"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'not a valid url' for '': invalid international domain name + + For more information, try '--help'. + "); +} diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 0739d8591..5b518cd8f 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -1274,6 +1274,42 @@ impl TestContext { command } + /// Create a `uv auth login` command. + pub fn auth_login(&self) -> Command { + let mut command = Self::new_command(); + command.arg("auth").arg("login"); + self.add_shared_options(&mut command, false); + command + } + + /// Create a `uv auth logout` command. + pub fn auth_logout(&self) -> Command { + let mut command = Self::new_command(); + command.arg("auth").arg("logout"); + self.add_shared_options(&mut command, false); + command + } + + /// Create a `uv auth token` command. + pub fn auth_token(&self) -> Command { + let mut command = Self::new_command(); + command.arg("auth").arg("token"); + self.add_shared_options(&mut command, false); + command + } + + /// Set `HOME` to the real home directory. + /// + /// We need this for testing commands which use the macOS keychain. + #[must_use] + pub fn with_real_home(mut self) -> Self { + if let Some(home) = env::var_os(EnvVars::HOME) { + self.extra_env + .push((EnvVars::HOME.to_string().into(), home)); + } + self + } + /// Run the given python code and check whether it succeeds. pub fn assert_command(&self, command: &str) -> Assert { self.python_command() diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index 5231f70d5..6a2e9bc48 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -16,6 +16,7 @@ fn help() { Usage: uv [OPTIONS] Commands: + auth Manage authentication run Run a command or script init Create a new project add Add dependencies to the project @@ -97,6 +98,7 @@ fn help_flag() { Usage: uv [OPTIONS] Commands: + auth Manage authentication run Run a command or script init Create a new project add Add dependencies to the project @@ -176,6 +178,7 @@ fn help_short_flag() { Usage: uv [OPTIONS] Commands: + auth Manage authentication run Run a command or script init Create a new project add Add dependencies to the project @@ -874,6 +877,7 @@ fn help_unknown_subcommand() { ----- stderr ----- error: There is no command `foobar` for `uv`. Did you mean one of: + auth run init add @@ -902,6 +906,7 @@ fn help_unknown_subcommand() { ----- stderr ----- error: There is no command `foo bar` for `uv`. Did you mean one of: + auth run init add @@ -959,6 +964,7 @@ fn help_with_global_option() { Usage: uv [OPTIONS] Commands: + auth Manage authentication run Run a command or script init Create a new project add Add dependencies to the project @@ -1081,6 +1087,7 @@ fn help_with_no_pager() { Usage: uv [OPTIONS] Commands: + auth Manage authentication run Run a command or script init Create a new project add Add dependencies to the project diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 70e2de964..0e47d254a 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -3,6 +3,9 @@ pub(crate) mod common; +#[cfg(feature = "keyring-tests")] +mod auth; + mod branching_urls; #[cfg(all(feature = "python", feature = "pypi"))] diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 487534b42..cc2a1ea50 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -7684,7 +7684,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_KEYRING, ), }, python_preference: Managed, @@ -7908,7 +7908,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_KEYRING, ), }, python_preference: Managed, diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 158f2cc1e..4e3c05d4a 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -12,7 +12,8 @@ uv [OPTIONS]

Commands

-
uv run

Run a command or script

+
uv auth

Manage authentication

+
uv run

Run a command or script

uv init

Create a new project

uv add

Add dependencies to the project

uv remove

Remove dependencies from the project

@@ -33,6 +34,238 @@ uv [OPTIONS]
uv help

Display documentation for a command

+## uv auth + +Manage authentication + +

Usage

+ +``` +uv auth [OPTIONS] +``` + +

Commands

+ +
uv auth login

Login to a service

+
uv auth logout

Logout of a service

+
uv auth token

Show the authentication token for a service

+
+ +### uv auth login + +Login to a service + +

Usage

+ +``` +uv auth login [OPTIONS] +``` + +

Arguments

+ +
SERVICE

The service to login to

+
+ +

Options

+ +
--allow-insecure-host, --trusted-host allow-insecure-host

Allow insecure connections to a host.

+

Can be provided multiple times.

+

Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).

+

WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

+

May also be set with the UV_INSECURE_HOST environment variable.

--cache-dir cache-dir

Path to the cache directory.

+

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+

To view the location of the cache directory, run uv cache dir.

+

May also be set with the UV_CACHE_DIR environment variable.

--color color-choice

Control the use of color in output.

+

By default, uv will automatically detect support for colors when writing to a terminal.

+

Possible values:

+
    +
  • auto: Enables colored output only when the output is going to a terminal or TTY with support
  • +
  • always: Enables colored output regardless of the detected environment
  • +
  • never: Disables colored output
  • +
--config-file config-file

The path to a uv.toml file to use for configuration.

+

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

+

May also be set with the UV_CONFIG_FILE environment variable.

--directory directory

Change to the given directory prior to running the command.

+

Relative paths are resolved with the given directory as the base.

+

See --project to only change the project root directory.

+
--help, -h

Display the concise help for this command

+
--keyring-provider keyring-provider

The keyring provider to use for storage of credentials.

+

Only --keyring-provider native is supported for login, which uses the system keyring via an integration built into uv.

+

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

+
    +
  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • +
  • subprocess: Use the keyring command for credential lookup
  • +
--managed-python

Require use of uv-managed Python versions.

+

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

+

May also be set with the UV_MANAGED_PYTHON environment variable.

--native-tls

Whether to load TLS certificates from the platform's native certificate store.

+

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

+

However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.

+

May also be set with the UV_NATIVE_TLS environment variable.

--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

+

May also be set with the UV_NO_CACHE environment variable.

--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

+

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

+

May also be set with the UV_NO_CONFIG environment variable.

--no-managed-python

Disable use of uv-managed Python versions.

+

Instead, uv will search for a suitable Python version on the system.

+

May also be set with the UV_NO_MANAGED_PYTHON environment variable.

--no-progress

Hide all progress outputs.

+

For example, spinners or progress bars.

+

May also be set with the UV_NO_PROGRESS environment variable.

--no-python-downloads

Disable automatic downloads of Python.

+
--offline

Disable network access.

+

When disabled, uv will only use locally cached data and locally available files.

+

May also be set with the UV_OFFLINE environment variable.

--password password

The password to use for the service

+
--project project

Run the command within the given project directory.

+

All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (.venv).

+

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

+

See --directory to change the working directory entirely.

+

This setting has no effect when used in the uv pip interface.

+

May also be set with the UV_PROJECT environment variable.

--quiet, -q

Use quiet output.

+

Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

+
--token, -t token

The token to use for the service.

+

The username will be set to __token__.

+
--username, -u username

The username to use for the service

+
--verbose, -v

Use verbose output.

+

You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)

+
+ +### uv auth logout + +Logout of a service + +

Usage

+ +``` +uv auth logout [OPTIONS] +``` + +

Arguments

+ +
SERVICE

The service to logout of

+
+ +

Options

+ +
--allow-insecure-host, --trusted-host allow-insecure-host

Allow insecure connections to a host.

+

Can be provided multiple times.

+

Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).

+

WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

+

May also be set with the UV_INSECURE_HOST environment variable.

--cache-dir cache-dir

Path to the cache directory.

+

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+

To view the location of the cache directory, run uv cache dir.

+

May also be set with the UV_CACHE_DIR environment variable.

--color color-choice

Control the use of color in output.

+

By default, uv will automatically detect support for colors when writing to a terminal.

+

Possible values:

+
    +
  • auto: Enables colored output only when the output is going to a terminal or TTY with support
  • +
  • always: Enables colored output regardless of the detected environment
  • +
  • never: Disables colored output
  • +
--config-file config-file

The path to a uv.toml file to use for configuration.

+

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

+

May also be set with the UV_CONFIG_FILE environment variable.

--directory directory

Change to the given directory prior to running the command.

+

Relative paths are resolved with the given directory as the base.

+

See --project to only change the project root directory.

+
--help, -h

Display the concise help for this command

+
--keyring-provider keyring-provider

The keyring provider to use for storage of credentials.

+

Only --keyring-provider native is supported for logout, which uses the system keyring via an integration built into uv.

+

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

+
    +
  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • +
  • subprocess: Use the keyring command for credential lookup
  • +
--managed-python

Require use of uv-managed Python versions.

+

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

+

May also be set with the UV_MANAGED_PYTHON environment variable.

--native-tls

Whether to load TLS certificates from the platform's native certificate store.

+

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

+

However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.

+

May also be set with the UV_NATIVE_TLS environment variable.

--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

+

May also be set with the UV_NO_CACHE environment variable.

--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

+

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

+

May also be set with the UV_NO_CONFIG environment variable.

--no-managed-python

Disable use of uv-managed Python versions.

+

Instead, uv will search for a suitable Python version on the system.

+

May also be set with the UV_NO_MANAGED_PYTHON environment variable.

--no-progress

Hide all progress outputs.

+

For example, spinners or progress bars.

+

May also be set with the UV_NO_PROGRESS environment variable.

--no-python-downloads

Disable automatic downloads of Python.

+
--offline

Disable network access.

+

When disabled, uv will only use locally cached data and locally available files.

+

May also be set with the UV_OFFLINE environment variable.

--project project

Run the command within the given project directory.

+

All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (.venv).

+

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

+

See --directory to change the working directory entirely.

+

This setting has no effect when used in the uv pip interface.

+

May also be set with the UV_PROJECT environment variable.

--quiet, -q

Use quiet output.

+

Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

+
--username, -u username

The username to logout

+
--verbose, -v

Use verbose output.

+

You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)

+
+ +### uv auth token + +Show the authentication token for a service + +

Usage

+ +``` +uv auth token [OPTIONS] +``` + +

Arguments

+ +
SERVICE

The service to lookup

+
+ +

Options

+ +
--allow-insecure-host, --trusted-host allow-insecure-host

Allow insecure connections to a host.

+

Can be provided multiple times.

+

Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).

+

WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

+

May also be set with the UV_INSECURE_HOST environment variable.

--cache-dir cache-dir

Path to the cache directory.

+

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+

To view the location of the cache directory, run uv cache dir.

+

May also be set with the UV_CACHE_DIR environment variable.

--color color-choice

Control the use of color in output.

+

By default, uv will automatically detect support for colors when writing to a terminal.

+

Possible values:

+
    +
  • auto: Enables colored output only when the output is going to a terminal or TTY with support
  • +
  • always: Enables colored output regardless of the detected environment
  • +
  • never: Disables colored output
  • +
--config-file config-file

The path to a uv.toml file to use for configuration.

+

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

+

May also be set with the UV_CONFIG_FILE environment variable.

--directory directory

Change to the given directory prior to running the command.

+

Relative paths are resolved with the given directory as the base.

+

See --project to only change the project root directory.

+
--help, -h

Display the concise help for this command

+
--keyring-provider keyring-provider

The keyring provider to use for reading credentials

+

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

+
    +
  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • +
  • subprocess: Use the keyring command for credential lookup
  • +
--managed-python

Require use of uv-managed Python versions.

+

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

+

May also be set with the UV_MANAGED_PYTHON environment variable.

--native-tls

Whether to load TLS certificates from the platform's native certificate store.

+

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

+

However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.

+

May also be set with the UV_NATIVE_TLS environment variable.

--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

+

May also be set with the UV_NO_CACHE environment variable.

--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

+

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

+

May also be set with the UV_NO_CONFIG environment variable.

--no-managed-python

Disable use of uv-managed Python versions.

+

Instead, uv will search for a suitable Python version on the system.

+

May also be set with the UV_NO_MANAGED_PYTHON environment variable.

--no-progress

Hide all progress outputs.

+

For example, spinners or progress bars.

+

May also be set with the UV_NO_PROGRESS environment variable.

--no-python-downloads

Disable automatic downloads of Python.

+
--offline

Disable network access.

+

When disabled, uv will only use locally cached data and locally available files.

+

May also be set with the UV_OFFLINE environment variable.

--project project

Run the command within the given project directory.

+

All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (.venv).

+

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

+

See --directory to change the working directory entirely.

+

This setting has no effect when used in the uv pip interface.

+

May also be set with the UV_PROJECT environment variable.

--quiet, -q

Use quiet output.

+

Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

+
--username, -u username

The username to lookup

+
--verbose, -v

Use verbose output.

+

You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)

+
+ ## uv run Run a command or script. @@ -149,6 +382,7 @@ uv run [OPTIONS] [COMMAND]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -554,6 +788,7 @@ uv add [OPTIONS] >

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -754,6 +989,7 @@ uv remove [OPTIONS] ...

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -936,6 +1172,7 @@ uv version [OPTIONS] [VALUE]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -1134,6 +1371,7 @@ uv sync [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -1379,6 +1617,7 @@ uv lock [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -1558,6 +1797,7 @@ uv export [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -1752,6 +1992,7 @@ uv tree [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -2095,6 +2336,7 @@ uv tool run [OPTIONS] [COMMAND]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -2316,6 +2558,7 @@ uv tool install [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -2529,6 +2772,7 @@ uv tool upgrade [OPTIONS] ...

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -3749,6 +3993,7 @@ uv pip compile [OPTIONS] >

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -4044,6 +4289,7 @@ uv pip sync [OPTIONS] ...

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -4317,6 +4563,7 @@ uv pip install [OPTIONS] |--editable May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -4563,6 +4810,7 @@ uv pip uninstall [OPTIONS] >

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup
--managed-python

Require use of uv-managed Python versions.

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

@@ -4741,6 +4989,7 @@ uv pip list [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup
--managed-python

Require use of uv-managed Python versions.

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

@@ -4916,6 +5165,7 @@ uv pip tree [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup
--managed-python

Require use of uv-managed Python versions.

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

@@ -5157,6 +5407,7 @@ uv venv [OPTIONS] [PATH]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used for installing seed packages.

@@ -5313,6 +5564,7 @@ uv build [OPTIONS] [SRC]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -5465,6 +5717,7 @@ uv publish --publish-url https://upload.pypi.org/legacy/ --check-url https://pyp

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use a native integration with the system keychain for credential lookup
  • subprocess: Use the keyring command for credential lookup
--managed-python

Require use of uv-managed Python versions.

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

diff --git a/uv.schema.json b/uv.schema.json index 655ce6a84..e062fca5b 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -1144,6 +1144,11 @@ "type": "string", "const": "disabled" }, + { + "description": "Use a native integration with the system keychain for credential lookup.", + "type": "string", + "const": "native" + }, { "description": "Use the `keyring` command for credential lookup.", "type": "string",