1use crate::{
4 core::features::cargo_docs_link,
5 util::{CanonicalUrl, CargoResult, GlobalContext, IntoUrl, context::ConfigKey},
6};
7use anyhow::{Context as _, bail};
8use cargo_credential::{
9 Action, CacheControl, Credential, CredentialResponse, LoginOptions, Operation, RegistryInfo,
10 Secret,
11};
12
13use core::fmt;
14use serde::Deserialize;
15use std::error::Error;
16use time::{Duration, OffsetDateTime};
17use url::Url;
18
19use crate::core::SourceId;
20use crate::util::context::Value;
21use crate::util::credential::adaptor::BasicProcessCredential;
22use crate::util::credential::paseto::PasetoCredential;
23
24use super::{
25 context::{CredentialCacheValue, OptValue, PathAndArgs},
26 credential::process::CredentialProcessCredential,
27 credential::token::TokenCredential,
28};
29
30#[derive(Deserialize, Clone, Debug)]
34#[serde(rename_all = "kebab-case")]
35pub struct RegistryConfig {
36 pub index: Option<String>,
37 pub token: OptValue<Secret<String>>,
38 pub credential_provider: Option<PathAndArgs>,
39 pub secret_key: OptValue<Secret<String>>,
40 pub secret_key_subject: Option<String>,
41 #[serde(rename = "protocol")]
42 _protocol: Option<String>,
43}
44
45#[derive(Deserialize)]
50#[serde(rename_all = "kebab-case")]
51pub struct RegistryConfigExtended {
52 pub index: Option<String>,
53 pub token: OptValue<Secret<String>>,
54 pub credential_provider: Option<PathAndArgs>,
55 pub secret_key: OptValue<Secret<String>>,
56 pub secret_key_subject: Option<String>,
57 #[serde(rename = "default")]
58 _default: Option<String>,
59 #[serde(rename = "global-credential-providers")]
60 _global_credential_providers: Option<Vec<String>>,
61}
62
63impl RegistryConfigExtended {
64 pub fn to_registry_config(self) -> RegistryConfig {
65 RegistryConfig {
66 index: self.index,
67 token: self.token,
68 credential_provider: self.credential_provider,
69 secret_key: self.secret_key,
70 secret_key_subject: self.secret_key_subject,
71 _protocol: None,
72 }
73 }
74}
75
76fn credential_provider(
78 gctx: &GlobalContext,
79 sid: &SourceId,
80 require_cred_provider_config: bool,
81 show_warnings: bool,
82) -> CargoResult<Vec<Vec<String>>> {
83 let warn = |message: String| {
84 if show_warnings {
85 gctx.shell().warn(message)
86 } else {
87 Ok(())
88 }
89 };
90
91 let cfg = registry_credential_config_raw(gctx, sid)?;
92 let mut global_provider_defined = true;
93 let default_providers = || {
94 global_provider_defined = false;
95 if gctx.cli_unstable().asymmetric_token {
96 vec![
98 vec!["cargo:token".to_string()],
99 vec!["cargo:paseto".to_string()],
100 ]
101 } else {
102 vec![vec!["cargo:token".to_string()]]
103 }
104 };
105 let global_providers = gctx
106 .get::<Option<Vec<Value<String>>>>("registry.global-credential-providers")?
107 .filter(|p| !p.is_empty())
108 .map(|p| {
109 p.iter()
110 .rev()
111 .map(PathAndArgs::from_whitespace_separated_string)
112 .map(|p| resolve_credential_alias(gctx, p))
113 .collect()
114 })
115 .unwrap_or_else(default_providers);
116 tracing::debug!(?global_providers);
117
118 match cfg {
119 Some(RegistryConfig {
121 credential_provider: Some(provider),
122 token,
123 secret_key,
124 ..
125 }) => {
126 let provider = resolve_credential_alias(gctx, provider);
127 if let Some(token) = token {
128 if provider[0] != "cargo:token" {
129 warn(format!(
130 "{sid} has a token configured in {} that will be ignored \
131 because this registry is configured to use credential-provider `{}`",
132 token.definition, provider[0],
133 ))?;
134 }
135 }
136 if let Some(secret_key) = secret_key {
137 if provider[0] != "cargo:paseto" {
138 warn(format!(
139 "{sid} has a secret-key configured in {} that will be ignored \
140 because this registry is configured to use credential-provider `{}`",
141 secret_key.definition, provider[0],
142 ))?;
143 }
144 }
145 return Ok(vec![provider]);
146 }
147
148 Some(RegistryConfig {
150 token: Some(token),
151 secret_key: Some(secret_key),
152 ..
153 }) if gctx.cli_unstable().asymmetric_token => {
154 let token_pos = global_providers
155 .iter()
156 .position(|p| p.first().map(String::as_str) == Some("cargo:token"));
157 let paseto_pos = global_providers
158 .iter()
159 .position(|p| p.first().map(String::as_str) == Some("cargo:paseto"));
160 match (token_pos, paseto_pos) {
161 (Some(token_pos), Some(paseto_pos)) => {
162 if token_pos < paseto_pos {
163 warn(format!(
164 "{sid} has a `secret_key` configured in {} that will be ignored \
165 because a `token` is also configured, and the `cargo:token` provider is \
166 configured with higher precedence",
167 secret_key.definition
168 ))?;
169 } else {
170 warn(format!(
171 "{sid} has a `token` configured in {} that will be ignored \
172 because a `secret_key` is also configured, and the `cargo:paseto` provider is \
173 configured with higher precedence",
174 token.definition
175 ))?;
176 }
177 }
178 (_, _) => {
179 }
181 }
182 }
183
184 Some(RegistryConfig {
186 token: Some(token), ..
187 }) => {
188 if !global_providers
189 .iter()
190 .any(|p| p.first().map(String::as_str) == Some("cargo:token"))
191 {
192 warn(format!(
193 "{sid} has a token configured in {} that will be ignored \
194 because the `cargo:token` credential provider is not listed in \
195 `registry.global-credential-providers`",
196 token.definition
197 ))?;
198 }
199 }
200
201 Some(RegistryConfig {
203 secret_key: Some(token),
204 ..
205 }) if gctx.cli_unstable().asymmetric_token => {
206 if !global_providers
207 .iter()
208 .any(|p| p.first().map(String::as_str) == Some("cargo:paseto"))
209 {
210 warn(format!(
211 "{sid} has a secret-key configured in {} that will be ignored \
212 because the `cargo:paseto` credential provider is not listed in \
213 `registry.global-credential-providers`",
214 token.definition
215 ))?;
216 }
217 }
218
219 None | Some(RegistryConfig { .. }) => {}
221 };
222 if !global_provider_defined && require_cred_provider_config {
223 bail!(
224 "authenticated registries require a credential-provider to be configured\n\
225 see {} for details",
226 cargo_docs_link("reference/registry-authentication.html")
227 );
228 }
229 Ok(global_providers)
230}
231
232pub fn registry_credential_config_raw(
234 gctx: &GlobalContext,
235 sid: &SourceId,
236) -> CargoResult<Option<RegistryConfig>> {
237 let mut cache = gctx.registry_config();
238 if let Some(cfg) = cache.get(&sid) {
239 return Ok(cfg.clone());
240 }
241 let cfg = registry_credential_config_raw_uncached(gctx, sid)?;
242 cache.insert(*sid, cfg.clone());
243 return Ok(cfg);
244}
245
246fn registry_credential_config_raw_uncached(
247 gctx: &GlobalContext,
248 sid: &SourceId,
249) -> CargoResult<Option<RegistryConfig>> {
250 tracing::trace!("loading credential config for {}", sid);
251 gctx.load_credentials()?;
252 if !sid.is_remote_registry() {
253 bail!(
254 "{} does not support API commands.\n\
255 Check for a source-replacement in .cargo/config.",
256 sid
257 );
258 }
259
260 if sid.is_crates_io() {
262 gctx.check_registry_index_not_set()?;
263 return Ok(gctx
264 .get::<Option<RegistryConfigExtended>>("registry")?
265 .map(|c| c.to_registry_config()));
266 }
267
268 let name = {
281 let index = sid.canonical_url();
283 let mut names: Vec<_> = gctx
284 .env()
285 .filter_map(|(k, v)| {
286 Some((
287 k.strip_prefix("CARGO_REGISTRIES_")?
288 .strip_suffix("_INDEX")?,
289 v,
290 ))
291 })
292 .filter_map(|(k, v)| Some((k, CanonicalUrl::new(&v.into_url().ok()?).ok()?)))
293 .filter(|(_, v)| v == index)
294 .map(|(k, _)| k.to_lowercase())
295 .collect();
296
297 if names.len() == 0 {
299 if let Some(registries) = gctx.values()?.get("registries") {
300 let (registries, _) = registries.table("registries")?;
301 for (name, value) in registries {
302 if let Some(v) = value.table(&format!("registries.{name}"))?.0.get("index") {
303 let (v, _) = v.string(&format!("registries.{name}.index"))?;
304 if index == &CanonicalUrl::new(&v.into_url()?)? {
305 names.push(name.clone());
306 }
307 }
308 }
309 }
310 }
311 names.sort();
312 match names.len() {
313 0 => None,
314 1 => Some(std::mem::take(&mut names[0])),
315 _ => anyhow::bail!(
316 "multiple registries are configured with the same index url '{}': {}",
317 &sid.as_url(),
318 names.join(", ")
319 ),
320 }
321 };
322
323 if let Some(name) = name.as_deref() {
328 if Some(name) != sid.alt_registry_key() {
329 gctx.shell().note(format!(
330 "name of alternative registry `{}` set to `{name}`",
331 sid.url()
332 ))?
333 }
334 }
335
336 if let Some(name) = &name {
337 tracing::debug!("found alternative registry name `{name}` for {sid}");
338 gctx.get::<Option<RegistryConfig>>(&format!("registries.{name}"))
339 } else {
340 tracing::debug!("no registry name found for {sid}");
341 Ok(None)
342 }
343}
344
345fn resolve_credential_alias(gctx: &GlobalContext, mut provider: PathAndArgs) -> Vec<String> {
347 if provider.args.is_empty() {
348 let name = provider.path.raw_value();
349 let key = format!("credential-alias.{name}");
350 if let Ok(alias) = gctx.get::<Value<PathAndArgs>>(&key) {
351 tracing::debug!("resolving credential alias '{key}' -> '{alias:?}'");
352 if BUILT_IN_PROVIDERS.contains(&name) {
353 let _ = gctx.shell().warn(format!(
354 "credential-alias `{name}` (defined in `{}`) will be \
355 ignored because it would shadow a built-in credential-provider",
356 alias.definition
357 ));
358 } else {
359 provider = alias.val;
360 }
361 }
362 }
363 provider.args.insert(
364 0,
365 provider
366 .path
367 .resolve_program(gctx)
368 .to_str()
369 .unwrap()
370 .to_string(),
371 );
372 provider.args
373}
374
375#[derive(Debug, PartialEq)]
376pub enum AuthorizationErrorReason {
377 TokenMissing,
378 TokenRejected,
379}
380
381impl fmt::Display for AuthorizationErrorReason {
382 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383 match self {
384 AuthorizationErrorReason::TokenMissing => write!(f, "no token found"),
385 AuthorizationErrorReason::TokenRejected => write!(f, "token rejected"),
386 }
387 }
388}
389
390#[derive(Debug)]
392pub struct AuthorizationError {
393 sid: SourceId,
395 default_registry: Option<String>,
397 pub login_url: Option<Url>,
399 reason: AuthorizationErrorReason,
401 supports_cargo_token_credential_provider: bool,
403 token_lacks_scheme: Option<bool>,
405}
406
407impl AuthorizationError {
408 pub fn new(
409 gctx: &GlobalContext,
410 sid: SourceId,
411 login_url: Option<Url>,
412 reason: AuthorizationErrorReason,
413 ) -> CargoResult<Self> {
414 let supports_cargo_token_credential_provider =
418 credential_provider(gctx, &sid, false, false)?
419 .iter()
420 .any(|p| p.first().map(String::as_str) == Some("cargo:token"));
421 let cache = gctx.credential_cache();
422 let token_lacks_scheme = cache
423 .get(sid.canonical_url())
424 .map(|entry| !entry.token_value.as_deref().expose().contains(' '));
425 Ok(AuthorizationError {
426 sid,
427 default_registry: gctx.default_registry()?,
428 login_url,
429 reason,
430 supports_cargo_token_credential_provider,
431 token_lacks_scheme,
432 })
433 }
434}
435
436impl Error for AuthorizationError {}
437impl fmt::Display for AuthorizationError {
438 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439 if self.sid.is_crates_io() {
440 let args = if self.default_registry.is_some() {
441 " --registry crates-io"
442 } else {
443 ""
444 };
445 write!(f, "{}, please run `cargo login{args}`", self.reason)?;
446 if self.supports_cargo_token_credential_provider {
447 write!(f, "\nor use environment variable CARGO_REGISTRY_TOKEN")?;
448 }
449 Ok(())
450 } else if let Some(name) = self.sid.alt_registry_key() {
451 write!(
452 f,
453 "{} for `{}`",
454 self.reason,
455 self.sid.display_registry_name()
456 )?;
457 if self.supports_cargo_token_credential_provider {
458 let key = ConfigKey::from_str(&format!("registries.{name}.token"));
459 write!(
460 f,
461 ", please run `cargo login --registry {name}`\n\
462 or use environment variable {}",
463 key.as_env_key()
464 )?;
465 } else {
466 write!(
467 f,
468 "\nYou may need to log in using this registry's credential provider"
469 )?;
470 }
471
472 if self.reason == AuthorizationErrorReason::TokenRejected {
473 if self.token_lacks_scheme == Some(true) {
474 write!(
475 f,
476 "\nnote: the token does not include an authentication scheme"
477 )?;
478 }
479 }
480 Ok(())
481 } else if self.reason == AuthorizationErrorReason::TokenMissing {
482 write!(
483 f,
484 r#"{} for `{}`
485consider setting up an alternate registry in Cargo's configuration
486as described by https://doc.rust-lang.org/cargo/reference/registries.html
487
488[registries]
489my-registry = {{ index = "{}" }}
490"#,
491 self.reason,
492 self.sid.display_registry_name(),
493 self.sid.url()
494 )
495 } else {
496 write!(
497 f,
498 r#"{} for `{}`"#,
499 self.reason,
500 self.sid.display_registry_name(),
501 )
502 }
503 }
504}
505
506pub fn cache_token_from_commandline(gctx: &GlobalContext, sid: &SourceId, token: Secret<&str>) {
508 let url = sid.canonical_url();
509 gctx.credential_cache().insert(
510 url.clone(),
511 CredentialCacheValue {
512 token_value: token.to_owned(),
513 expiration: None,
514 operation_independent: true,
515 },
516 );
517}
518
519static BUILT_IN_PROVIDERS: &[&'static str] = &[
522 "cargo:token",
523 "cargo:paseto",
524 "cargo:token-from-stdout",
525 "cargo:wincred",
526 "cargo:macos-keychain",
527 "cargo:libsecret",
528];
529
530#[cfg(target_os = "linux")]
533fn get_credential_libsecret()
534-> CargoResult<&'static cargo_credential_libsecret::LibSecretCredential> {
535 static CARGO_CREDENTIAL_LIBSECRET: std::sync::OnceLock<
536 cargo_credential_libsecret::LibSecretCredential,
537 > = std::sync::OnceLock::new();
538 match CARGO_CREDENTIAL_LIBSECRET.get() {
542 Some(lib) => Ok(lib),
543 None => {
544 let _ = CARGO_CREDENTIAL_LIBSECRET
545 .set(cargo_credential_libsecret::LibSecretCredential::new()?);
546 Ok(CARGO_CREDENTIAL_LIBSECRET.get().unwrap())
547 }
548 }
549}
550
551fn credential_action(
552 gctx: &GlobalContext,
553 sid: &SourceId,
554 action: Action<'_>,
555 headers: Vec<String>,
556 args: &[&str],
557 require_cred_provider_config: bool,
558) -> CargoResult<CredentialResponse> {
559 let name = sid.alt_registry_key();
560 let registry = RegistryInfo {
561 index_url: sid.url().as_str(),
562 name,
563 headers,
564 };
565 let providers = credential_provider(gctx, sid, require_cred_provider_config, true)?;
566 let mut any_not_found = false;
567 for provider in providers {
568 let args: Vec<&str> = provider
569 .iter()
570 .map(String::as_str)
571 .chain(args.iter().copied())
572 .collect();
573 let process = args[0];
574 tracing::debug!("attempting credential provider: {args:?}");
575 let provider: Box<dyn Credential> = match process {
577 "cargo:token" => Box::new(TokenCredential::new(gctx)),
578 "cargo:paseto" if gctx.cli_unstable().asymmetric_token => {
579 Box::new(PasetoCredential::new(gctx))
580 }
581 "cargo:paseto" => bail!("cargo:paseto requires -Zasymmetric-token"),
582 "cargo:token-from-stdout" => Box::new(BasicProcessCredential {}),
583 #[cfg(windows)]
584 "cargo:wincred" => Box::new(cargo_credential_wincred::WindowsCredential {}),
585 #[cfg(target_os = "macos")]
586 "cargo:macos-keychain" => Box::new(cargo_credential_macos_keychain::MacKeychain {}),
587 #[cfg(target_os = "linux")]
588 "cargo:libsecret" => Box::new(get_credential_libsecret()?),
589 name if BUILT_IN_PROVIDERS.contains(&name) => {
590 Box::new(cargo_credential::UnsupportedCredential {})
591 }
592 process => Box::new(CredentialProcessCredential::new(process)),
593 };
594 gctx.shell().verbose(|c| {
595 c.status(
596 "Credential",
597 format!(
598 "{} {action} {}",
599 args.join(" "),
600 sid.display_registry_name()
601 ),
602 )
603 })?;
604 match provider.perform(®istry, &action, &args[1..]) {
605 Ok(response) => return Ok(response),
606 Err(cargo_credential::Error::UrlNotSupported) => {}
607 Err(cargo_credential::Error::NotFound) => any_not_found = true,
608 e => {
609 return e.with_context(|| {
610 format!(
611 "credential provider `{}` failed action `{action}`",
612 args.join(" ")
613 )
614 });
615 }
616 }
617 }
618 if any_not_found {
619 Err(cargo_credential::Error::NotFound.into())
620 } else {
621 anyhow::bail!("no credential providers could handle the request")
622 }
623}
624
625pub fn auth_token(
629 gctx: &GlobalContext,
630 sid: &SourceId,
631 login_url: Option<&Url>,
632 operation: Operation<'_>,
633 headers: Vec<String>,
634 require_cred_provider_config: bool,
635) -> CargoResult<String> {
636 match auth_token_optional(gctx, sid, operation, headers, require_cred_provider_config)? {
637 Some(token) => Ok(token.expose()),
638 None => Err(AuthorizationError::new(
639 gctx,
640 *sid,
641 login_url.cloned(),
642 AuthorizationErrorReason::TokenMissing,
643 )?
644 .into()),
645 }
646}
647
648fn auth_token_optional(
650 gctx: &GlobalContext,
651 sid: &SourceId,
652 operation: Operation<'_>,
653 headers: Vec<String>,
654 require_cred_provider_config: bool,
655) -> CargoResult<Option<Secret<String>>> {
656 tracing::trace!("token requested for {}", sid.display_registry_name());
657 let mut cache = gctx.credential_cache();
658 let url = sid.canonical_url();
659 if let Some(cached_token) = cache.get(url) {
660 if cached_token
661 .expiration
662 .map(|exp| OffsetDateTime::now_utc() + Duration::minutes(1) < exp)
663 .unwrap_or(true)
664 {
665 if cached_token.operation_independent || matches!(operation, Operation::Read) {
666 tracing::trace!("using token from in-memory cache");
667 return Ok(Some(cached_token.token_value.clone()));
668 }
669 } else {
670 cache.remove(url);
672 }
673 }
674
675 let credential_response = credential_action(
676 gctx,
677 sid,
678 Action::Get(operation),
679 headers,
680 &[],
681 require_cred_provider_config,
682 );
683 if let Some(e) = credential_response.as_ref().err() {
684 if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
685 if matches!(e, cargo_credential::Error::NotFound) {
686 return Ok(None);
687 }
688 }
689 }
690 let credential_response = credential_response?;
691
692 let CredentialResponse::Get {
693 token,
694 cache: cache_control,
695 operation_independent,
696 } = credential_response
697 else {
698 bail!(
699 "credential provider produced unexpected response for `get` request: {credential_response:?}"
700 )
701 };
702 let token = Secret::from(token);
703 tracing::trace!("found token");
704 let expiration = match cache_control {
705 CacheControl::Expires { expiration } => Some(expiration),
706 CacheControl::Session => None,
707 CacheControl::Never | _ => return Ok(Some(token)),
708 };
709
710 cache.insert(
711 url.clone(),
712 CredentialCacheValue {
713 token_value: token.clone(),
714 expiration,
715 operation_independent,
716 },
717 );
718 Ok(Some(token))
719}
720
721pub fn logout(gctx: &GlobalContext, sid: &SourceId) -> CargoResult<()> {
723 let credential_response = credential_action(gctx, sid, Action::Logout, vec![], &[], false);
724 if let Some(e) = credential_response.as_ref().err() {
725 if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
726 if matches!(e, cargo_credential::Error::NotFound) {
727 gctx.shell().status(
728 "Logout",
729 format!(
730 "not currently logged in to `{}`",
731 sid.display_registry_name()
732 ),
733 )?;
734 return Ok(());
735 }
736 }
737 }
738 let credential_response = credential_response?;
739 let CredentialResponse::Logout = credential_response else {
740 bail!(
741 "credential provider produced unexpected response for `logout` request: {credential_response:?}"
742 )
743 };
744 Ok(())
745}
746
747pub fn login(
749 gctx: &GlobalContext,
750 sid: &SourceId,
751 options: LoginOptions<'_>,
752 args: &[&str],
753) -> CargoResult<()> {
754 let credential_response =
755 credential_action(gctx, sid, Action::Login(options), vec![], args, false)?;
756 let CredentialResponse::Login = credential_response else {
757 bail!(
758 "credential provider produced unexpected response for `login` request: {credential_response:?}"
759 )
760 };
761 Ok(())
762}