| //! Registry authentication support. | |
| use crate::{ | |
| context::ConfigKey, | |
| util::{CargoResult, GlobalContext}, | |
| workspace::features::cargo_docs_link, | |
| }; | |
| use anyhow::{Context as _, bail}; | |
| use cargo_credential::{ | |
| Action, CacheControl, Credential, CredentialResponse, LoginOptions, Operation, RegistryInfo, | |
| Secret, | |
| }; | |
| use core::fmt; | |
| use std::error::Error; | |
| use time::{Duration, OffsetDateTime}; | |
| use url::Url; | |
| use crate::context::Value; | |
| use crate::util::credential::adaptor::BasicProcessCredential; | |
| use crate::workspace::SourceId; | |
| use super::{credential::process::CredentialProcessCredential, credential::token::TokenCredential}; | |
| use crate::context::{ | |
| ConfigRelativePathResolveExt, CredentialCacheValue, PathAndArgs, RegistryConfig, | |
| }; | |
| /// Get the list of credential providers for a registry source. | |
| fn credential_provider( | |
| gctx: &GlobalContext, | |
| sid: &SourceId, | |
| require_cred_provider_config: bool, | |
| show_warnings: bool, | |
| ) -> CargoResult<Vec<Vec<String>>> { | |
| let warn = |message: String| { | |
| if show_warnings { | |
| gctx.shell().warn(message) | |
| } else { | |
| Ok(()) | |
| } | |
| }; | |
| let cfg = gctx.registry_config(sid)?; | |
| let token = gctx.registry_credentials(sid)?.and_then(|c| c.token); | |
| let mut global_provider_defined = true; | |
| let default_providers = || { | |
| global_provider_defined = false; | |
| vec![vec!["cargo:token".to_string()]] | |
| }; | |
| let global_providers = gctx | |
| .get::<Option<Vec<Value<String>>>>(["registry", "global-credential-providers"])? | |
| .filter(|p| !p.is_empty()) | |
| .map(|p| { | |
| p.iter() | |
| .rev() | |
| .map(PathAndArgs::from_whitespace_separated_string) | |
| .map(|p| resolve_credential_alias(gctx, p)) | |
| .collect() | |
| }) | |
| .unwrap_or_else(default_providers); | |
| tracing::debug!(?global_providers); | |
| match (cfg, token) { | |
| // If there's a specific provider configured for this registry, use it. | |
| ( | |
| Some(RegistryConfig { | |
| credential_provider: Some(provider), | |
| .. | |
| }), | |
| token, | |
| ) => { | |
| let provider = resolve_credential_alias(gctx, provider); | |
| if let Some(token) = token { | |
| if provider[0] != "cargo:token" { | |
| warn(format!( | |
| "{sid} has a token configured in {} that will be ignored \ | |
| because this registry is configured to use credential-provider `{}`", | |
| token.definition, provider[0], | |
| ))?; | |
| } | |
| } | |
| return Ok(vec![provider]); | |
| } | |
| // Check if a `token` is configured that will be ignored. | |
| (_, Some(token)) => { | |
| if !global_providers | |
| .iter() | |
| .any(|p| p.first().map(String::as_str) == Some("cargo:token")) | |
| { | |
| warn(format!( | |
| "{sid} has a token configured in {} that will be ignored \ | |
| because the `cargo:token` credential provider is not listed in \ | |
| `registry.global-credential-providers`", | |
| token.definition | |
| ))?; | |
| } | |
| } | |
| // If we couldn't find a registry-specific provider, use the fallback provider list. | |
| _ => {} | |
| }; | |
| if !global_provider_defined && require_cred_provider_config { | |
| bail!( | |
| "authenticated registries require a credential-provider to be configured\n\ | |
| see {} for details", | |
| cargo_docs_link("reference/registry-authentication.html") | |
| ); | |
| } | |
| Ok(global_providers) | |
| } | |
| /// Use the `[credential-alias]` table to see if the provider name has been aliased. | |
| fn resolve_credential_alias(gctx: &GlobalContext, mut provider: PathAndArgs) -> Vec<String> { | |
| if provider.args.is_empty() { | |
| let name = provider.path.raw_value(); | |
| let key = format!("credential-alias.{name}"); | |
| if let Ok(alias) = gctx.get::<Value<PathAndArgs>>(["credential-alias", name]) { | |
| tracing::debug!("resolving credential alias '{key}' -> '{alias:?}'"); | |
| if BUILT_IN_PROVIDERS.contains(&name) { | |
| let _ = gctx.shell().warn(format!( | |
| "credential-alias `{name}` (defined in `{}`) will be \ | |
| ignored because it would shadow a built-in credential-provider", | |
| alias.definition | |
| )); | |
| } else { | |
| provider = alias.val; | |
| } | |
| } | |
| } | |
| provider.args.insert( | |
| 0, | |
| provider | |
| .path | |
| .resolve_program(gctx) | |
| .to_str() | |
| .unwrap() | |
| .to_string(), | |
| ); | |
| provider.args | |
| } | |
| #[derive(Debug, PartialEq)] | |
| pub enum AuthorizationErrorReason { | |
| TokenMissing, | |
| TokenRejected, | |
| } | |
| impl fmt::Display for AuthorizationErrorReason { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| match self { | |
| AuthorizationErrorReason::TokenMissing => write!(f, "no token found"), | |
| AuthorizationErrorReason::TokenRejected => write!(f, "token rejected"), | |
| } | |
| } | |
| } | |
| /// An authorization error from accessing a registry. | |
| #[derive(Debug)] | |
| pub struct AuthorizationError { | |
| /// Url that was attempted | |
| sid: SourceId, | |
| /// The `registry.default` config value. | |
| default_registry: Option<String>, | |
| /// Url where the user could log in. | |
| pub login_url: Option<Url>, | |
| /// Specific reason indicating what failed | |
| reason: AuthorizationErrorReason, | |
| /// Should `cargo login` and the `_TOKEN` env var be included when displaying this error? | |
| supports_cargo_token_credential_provider: bool, | |
| /// Whether the cached token appears to lack an authentication scheme (no space found). | |
| token_lacks_scheme: Option<bool>, | |
| } | |
| impl AuthorizationError { | |
| pub fn new( | |
| gctx: &GlobalContext, | |
| sid: SourceId, | |
| login_url: Option<Url>, | |
| reason: AuthorizationErrorReason, | |
| ) -> CargoResult<Self> { | |
| // Only display the _TOKEN environment variable suggestion if the `cargo:token` credential | |
| // provider is available for the source. Otherwise setting the environment variable will | |
| // have no effect. | |
| let supports_cargo_token_credential_provider = | |
| credential_provider(gctx, &sid, false, false)? | |
| .iter() | |
| .any(|p| p.first().map(String::as_str) == Some("cargo:token")); | |
| let cache = gctx.credential_cache(); | |
| let token_lacks_scheme = cache | |
| .get(sid.canonical_url()) | |
| .map(|entry| !entry.token_value.as_deref().expose().contains(' ')); | |
| Ok(AuthorizationError { | |
| sid, | |
| default_registry: gctx.default_registry()?, | |
| login_url, | |
| reason, | |
| supports_cargo_token_credential_provider, | |
| token_lacks_scheme, | |
| }) | |
| } | |
| } | |
| impl Error for AuthorizationError {} | |
| impl fmt::Display for AuthorizationError { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| if self.sid.is_crates_io() { | |
| let args = if self.default_registry.is_some() { | |
| " --registry crates-io" | |
| } else { | |
| "" | |
| }; | |
| write!(f, "{}, please run `cargo login{args}`", self.reason)?; | |
| if self.supports_cargo_token_credential_provider { | |
| write!(f, "\nor use environment variable CARGO_REGISTRY_TOKEN")?; | |
| } | |
| Ok(()) | |
| } else if let Some(name) = self.sid.alt_registry_key() { | |
| write!( | |
| f, | |
| "{} for `{}`", | |
| self.reason, | |
| self.sid.display_registry_name() | |
| )?; | |
| if self.supports_cargo_token_credential_provider { | |
| let key = ConfigKey::from_str(&format!("registries.{name}.token")); | |
| write!( | |
| f, | |
| ", please run `cargo login --registry {name}`\n\ | |
| or use environment variable {}", | |
| key.as_env_key() | |
| )?; | |
| } else { | |
| write!( | |
| f, | |
| "\nYou may need to log in using this registry's credential provider" | |
| )?; | |
| } | |
| if self.reason == AuthorizationErrorReason::TokenRejected { | |
| if self.token_lacks_scheme == Some(true) { | |
| write!( | |
| f, | |
| "\nnote: the token does not include an authentication scheme" | |
| )?; | |
| } | |
| } | |
| Ok(()) | |
| } else if self.reason == AuthorizationErrorReason::TokenMissing { | |
| write!( | |
| f, | |
| r#"{} for `{}` | |
| consider setting up an alternate registry in Cargo's configuration | |
| as described by https://doc.rust-lang.org/cargo/reference/registries.html | |
| [registries] | |
| my-registry = {{ index = "{}" }} | |
| "#, | |
| self.reason, | |
| self.sid.display_registry_name(), | |
| self.sid.url() | |
| ) | |
| } else { | |
| write!( | |
| f, | |
| r#"{} for `{}`"#, | |
| self.reason, | |
| self.sid.display_registry_name(), | |
| ) | |
| } | |
| } | |
| } | |
| /// Store a token in the cache for future calls. | |
| pub fn cache_token_from_commandline(gctx: &GlobalContext, sid: &SourceId, token: Secret<&str>) { | |
| let url = sid.canonical_url(); | |
| gctx.credential_cache().insert( | |
| url.clone(), | |
| CredentialCacheValue { | |
| token_value: token.to_owned(), | |
| expiration: None, | |
| operation_independent: true, | |
| }, | |
| ); | |
| } | |
| /// List of credential providers built-in to Cargo. | |
| /// Keep in sync with the `match` in `credential_action`. | |
| static BUILT_IN_PROVIDERS: &[&'static str] = &[ | |
| "cargo:token", | |
| "cargo:token-from-stdout", | |
| "cargo:wincred", | |
| "cargo:macos-keychain", | |
| "cargo:libsecret", | |
| ]; | |
| /// Retrieves a cached instance of `LibSecretCredential`. | |
| /// Must be cached to avoid repeated load/unload cycles, which are not supported by `glib`. | |
| #[cfg(target_os = "linux")] | |
| fn get_credential_libsecret() | |
| -> CargoResult<&'static cargo_credential_libsecret::LibSecretCredential> { | |
| static CARGO_CREDENTIAL_LIBSECRET: std::sync::OnceLock< | |
| cargo_credential_libsecret::LibSecretCredential, | |
| > = std::sync::OnceLock::new(); | |
| // Unfortunately `get_or_try_init` is not yet stable. This workaround is not threadsafe but | |
| // loading libsecret twice will only temporary increment the ref counter, which is decrement | |
| // again when `drop` is called. | |
| match CARGO_CREDENTIAL_LIBSECRET.get() { | |
| Some(lib) => Ok(lib), | |
| None => { | |
| let _ = CARGO_CREDENTIAL_LIBSECRET | |
| .set(cargo_credential_libsecret::LibSecretCredential::new()?); | |
| Ok(CARGO_CREDENTIAL_LIBSECRET.get().unwrap()) | |
| } | |
| } | |
| } | |
| fn credential_action( | |
| gctx: &GlobalContext, | |
| sid: &SourceId, | |
| action: Action<'_>, | |
| headers: Vec<String>, | |
| args: &[&str], | |
| require_cred_provider_config: bool, | |
| ) -> CargoResult<CredentialResponse> { | |
| let name = sid.alt_registry_key(); | |
| let registry = RegistryInfo { | |
| index_url: sid.url().as_str(), | |
| name, | |
| headers, | |
| }; | |
| let providers = credential_provider(gctx, sid, require_cred_provider_config, true)?; | |
| let mut any_not_found = false; | |
| for provider in providers { | |
| let args: Vec<&str> = provider | |
| .iter() | |
| .map(String::as_str) | |
| .chain(args.iter().copied()) | |
| .collect(); | |
| let process = args[0]; | |
| tracing::debug!("attempting credential provider: {args:?}"); | |
| // If the available built-in providers are changed, update the `BUILT_IN_PROVIDERS` list. | |
| let provider: Box<dyn Credential> = match process { | |
| "cargo:token" => Box::new(TokenCredential::new(gctx)), | |
| "cargo:token-from-stdout" => Box::new(BasicProcessCredential {}), | |
| #[cfg(windows)] | |
| "cargo:wincred" => Box::new(cargo_credential_wincred::WindowsCredential {}), | |
| #[cfg(target_os = "macos")] | |
| "cargo:macos-keychain" => Box::new(cargo_credential_macos_keychain::MacKeychain {}), | |
| #[cfg(target_os = "linux")] | |
| "cargo:libsecret" => Box::new(get_credential_libsecret()?), | |
| name if BUILT_IN_PROVIDERS.contains(&name) => { | |
| Box::new(cargo_credential::UnsupportedCredential {}) | |
| } | |
| process => Box::new(CredentialProcessCredential::new(process)), | |
| }; | |
| gctx.shell().verbose(|c| { | |
| c.status( | |
| "Credential", | |
| format!( | |
| "{} {action} {}", | |
| args.join(" "), | |
| sid.display_registry_name() | |
| ), | |
| ) | |
| })?; | |
| match provider.perform(®istry, &action, &args[1..]) { | |
| Ok(response) => return Ok(response), | |
| Err(cargo_credential::Error::UrlNotSupported) => {} | |
| Err(cargo_credential::Error::NotFound) => any_not_found = true, | |
| e => { | |
| return e.with_context(|| { | |
| format!( | |
| "credential provider `{}` failed action `{action}`", | |
| args.join(" ") | |
| ) | |
| }); | |
| } | |
| } | |
| } | |
| if any_not_found { | |
| Err(cargo_credential::Error::NotFound.into()) | |
| } else { | |
| anyhow::bail!("no credential providers could handle the request") | |
| } | |
| } | |
| /// Returns the token to use for the given registry. | |
| /// If a `login_url` is provided and a token is not available, the | |
| /// `login_url` will be included in the returned error. | |
| pub fn auth_token( | |
| gctx: &GlobalContext, | |
| sid: &SourceId, | |
| login_url: Option<&Url>, | |
| operation: Operation<'_>, | |
| headers: Vec<String>, | |
| require_cred_provider_config: bool, | |
| ) -> CargoResult<String> { | |
| match auth_token_optional(gctx, sid, operation, headers, require_cred_provider_config)? { | |
| Some(token) => Ok(token.expose()), | |
| None => Err(AuthorizationError::new( | |
| gctx, | |
| *sid, | |
| login_url.cloned(), | |
| AuthorizationErrorReason::TokenMissing, | |
| )? | |
| .into()), | |
| } | |
| } | |
| /// Returns the token to use for the given registry. | |
| fn auth_token_optional( | |
| gctx: &GlobalContext, | |
| sid: &SourceId, | |
| operation: Operation<'_>, | |
| headers: Vec<String>, | |
| require_cred_provider_config: bool, | |
| ) -> CargoResult<Option<Secret<String>>> { | |
| tracing::trace!("token requested for {}", sid.display_registry_name()); | |
| let mut cache = gctx.credential_cache(); | |
| let url = sid.canonical_url(); | |
| if let Some(cached_token) = cache.get(url) { | |
| if cached_token | |
| .expiration | |
| .map(|exp| OffsetDateTime::now_utc() + Duration::minutes(1) < exp) | |
| .unwrap_or(true) | |
| { | |
| if cached_token.operation_independent || matches!(operation, Operation::Read) { | |
| tracing::trace!("using token from in-memory cache"); | |
| return Ok(Some(cached_token.token_value.clone())); | |
| } | |
| } else { | |
| // Remove expired token from the cache | |
| cache.remove(url); | |
| } | |
| } | |
| let credential_response = credential_action( | |
| gctx, | |
| sid, | |
| Action::Get(operation), | |
| headers, | |
| &[], | |
| require_cred_provider_config, | |
| ); | |
| if let Some(e) = credential_response.as_ref().err() { | |
| if let Some(e) = e.downcast_ref::<cargo_credential::Error>() { | |
| if matches!(e, cargo_credential::Error::NotFound) { | |
| return Ok(None); | |
| } | |
| } | |
| } | |
| let credential_response = credential_response?; | |
| let CredentialResponse::Get { | |
| token, | |
| cache: cache_control, | |
| operation_independent, | |
| } = credential_response | |
| else { | |
| bail!( | |
| "credential provider produced unexpected response for `get` request: {credential_response:?}" | |
| ) | |
| }; | |
| let token = Secret::from(token); | |
| tracing::trace!("found token"); | |
| let expiration = match cache_control { | |
| CacheControl::Expires { expiration } => Some(expiration), | |
| CacheControl::Session => None, | |
| CacheControl::Never | _ => return Ok(Some(token)), | |
| }; | |
| cache.insert( | |
| url.clone(), | |
| CredentialCacheValue { | |
| token_value: token.clone(), | |
| expiration, | |
| operation_independent, | |
| }, | |
| ); | |
| Ok(Some(token)) | |
| } | |
| /// Log out from the given registry. | |
| pub fn logout(gctx: &GlobalContext, sid: &SourceId) -> CargoResult<()> { | |
| let credential_response = credential_action(gctx, sid, Action::Logout, vec![], &[], false); | |
| if let Some(e) = credential_response.as_ref().err() { | |
| if let Some(e) = e.downcast_ref::<cargo_credential::Error>() { | |
| if matches!(e, cargo_credential::Error::NotFound) { | |
| gctx.shell().status( | |
| "Logout", | |
| format!( | |
| "not currently logged in to `{}`", | |
| sid.display_registry_name() | |
| ), | |
| )?; | |
| return Ok(()); | |
| } | |
| } | |
| } | |
| let credential_response = credential_response?; | |
| let CredentialResponse::Logout = credential_response else { | |
| bail!( | |
| "credential provider produced unexpected response for `logout` request: {credential_response:?}" | |
| ) | |
| }; | |
| Ok(()) | |
| } | |
| /// Log in to the given registry. | |
| pub fn login( | |
| gctx: &GlobalContext, | |
| sid: &SourceId, | |
| options: LoginOptions<'_>, | |
| args: &[&str], | |
| ) -> CargoResult<()> { | |
| let credential_response = | |
| credential_action(gctx, sid, Action::Login(options), vec![], args, false)?; | |
| let CredentialResponse::Login = credential_response else { | |
| bail!( | |
| "credential provider produced unexpected response for `login` request: {credential_response:?}" | |
| ) | |
| }; | |
| Ok(()) | |
| } |