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