1use crate::{
4 context::ConfigKey,
5 util::{CargoResult, GlobalContext},
6 workspace::features::cargo_docs_link,
7};
8use anyhow::{Context as _, bail};
9use cargo_credential::{
10 Action, CacheControl, Credential, CredentialResponse, LoginOptions, Operation, RegistryInfo,
11 Secret,
12};
13
14use core::fmt;
15use std::error::Error;
16use time::{Duration, OffsetDateTime};
17use url::Url;
18
19use crate::context::Value;
20use crate::util::credential::adaptor::BasicProcessCredential;
21use crate::workspace::SourceId;
22
23use super::{credential::process::CredentialProcessCredential, credential::token::TokenCredential};
24use crate::context::{
25 ConfigRelativePathResolveExt, CredentialCacheValue, PathAndArgs, RegistryConfig,
26};
27
28fn credential_provider(
30 gctx: &GlobalContext,
31 sid: &SourceId,
32 require_cred_provider_config: bool,
33 show_warnings: bool,
34) -> CargoResult<Vec<Vec<String>>> {
35 let warn = |message: String| {
36 if show_warnings {
37 gctx.shell().warn(message)
38 } else {
39 Ok(())
40 }
41 };
42
43 let cfg = gctx.registry_config(sid)?;
44 let token = gctx.registry_credentials(sid)?.and_then(|c| c.token);
45 let mut global_provider_defined = true;
46 let default_providers = || {
47 global_provider_defined = false;
48 vec![vec!["cargo:token".to_string()]]
49 };
50 let global_providers = gctx
51 .get::<Option<Vec<Value<String>>>>(["registry", "global-credential-providers"])?
52 .filter(|p| !p.is_empty())
53 .map(|p| {
54 p.iter()
55 .rev()
56 .map(PathAndArgs::from_whitespace_separated_string)
57 .map(|p| resolve_credential_alias(gctx, p))
58 .collect()
59 })
60 .unwrap_or_else(default_providers);
61 tracing::debug!(?global_providers);
62
63 match (cfg, token) {
64 (
66 Some(RegistryConfig {
67 credential_provider: Some(provider),
68 ..
69 }),
70 token,
71 ) => {
72 let provider = resolve_credential_alias(gctx, provider);
73 if let Some(token) = token {
74 if provider[0] != "cargo:token" {
75 warn(format!(
76 "{sid} has a token configured in {} that will be ignored \
77 because this registry is configured to use credential-provider `{}`",
78 token.definition, provider[0],
79 ))?;
80 }
81 }
82 return Ok(vec![provider]);
83 }
84
85 (_, Some(token)) => {
87 if !global_providers
88 .iter()
89 .any(|p| p.first().map(String::as_str) == Some("cargo:token"))
90 {
91 warn(format!(
92 "{sid} has a token configured in {} that will be ignored \
93 because the `cargo:token` credential provider is not listed in \
94 `registry.global-credential-providers`",
95 token.definition
96 ))?;
97 }
98 }
99
100 _ => {}
102 };
103 if !global_provider_defined && require_cred_provider_config {
104 bail!(
105 "authenticated registries require a credential-provider to be configured\n\
106 see {} for details",
107 cargo_docs_link("reference/registry-authentication.html")
108 );
109 }
110 Ok(global_providers)
111}
112
113fn resolve_credential_alias(gctx: &GlobalContext, mut provider: PathAndArgs) -> Vec<String> {
115 if provider.args.is_empty() {
116 let name = provider.path.raw_value();
117 let key = format!("credential-alias.{name}");
118 if let Ok(alias) = gctx.get::<Value<PathAndArgs>>(["credential-alias", name]) {
119 tracing::debug!("resolving credential alias '{key}' -> '{alias:?}'");
120 if BUILT_IN_PROVIDERS.contains(&name) {
121 let _ = gctx.shell().warn(format!(
122 "credential-alias `{name}` (defined in `{}`) will be \
123 ignored because it would shadow a built-in credential-provider",
124 alias.definition
125 ));
126 } else {
127 provider = alias.val;
128 }
129 }
130 }
131 provider.args.insert(
132 0,
133 provider
134 .path
135 .resolve_program(gctx)
136 .to_str()
137 .unwrap()
138 .to_string(),
139 );
140 provider.args
141}
142
143#[derive(Debug, PartialEq)]
144pub enum AuthorizationErrorReason {
145 TokenMissing,
146 TokenRejected,
147}
148
149impl fmt::Display for AuthorizationErrorReason {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 match self {
152 AuthorizationErrorReason::TokenMissing => write!(f, "no token found"),
153 AuthorizationErrorReason::TokenRejected => write!(f, "token rejected"),
154 }
155 }
156}
157
158#[derive(Debug)]
160pub struct AuthorizationError {
161 sid: SourceId,
163 default_registry: Option<String>,
165 pub login_url: Option<Url>,
167 reason: AuthorizationErrorReason,
169 supports_cargo_token_credential_provider: bool,
171 token_lacks_scheme: Option<bool>,
173}
174
175impl AuthorizationError {
176 pub fn new(
177 gctx: &GlobalContext,
178 sid: SourceId,
179 login_url: Option<Url>,
180 reason: AuthorizationErrorReason,
181 ) -> CargoResult<Self> {
182 let supports_cargo_token_credential_provider =
186 credential_provider(gctx, &sid, false, false)?
187 .iter()
188 .any(|p| p.first().map(String::as_str) == Some("cargo:token"));
189 let cache = gctx.credential_cache();
190 let token_lacks_scheme = cache
191 .get(sid.canonical_url())
192 .map(|entry| !entry.token_value.as_deref().expose().contains(' '));
193 Ok(AuthorizationError {
194 sid,
195 default_registry: gctx.default_registry()?,
196 login_url,
197 reason,
198 supports_cargo_token_credential_provider,
199 token_lacks_scheme,
200 })
201 }
202}
203
204impl Error for AuthorizationError {}
205impl fmt::Display for AuthorizationError {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 if self.sid.is_crates_io() {
208 let args = if self.default_registry.is_some() {
209 " --registry crates-io"
210 } else {
211 ""
212 };
213 write!(f, "{}, please run `cargo login{args}`", self.reason)?;
214 if self.supports_cargo_token_credential_provider {
215 write!(f, "\nor use environment variable CARGO_REGISTRY_TOKEN")?;
216 }
217 Ok(())
218 } else if let Some(name) = self.sid.alt_registry_key() {
219 write!(
220 f,
221 "{} for `{}`",
222 self.reason,
223 self.sid.display_registry_name()
224 )?;
225 if self.supports_cargo_token_credential_provider {
226 let key = ConfigKey::from_str(&format!("registries.{name}.token"));
227 write!(
228 f,
229 ", please run `cargo login --registry {name}`\n\
230 or use environment variable {}",
231 key.as_env_key()
232 )?;
233 } else {
234 write!(
235 f,
236 "\nYou may need to log in using this registry's credential provider"
237 )?;
238 }
239
240 if self.reason == AuthorizationErrorReason::TokenRejected {
241 if self.token_lacks_scheme == Some(true) {
242 write!(
243 f,
244 "\nnote: the token does not include an authentication scheme"
245 )?;
246 }
247 }
248 Ok(())
249 } else if self.reason == AuthorizationErrorReason::TokenMissing {
250 write!(
251 f,
252 r#"{} for `{}`
253consider setting up an alternate registry in Cargo's configuration
254as described by https://doc.rust-lang.org/cargo/reference/registries.html
255
256[registries]
257my-registry = {{ index = "{}" }}
258"#,
259 self.reason,
260 self.sid.display_registry_name(),
261 self.sid.url()
262 )
263 } else {
264 write!(
265 f,
266 r#"{} for `{}`"#,
267 self.reason,
268 self.sid.display_registry_name(),
269 )
270 }
271 }
272}
273
274pub fn cache_token_from_commandline(gctx: &GlobalContext, sid: &SourceId, token: Secret<&str>) {
276 let url = sid.canonical_url();
277 gctx.credential_cache().insert(
278 url.clone(),
279 CredentialCacheValue {
280 token_value: token.to_owned(),
281 expiration: None,
282 operation_independent: true,
283 },
284 );
285}
286
287static BUILT_IN_PROVIDERS: &[&'static str] = &[
290 "cargo:token",
291 "cargo:token-from-stdout",
292 "cargo:wincred",
293 "cargo:macos-keychain",
294 "cargo:libsecret",
295];
296
297#[cfg(target_os = "linux")]
300fn get_credential_libsecret()
301-> CargoResult<&'static cargo_credential_libsecret::LibSecretCredential> {
302 static CARGO_CREDENTIAL_LIBSECRET: std::sync::OnceLock<
303 cargo_credential_libsecret::LibSecretCredential,
304 > = std::sync::OnceLock::new();
305 match CARGO_CREDENTIAL_LIBSECRET.get() {
309 Some(lib) => Ok(lib),
310 None => {
311 let _ = CARGO_CREDENTIAL_LIBSECRET
312 .set(cargo_credential_libsecret::LibSecretCredential::new()?);
313 Ok(CARGO_CREDENTIAL_LIBSECRET.get().unwrap())
314 }
315 }
316}
317
318fn credential_action(
319 gctx: &GlobalContext,
320 sid: &SourceId,
321 action: Action<'_>,
322 headers: Vec<String>,
323 args: &[&str],
324 require_cred_provider_config: bool,
325) -> CargoResult<CredentialResponse> {
326 let name = sid.alt_registry_key();
327 let registry = RegistryInfo {
328 index_url: sid.url().as_str(),
329 name,
330 headers,
331 };
332 let providers = credential_provider(gctx, sid, require_cred_provider_config, true)?;
333 let mut any_not_found = false;
334 for provider in providers {
335 let args: Vec<&str> = provider
336 .iter()
337 .map(String::as_str)
338 .chain(args.iter().copied())
339 .collect();
340 let process = args[0];
341 tracing::debug!("attempting credential provider: {args:?}");
342 let provider: Box<dyn Credential> = match process {
344 "cargo:token" => Box::new(TokenCredential::new(gctx)),
345 "cargo:token-from-stdout" => Box::new(BasicProcessCredential {}),
346 #[cfg(windows)]
347 "cargo:wincred" => Box::new(cargo_credential_wincred::WindowsCredential {}),
348 #[cfg(target_os = "macos")]
349 "cargo:macos-keychain" => Box::new(cargo_credential_macos_keychain::MacKeychain {}),
350 #[cfg(target_os = "linux")]
351 "cargo:libsecret" => Box::new(get_credential_libsecret()?),
352 name if BUILT_IN_PROVIDERS.contains(&name) => {
353 Box::new(cargo_credential::UnsupportedCredential {})
354 }
355 process => Box::new(CredentialProcessCredential::new(process)),
356 };
357 gctx.shell().verbose(|c| {
358 c.status(
359 "Credential",
360 format!(
361 "{} {action} {}",
362 args.join(" "),
363 sid.display_registry_name()
364 ),
365 )
366 })?;
367 match provider.perform(®istry, &action, &args[1..]) {
368 Ok(response) => return Ok(response),
369 Err(cargo_credential::Error::UrlNotSupported) => {}
370 Err(cargo_credential::Error::NotFound) => any_not_found = true,
371 e => {
372 return e.with_context(|| {
373 format!(
374 "credential provider `{}` failed action `{action}`",
375 args.join(" ")
376 )
377 });
378 }
379 }
380 }
381 if any_not_found {
382 Err(cargo_credential::Error::NotFound.into())
383 } else {
384 anyhow::bail!("no credential providers could handle the request")
385 }
386}
387
388pub fn auth_token(
392 gctx: &GlobalContext,
393 sid: &SourceId,
394 login_url: Option<&Url>,
395 operation: Operation<'_>,
396 headers: Vec<String>,
397 require_cred_provider_config: bool,
398) -> CargoResult<String> {
399 match auth_token_optional(gctx, sid, operation, headers, require_cred_provider_config)? {
400 Some(token) => Ok(token.expose()),
401 None => Err(AuthorizationError::new(
402 gctx,
403 *sid,
404 login_url.cloned(),
405 AuthorizationErrorReason::TokenMissing,
406 )?
407 .into()),
408 }
409}
410
411fn auth_token_optional(
413 gctx: &GlobalContext,
414 sid: &SourceId,
415 operation: Operation<'_>,
416 headers: Vec<String>,
417 require_cred_provider_config: bool,
418) -> CargoResult<Option<Secret<String>>> {
419 tracing::trace!("token requested for {}", sid.display_registry_name());
420 let mut cache = gctx.credential_cache();
421 let url = sid.canonical_url();
422 if let Some(cached_token) = cache.get(url) {
423 if cached_token
424 .expiration
425 .map(|exp| OffsetDateTime::now_utc() + Duration::minutes(1) < exp)
426 .unwrap_or(true)
427 {
428 if cached_token.operation_independent || matches!(operation, Operation::Read) {
429 tracing::trace!("using token from in-memory cache");
430 return Ok(Some(cached_token.token_value.clone()));
431 }
432 } else {
433 cache.remove(url);
435 }
436 }
437
438 let credential_response = credential_action(
439 gctx,
440 sid,
441 Action::Get(operation),
442 headers,
443 &[],
444 require_cred_provider_config,
445 );
446 if let Some(e) = credential_response.as_ref().err() {
447 if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
448 if matches!(e, cargo_credential::Error::NotFound) {
449 return Ok(None);
450 }
451 }
452 }
453 let credential_response = credential_response?;
454
455 let CredentialResponse::Get {
456 token,
457 cache: cache_control,
458 operation_independent,
459 } = credential_response
460 else {
461 bail!(
462 "credential provider produced unexpected response for `get` request: {credential_response:?}"
463 )
464 };
465 let token = Secret::from(token);
466 tracing::trace!("found token");
467 let expiration = match cache_control {
468 CacheControl::Expires { expiration } => Some(expiration),
469 CacheControl::Session => None,
470 CacheControl::Never | _ => return Ok(Some(token)),
471 };
472
473 cache.insert(
474 url.clone(),
475 CredentialCacheValue {
476 token_value: token.clone(),
477 expiration,
478 operation_independent,
479 },
480 );
481 Ok(Some(token))
482}
483
484pub fn logout(gctx: &GlobalContext, sid: &SourceId) -> CargoResult<()> {
486 let credential_response = credential_action(gctx, sid, Action::Logout, vec![], &[], false);
487 if let Some(e) = credential_response.as_ref().err() {
488 if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
489 if matches!(e, cargo_credential::Error::NotFound) {
490 gctx.shell().status(
491 "Logout",
492 format!(
493 "not currently logged in to `{}`",
494 sid.display_registry_name()
495 ),
496 )?;
497 return Ok(());
498 }
499 }
500 }
501 let credential_response = credential_response?;
502 let CredentialResponse::Logout = credential_response else {
503 bail!(
504 "credential provider produced unexpected response for `logout` request: {credential_response:?}"
505 )
506 };
507 Ok(())
508}
509
510pub fn login(
512 gctx: &GlobalContext,
513 sid: &SourceId,
514 options: LoginOptions<'_>,
515 args: &[&str],
516) -> CargoResult<()> {
517 let credential_response =
518 credential_action(gctx, sid, Action::Login(options), vec![], args, false)?;
519 let CredentialResponse::Login = credential_response else {
520 bail!(
521 "credential provider produced unexpected response for `login` request: {credential_response:?}"
522 )
523 };
524 Ok(())
525}