1use crate::{
4 context::ConfigKey,
5 util::{CanonicalUrl, CargoResult, GlobalContext, IntoUrl},
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::{CredentialCacheValue, GlobalRegistryConfig, PathAndArgs, RegistryConfig};
25
26fn credential_provider(
28 gctx: &GlobalContext,
29 sid: &SourceId,
30 require_cred_provider_config: bool,
31 show_warnings: bool,
32) -> CargoResult<Vec<Vec<String>>> {
33 let warn = |message: String| {
34 if show_warnings {
35 gctx.shell().warn(message)
36 } else {
37 Ok(())
38 }
39 };
40
41 let cfg = registry_credential_config_raw(gctx, sid)?;
42 let mut global_provider_defined = true;
43 let default_providers = || {
44 global_provider_defined = false;
45 vec![vec!["cargo:token".to_string()]]
46 };
47 let global_providers = gctx
48 .get::<Option<Vec<Value<String>>>>(["registry", "global-credential-providers"])?
49 .filter(|p| !p.is_empty())
50 .map(|p| {
51 p.iter()
52 .rev()
53 .map(PathAndArgs::from_whitespace_separated_string)
54 .map(|p| resolve_credential_alias(gctx, p))
55 .collect()
56 })
57 .unwrap_or_else(default_providers);
58 tracing::debug!(?global_providers);
59
60 match cfg {
61 Some(RegistryConfig {
63 credential_provider: Some(provider),
64 token,
65 ..
66 }) => {
67 let provider = resolve_credential_alias(gctx, provider);
68 if let Some(token) = token {
69 if provider[0] != "cargo:token" {
70 warn(format!(
71 "{sid} has a token configured in {} that will be ignored \
72 because this registry is configured to use credential-provider `{}`",
73 token.definition, provider[0],
74 ))?;
75 }
76 }
77 return Ok(vec![provider]);
78 }
79
80 Some(RegistryConfig {
82 token: Some(token), ..
83 }) => {
84 if !global_providers
85 .iter()
86 .any(|p| p.first().map(String::as_str) == Some("cargo:token"))
87 {
88 warn(format!(
89 "{sid} has a token configured in {} that will be ignored \
90 because the `cargo:token` credential provider is not listed in \
91 `registry.global-credential-providers`",
92 token.definition
93 ))?;
94 }
95 }
96
97 None | Some(RegistryConfig { .. }) => {}
99 };
100 if !global_provider_defined && require_cred_provider_config {
101 bail!(
102 "authenticated registries require a credential-provider to be configured\n\
103 see {} for details",
104 cargo_docs_link("reference/registry-authentication.html")
105 );
106 }
107 Ok(global_providers)
108}
109
110pub fn registry_credential_config_raw(
112 gctx: &GlobalContext,
113 sid: &SourceId,
114) -> CargoResult<Option<RegistryConfig>> {
115 let mut cache = gctx.registry_config();
116 if let Some(cfg) = cache.get(&sid) {
117 return Ok(cfg.clone());
118 }
119 let cfg = registry_credential_config_raw_uncached(gctx, sid)?;
120 cache.insert(*sid, cfg.clone());
121 return Ok(cfg);
122}
123
124fn registry_credential_config_raw_uncached(
125 gctx: &GlobalContext,
126 sid: &SourceId,
127) -> CargoResult<Option<RegistryConfig>> {
128 tracing::trace!("loading credential config for {}", sid);
129 gctx.load_credentials()?;
130 if !sid.is_remote_registry() {
131 bail!(
132 "{} does not support API commands.\n\
133 Check for a source-replacement in .cargo/config.",
134 sid
135 );
136 }
137
138 if sid.is_crates_io() {
140 gctx.check_registry_index_not_set()?;
141 return Ok(gctx
142 .get::<Option<GlobalRegistryConfig>>(["registry"])?
143 .map(|c| c.to_registry_config()));
144 }
145
146 let name = {
159 let index = sid.canonical_url();
161 let mut names: Vec<_> = gctx
162 .env()
163 .filter_map(|(k, v)| {
164 Some((
165 k.strip_prefix("CARGO_REGISTRIES_")?
166 .strip_suffix("_INDEX")?,
167 v,
168 ))
169 })
170 .filter_map(|(k, v)| Some((k, CanonicalUrl::new(&v.into_url().ok()?).ok()?)))
171 .filter(|(_, v)| v == index)
172 .map(|(k, _)| k.to_lowercase())
173 .collect();
174
175 if names.len() == 0 {
177 if let Some(registries) = gctx.values()?.get("registries") {
178 let (registries, _) = registries.table("registries")?;
179 for (name, value) in registries {
180 if let Some(v) = value.table(&format!("registries.{name}"))?.0.get("index") {
181 let (v, _) = v.string(&format!("registries.{name}.index"))?;
182 if index == &CanonicalUrl::new(&v.into_url()?)? {
183 names.push(name.clone());
184 }
185 }
186 }
187 }
188 }
189 names.sort();
190 match names.len() {
191 0 => None,
192 1 => Some(std::mem::take(&mut names[0])),
193 _ => anyhow::bail!(
194 "multiple registries are configured with the same index url '{}': {}",
195 &sid.as_url(),
196 names.join(", ")
197 ),
198 }
199 };
200
201 if let Some(name) = name.as_deref() {
206 if Some(name) != sid.alt_registry_key() {
207 gctx.shell().note(format!(
208 "name of alternative registry `{}` set to `{name}`",
209 sid.url()
210 ))?
211 }
212 }
213
214 if let Some(name) = &name {
215 tracing::debug!("found alternative registry name `{name}` for {sid}");
216 gctx.get::<Option<RegistryConfig>>(["registries", name.as_str()])
217 } else {
218 tracing::debug!("no registry name found for {sid}");
219 Ok(None)
220 }
221}
222
223fn resolve_credential_alias(gctx: &GlobalContext, mut provider: PathAndArgs) -> Vec<String> {
225 if provider.args.is_empty() {
226 let name = provider.path.raw_value();
227 let key = format!("credential-alias.{name}");
228 if let Ok(alias) = gctx.get::<Value<PathAndArgs>>(["credential-alias", name]) {
229 tracing::debug!("resolving credential alias '{key}' -> '{alias:?}'");
230 if BUILT_IN_PROVIDERS.contains(&name) {
231 let _ = gctx.shell().warn(format!(
232 "credential-alias `{name}` (defined in `{}`) will be \
233 ignored because it would shadow a built-in credential-provider",
234 alias.definition
235 ));
236 } else {
237 provider = alias.val;
238 }
239 }
240 }
241 provider.args.insert(
242 0,
243 provider
244 .path
245 .resolve_program(gctx)
246 .to_str()
247 .unwrap()
248 .to_string(),
249 );
250 provider.args
251}
252
253#[derive(Debug, PartialEq)]
254pub enum AuthorizationErrorReason {
255 TokenMissing,
256 TokenRejected,
257}
258
259impl fmt::Display for AuthorizationErrorReason {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 match self {
262 AuthorizationErrorReason::TokenMissing => write!(f, "no token found"),
263 AuthorizationErrorReason::TokenRejected => write!(f, "token rejected"),
264 }
265 }
266}
267
268#[derive(Debug)]
270pub struct AuthorizationError {
271 sid: SourceId,
273 default_registry: Option<String>,
275 pub login_url: Option<Url>,
277 reason: AuthorizationErrorReason,
279 supports_cargo_token_credential_provider: bool,
281 token_lacks_scheme: Option<bool>,
283}
284
285impl AuthorizationError {
286 pub fn new(
287 gctx: &GlobalContext,
288 sid: SourceId,
289 login_url: Option<Url>,
290 reason: AuthorizationErrorReason,
291 ) -> CargoResult<Self> {
292 let supports_cargo_token_credential_provider =
296 credential_provider(gctx, &sid, false, false)?
297 .iter()
298 .any(|p| p.first().map(String::as_str) == Some("cargo:token"));
299 let cache = gctx.credential_cache();
300 let token_lacks_scheme = cache
301 .get(sid.canonical_url())
302 .map(|entry| !entry.token_value.as_deref().expose().contains(' '));
303 Ok(AuthorizationError {
304 sid,
305 default_registry: gctx.default_registry()?,
306 login_url,
307 reason,
308 supports_cargo_token_credential_provider,
309 token_lacks_scheme,
310 })
311 }
312}
313
314impl Error for AuthorizationError {}
315impl fmt::Display for AuthorizationError {
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 if self.sid.is_crates_io() {
318 let args = if self.default_registry.is_some() {
319 " --registry crates-io"
320 } else {
321 ""
322 };
323 write!(f, "{}, please run `cargo login{args}`", self.reason)?;
324 if self.supports_cargo_token_credential_provider {
325 write!(f, "\nor use environment variable CARGO_REGISTRY_TOKEN")?;
326 }
327 Ok(())
328 } else if let Some(name) = self.sid.alt_registry_key() {
329 write!(
330 f,
331 "{} for `{}`",
332 self.reason,
333 self.sid.display_registry_name()
334 )?;
335 if self.supports_cargo_token_credential_provider {
336 let key = ConfigKey::from_str(&format!("registries.{name}.token"));
337 write!(
338 f,
339 ", please run `cargo login --registry {name}`\n\
340 or use environment variable {}",
341 key.as_env_key()
342 )?;
343 } else {
344 write!(
345 f,
346 "\nYou may need to log in using this registry's credential provider"
347 )?;
348 }
349
350 if self.reason == AuthorizationErrorReason::TokenRejected {
351 if self.token_lacks_scheme == Some(true) {
352 write!(
353 f,
354 "\nnote: the token does not include an authentication scheme"
355 )?;
356 }
357 }
358 Ok(())
359 } else if self.reason == AuthorizationErrorReason::TokenMissing {
360 write!(
361 f,
362 r#"{} for `{}`
363consider setting up an alternate registry in Cargo's configuration
364as described by https://doc.rust-lang.org/cargo/reference/registries.html
365
366[registries]
367my-registry = {{ index = "{}" }}
368"#,
369 self.reason,
370 self.sid.display_registry_name(),
371 self.sid.url()
372 )
373 } else {
374 write!(
375 f,
376 r#"{} for `{}`"#,
377 self.reason,
378 self.sid.display_registry_name(),
379 )
380 }
381 }
382}
383
384pub fn cache_token_from_commandline(gctx: &GlobalContext, sid: &SourceId, token: Secret<&str>) {
386 let url = sid.canonical_url();
387 gctx.credential_cache().insert(
388 url.clone(),
389 CredentialCacheValue {
390 token_value: token.to_owned(),
391 expiration: None,
392 operation_independent: true,
393 },
394 );
395}
396
397static BUILT_IN_PROVIDERS: &[&'static str] = &[
400 "cargo:token",
401 "cargo:token-from-stdout",
402 "cargo:wincred",
403 "cargo:macos-keychain",
404 "cargo:libsecret",
405];
406
407#[cfg(target_os = "linux")]
410fn get_credential_libsecret()
411-> CargoResult<&'static cargo_credential_libsecret::LibSecretCredential> {
412 static CARGO_CREDENTIAL_LIBSECRET: std::sync::OnceLock<
413 cargo_credential_libsecret::LibSecretCredential,
414 > = std::sync::OnceLock::new();
415 match CARGO_CREDENTIAL_LIBSECRET.get() {
419 Some(lib) => Ok(lib),
420 None => {
421 let _ = CARGO_CREDENTIAL_LIBSECRET
422 .set(cargo_credential_libsecret::LibSecretCredential::new()?);
423 Ok(CARGO_CREDENTIAL_LIBSECRET.get().unwrap())
424 }
425 }
426}
427
428fn credential_action(
429 gctx: &GlobalContext,
430 sid: &SourceId,
431 action: Action<'_>,
432 headers: Vec<String>,
433 args: &[&str],
434 require_cred_provider_config: bool,
435) -> CargoResult<CredentialResponse> {
436 let name = sid.alt_registry_key();
437 let registry = RegistryInfo {
438 index_url: sid.url().as_str(),
439 name,
440 headers,
441 };
442 let providers = credential_provider(gctx, sid, require_cred_provider_config, true)?;
443 let mut any_not_found = false;
444 for provider in providers {
445 let args: Vec<&str> = provider
446 .iter()
447 .map(String::as_str)
448 .chain(args.iter().copied())
449 .collect();
450 let process = args[0];
451 tracing::debug!("attempting credential provider: {args:?}");
452 let provider: Box<dyn Credential> = match process {
454 "cargo:token" => Box::new(TokenCredential::new(gctx)),
455 "cargo:token-from-stdout" => Box::new(BasicProcessCredential {}),
456 #[cfg(windows)]
457 "cargo:wincred" => Box::new(cargo_credential_wincred::WindowsCredential {}),
458 #[cfg(target_os = "macos")]
459 "cargo:macos-keychain" => Box::new(cargo_credential_macos_keychain::MacKeychain {}),
460 #[cfg(target_os = "linux")]
461 "cargo:libsecret" => Box::new(get_credential_libsecret()?),
462 name if BUILT_IN_PROVIDERS.contains(&name) => {
463 Box::new(cargo_credential::UnsupportedCredential {})
464 }
465 process => Box::new(CredentialProcessCredential::new(process)),
466 };
467 gctx.shell().verbose(|c| {
468 c.status(
469 "Credential",
470 format!(
471 "{} {action} {}",
472 args.join(" "),
473 sid.display_registry_name()
474 ),
475 )
476 })?;
477 match provider.perform(®istry, &action, &args[1..]) {
478 Ok(response) => return Ok(response),
479 Err(cargo_credential::Error::UrlNotSupported) => {}
480 Err(cargo_credential::Error::NotFound) => any_not_found = true,
481 e => {
482 return e.with_context(|| {
483 format!(
484 "credential provider `{}` failed action `{action}`",
485 args.join(" ")
486 )
487 });
488 }
489 }
490 }
491 if any_not_found {
492 Err(cargo_credential::Error::NotFound.into())
493 } else {
494 anyhow::bail!("no credential providers could handle the request")
495 }
496}
497
498pub fn auth_token(
502 gctx: &GlobalContext,
503 sid: &SourceId,
504 login_url: Option<&Url>,
505 operation: Operation<'_>,
506 headers: Vec<String>,
507 require_cred_provider_config: bool,
508) -> CargoResult<String> {
509 match auth_token_optional(gctx, sid, operation, headers, require_cred_provider_config)? {
510 Some(token) => Ok(token.expose()),
511 None => Err(AuthorizationError::new(
512 gctx,
513 *sid,
514 login_url.cloned(),
515 AuthorizationErrorReason::TokenMissing,
516 )?
517 .into()),
518 }
519}
520
521fn auth_token_optional(
523 gctx: &GlobalContext,
524 sid: &SourceId,
525 operation: Operation<'_>,
526 headers: Vec<String>,
527 require_cred_provider_config: bool,
528) -> CargoResult<Option<Secret<String>>> {
529 tracing::trace!("token requested for {}", sid.display_registry_name());
530 let mut cache = gctx.credential_cache();
531 let url = sid.canonical_url();
532 if let Some(cached_token) = cache.get(url) {
533 if cached_token
534 .expiration
535 .map(|exp| OffsetDateTime::now_utc() + Duration::minutes(1) < exp)
536 .unwrap_or(true)
537 {
538 if cached_token.operation_independent || matches!(operation, Operation::Read) {
539 tracing::trace!("using token from in-memory cache");
540 return Ok(Some(cached_token.token_value.clone()));
541 }
542 } else {
543 cache.remove(url);
545 }
546 }
547
548 let credential_response = credential_action(
549 gctx,
550 sid,
551 Action::Get(operation),
552 headers,
553 &[],
554 require_cred_provider_config,
555 );
556 if let Some(e) = credential_response.as_ref().err() {
557 if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
558 if matches!(e, cargo_credential::Error::NotFound) {
559 return Ok(None);
560 }
561 }
562 }
563 let credential_response = credential_response?;
564
565 let CredentialResponse::Get {
566 token,
567 cache: cache_control,
568 operation_independent,
569 } = credential_response
570 else {
571 bail!(
572 "credential provider produced unexpected response for `get` request: {credential_response:?}"
573 )
574 };
575 let token = Secret::from(token);
576 tracing::trace!("found token");
577 let expiration = match cache_control {
578 CacheControl::Expires { expiration } => Some(expiration),
579 CacheControl::Session => None,
580 CacheControl::Never | _ => return Ok(Some(token)),
581 };
582
583 cache.insert(
584 url.clone(),
585 CredentialCacheValue {
586 token_value: token.clone(),
587 expiration,
588 operation_independent,
589 },
590 );
591 Ok(Some(token))
592}
593
594pub fn logout(gctx: &GlobalContext, sid: &SourceId) -> CargoResult<()> {
596 let credential_response = credential_action(gctx, sid, Action::Logout, vec![], &[], false);
597 if let Some(e) = credential_response.as_ref().err() {
598 if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
599 if matches!(e, cargo_credential::Error::NotFound) {
600 gctx.shell().status(
601 "Logout",
602 format!(
603 "not currently logged in to `{}`",
604 sid.display_registry_name()
605 ),
606 )?;
607 return Ok(());
608 }
609 }
610 }
611 let credential_response = credential_response?;
612 let CredentialResponse::Logout = credential_response else {
613 bail!(
614 "credential provider produced unexpected response for `logout` request: {credential_response:?}"
615 )
616 };
617 Ok(())
618}
619
620pub fn login(
622 gctx: &GlobalContext,
623 sid: &SourceId,
624 options: LoginOptions<'_>,
625 args: &[&str],
626) -> CargoResult<()> {
627 let credential_response =
628 credential_action(gctx, sid, Action::Login(options), vec![], args, false)?;
629 let CredentialResponse::Login = credential_response else {
630 bail!(
631 "credential provider produced unexpected response for `login` request: {credential_response:?}"
632 )
633 };
634 Ok(())
635}