1use crate::{
4 core::features::cargo_docs_link,
5 util::{context::ConfigKey, CanonicalUrl, CargoResult, GlobalContext, IntoUrl},
6};
7use anyhow::{bail, Context as _};
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!("{sid} has a `token` configured in {} that will be ignored \
171 because a `secret_key` is also configured, and the `cargo:paseto` provider is \
172 configured with higher precedence", token.definition))?;
173 }
174 }
175 (_, _) => {
176 }
178 }
179 }
180
181 Some(RegistryConfig {
183 token: Some(token), ..
184 }) => {
185 if !global_providers
186 .iter()
187 .any(|p| p.first().map(String::as_str) == Some("cargo:token"))
188 {
189 warn(format!(
190 "{sid} has a token configured in {} that will be ignored \
191 because the `cargo:token` credential provider is not listed in \
192 `registry.global-credential-providers`",
193 token.definition
194 ))?;
195 }
196 }
197
198 Some(RegistryConfig {
200 secret_key: Some(token),
201 ..
202 }) if gctx.cli_unstable().asymmetric_token => {
203 if !global_providers
204 .iter()
205 .any(|p| p.first().map(String::as_str) == Some("cargo:paseto"))
206 {
207 warn(format!(
208 "{sid} has a secret-key configured in {} that will be ignored \
209 because the `cargo:paseto` credential provider is not listed in \
210 `registry.global-credential-providers`",
211 token.definition
212 ))?;
213 }
214 }
215
216 None | Some(RegistryConfig { .. }) => {}
218 };
219 if !global_provider_defined && require_cred_provider_config {
220 bail!(
221 "authenticated registries require a credential-provider to be configured\n\
222 see {} for details",
223 cargo_docs_link("reference/registry-authentication.html")
224 );
225 }
226 Ok(global_providers)
227}
228
229pub fn registry_credential_config_raw(
231 gctx: &GlobalContext,
232 sid: &SourceId,
233) -> CargoResult<Option<RegistryConfig>> {
234 let mut cache = gctx.registry_config();
235 if let Some(cfg) = cache.get(&sid) {
236 return Ok(cfg.clone());
237 }
238 let cfg = registry_credential_config_raw_uncached(gctx, sid)?;
239 cache.insert(*sid, cfg.clone());
240 return Ok(cfg);
241}
242
243fn registry_credential_config_raw_uncached(
244 gctx: &GlobalContext,
245 sid: &SourceId,
246) -> CargoResult<Option<RegistryConfig>> {
247 tracing::trace!("loading credential config for {}", sid);
248 gctx.load_credentials()?;
249 if !sid.is_remote_registry() {
250 bail!(
251 "{} does not support API commands.\n\
252 Check for a source-replacement in .cargo/config.",
253 sid
254 );
255 }
256
257 if sid.is_crates_io() {
259 gctx.check_registry_index_not_set()?;
260 return Ok(gctx
261 .get::<Option<RegistryConfigExtended>>("registry")?
262 .map(|c| c.to_registry_config()));
263 }
264
265 let name = {
278 let index = sid.canonical_url();
280 let mut names: Vec<_> = gctx
281 .env()
282 .filter_map(|(k, v)| {
283 Some((
284 k.strip_prefix("CARGO_REGISTRIES_")?
285 .strip_suffix("_INDEX")?,
286 v,
287 ))
288 })
289 .filter_map(|(k, v)| Some((k, CanonicalUrl::new(&v.into_url().ok()?).ok()?)))
290 .filter(|(_, v)| v == index)
291 .map(|(k, _)| k.to_lowercase())
292 .collect();
293
294 if names.len() == 0 {
296 if let Some(registries) = gctx.values()?.get("registries") {
297 let (registries, _) = registries.table("registries")?;
298 for (name, value) in registries {
299 if let Some(v) = value.table(&format!("registries.{name}"))?.0.get("index") {
300 let (v, _) = v.string(&format!("registries.{name}.index"))?;
301 if index == &CanonicalUrl::new(&v.into_url()?)? {
302 names.push(name.clone());
303 }
304 }
305 }
306 }
307 }
308 names.sort();
309 match names.len() {
310 0 => None,
311 1 => Some(std::mem::take(&mut names[0])),
312 _ => anyhow::bail!(
313 "multiple registries are configured with the same index url '{}': {}",
314 &sid.as_url(),
315 names.join(", ")
316 ),
317 }
318 };
319
320 if let Some(name) = name.as_deref() {
325 if Some(name) != sid.alt_registry_key() {
326 gctx.shell().note(format!(
327 "name of alternative registry `{}` set to `{name}`",
328 sid.url()
329 ))?
330 }
331 }
332
333 if let Some(name) = &name {
334 tracing::debug!("found alternative registry name `{name}` for {sid}");
335 gctx.get::<Option<RegistryConfig>>(&format!("registries.{name}"))
336 } else {
337 tracing::debug!("no registry name found for {sid}");
338 Ok(None)
339 }
340}
341
342fn resolve_credential_alias(gctx: &GlobalContext, mut provider: PathAndArgs) -> Vec<String> {
344 if provider.args.is_empty() {
345 let name = provider.path.raw_value();
346 let key = format!("credential-alias.{name}");
347 if let Ok(alias) = gctx.get::<Value<PathAndArgs>>(&key) {
348 tracing::debug!("resolving credential alias '{key}' -> '{alias:?}'");
349 if BUILT_IN_PROVIDERS.contains(&name) {
350 let _ = gctx.shell().warn(format!(
351 "credential-alias `{name}` (defined in `{}`) will be \
352 ignored because it would shadow a built-in credential-provider",
353 alias.definition
354 ));
355 } else {
356 provider = alias.val;
357 }
358 }
359 }
360 provider.args.insert(
361 0,
362 provider
363 .path
364 .resolve_program(gctx)
365 .to_str()
366 .unwrap()
367 .to_string(),
368 );
369 provider.args
370}
371
372#[derive(Debug, PartialEq)]
373pub enum AuthorizationErrorReason {
374 TokenMissing,
375 TokenRejected,
376}
377
378impl fmt::Display for AuthorizationErrorReason {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 match self {
381 AuthorizationErrorReason::TokenMissing => write!(f, "no token found"),
382 AuthorizationErrorReason::TokenRejected => write!(f, "token rejected"),
383 }
384 }
385}
386
387#[derive(Debug)]
389pub struct AuthorizationError {
390 sid: SourceId,
392 default_registry: Option<String>,
394 pub login_url: Option<Url>,
396 reason: AuthorizationErrorReason,
398 supports_cargo_token_credential_provider: bool,
400}
401
402impl AuthorizationError {
403 pub fn new(
404 gctx: &GlobalContext,
405 sid: SourceId,
406 login_url: Option<Url>,
407 reason: AuthorizationErrorReason,
408 ) -> CargoResult<Self> {
409 let supports_cargo_token_credential_provider =
413 credential_provider(gctx, &sid, false, false)?
414 .iter()
415 .any(|p| p.first().map(String::as_str) == Some("cargo:token"));
416 Ok(AuthorizationError {
417 sid,
418 default_registry: gctx.default_registry()?,
419 login_url,
420 reason,
421 supports_cargo_token_credential_provider,
422 })
423 }
424}
425
426impl Error for AuthorizationError {}
427impl fmt::Display for AuthorizationError {
428 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429 if self.sid.is_crates_io() {
430 let args = if self.default_registry.is_some() {
431 " --registry crates-io"
432 } else {
433 ""
434 };
435 write!(f, "{}, please run `cargo login{args}`", self.reason)?;
436 if self.supports_cargo_token_credential_provider {
437 write!(f, "\nor use environment variable CARGO_REGISTRY_TOKEN")?;
438 }
439 Ok(())
440 } else if let Some(name) = self.sid.alt_registry_key() {
441 write!(
442 f,
443 "{} for `{}`",
444 self.reason,
445 self.sid.display_registry_name()
446 )?;
447 if self.supports_cargo_token_credential_provider {
448 let key = ConfigKey::from_str(&format!("registries.{name}.token"));
449 write!(
450 f,
451 ", please run `cargo login --registry {name}`\n\
452 or use environment variable {}",
453 key.as_env_key()
454 )?;
455 } else {
456 write!(
457 f,
458 "\nYou may need to log in using this registry's credential provider"
459 )?;
460 }
461 Ok(())
462 } else if self.reason == AuthorizationErrorReason::TokenMissing {
463 write!(
464 f,
465 r#"{} for `{}`
466consider setting up an alternate registry in Cargo's configuration
467as described by https://doc.rust-lang.org/cargo/reference/registries.html
468
469[registries]
470my-registry = {{ index = "{}" }}
471"#,
472 self.reason,
473 self.sid.display_registry_name(),
474 self.sid.url()
475 )
476 } else {
477 write!(
478 f,
479 r#"{} for `{}`"#,
480 self.reason,
481 self.sid.display_registry_name(),
482 )
483 }
484 }
485}
486
487pub fn cache_token_from_commandline(gctx: &GlobalContext, sid: &SourceId, token: Secret<&str>) {
489 let url = sid.canonical_url();
490 gctx.credential_cache().insert(
491 url.clone(),
492 CredentialCacheValue {
493 token_value: token.to_owned(),
494 expiration: None,
495 operation_independent: true,
496 },
497 );
498}
499
500static BUILT_IN_PROVIDERS: &[&'static str] = &[
503 "cargo:token",
504 "cargo:paseto",
505 "cargo:token-from-stdout",
506 "cargo:wincred",
507 "cargo:macos-keychain",
508 "cargo:libsecret",
509];
510
511fn credential_action(
512 gctx: &GlobalContext,
513 sid: &SourceId,
514 action: Action<'_>,
515 headers: Vec<String>,
516 args: &[&str],
517 require_cred_provider_config: bool,
518) -> CargoResult<CredentialResponse> {
519 let name = sid.alt_registry_key();
520 let registry = RegistryInfo {
521 index_url: sid.url().as_str(),
522 name,
523 headers,
524 };
525 let providers = credential_provider(gctx, sid, require_cred_provider_config, true)?;
526 let mut any_not_found = false;
527 for provider in providers {
528 let args: Vec<&str> = provider
529 .iter()
530 .map(String::as_str)
531 .chain(args.iter().copied())
532 .collect();
533 let process = args[0];
534 tracing::debug!("attempting credential provider: {args:?}");
535 let provider: Box<dyn Credential> = match process {
537 "cargo:token" => Box::new(TokenCredential::new(gctx)),
538 "cargo:paseto" if gctx.cli_unstable().asymmetric_token => {
539 Box::new(PasetoCredential::new(gctx))
540 }
541 "cargo:paseto" => bail!("cargo:paseto requires -Zasymmetric-token"),
542 "cargo:token-from-stdout" => Box::new(BasicProcessCredential {}),
543 #[cfg(windows)]
544 "cargo:wincred" => Box::new(cargo_credential_wincred::WindowsCredential {}),
545 #[cfg(target_os = "macos")]
546 "cargo:macos-keychain" => Box::new(cargo_credential_macos_keychain::MacKeychain {}),
547 #[cfg(target_os = "linux")]
548 "cargo:libsecret" => Box::new(cargo_credential_libsecret::LibSecretCredential {}),
549 name if BUILT_IN_PROVIDERS.contains(&name) => {
550 Box::new(cargo_credential::UnsupportedCredential {})
551 }
552 process => Box::new(CredentialProcessCredential::new(process)),
553 };
554 gctx.shell().verbose(|c| {
555 c.status(
556 "Credential",
557 format!(
558 "{} {action} {}",
559 args.join(" "),
560 sid.display_registry_name()
561 ),
562 )
563 })?;
564 match provider.perform(®istry, &action, &args[1..]) {
565 Ok(response) => return Ok(response),
566 Err(cargo_credential::Error::UrlNotSupported) => {}
567 Err(cargo_credential::Error::NotFound) => any_not_found = true,
568 e => {
569 return e.with_context(|| {
570 format!(
571 "credential provider `{}` failed action `{action}`",
572 args.join(" ")
573 )
574 })
575 }
576 }
577 }
578 if any_not_found {
579 Err(cargo_credential::Error::NotFound.into())
580 } else {
581 anyhow::bail!("no credential providers could handle the request")
582 }
583}
584
585pub fn auth_token(
589 gctx: &GlobalContext,
590 sid: &SourceId,
591 login_url: Option<&Url>,
592 operation: Operation<'_>,
593 headers: Vec<String>,
594 require_cred_provider_config: bool,
595) -> CargoResult<String> {
596 match auth_token_optional(gctx, sid, operation, headers, require_cred_provider_config)? {
597 Some(token) => Ok(token.expose()),
598 None => Err(AuthorizationError::new(
599 gctx,
600 *sid,
601 login_url.cloned(),
602 AuthorizationErrorReason::TokenMissing,
603 )?
604 .into()),
605 }
606}
607
608fn auth_token_optional(
610 gctx: &GlobalContext,
611 sid: &SourceId,
612 operation: Operation<'_>,
613 headers: Vec<String>,
614 require_cred_provider_config: bool,
615) -> CargoResult<Option<Secret<String>>> {
616 tracing::trace!("token requested for {}", sid.display_registry_name());
617 let mut cache = gctx.credential_cache();
618 let url = sid.canonical_url();
619 if let Some(cached_token) = cache.get(url) {
620 if cached_token
621 .expiration
622 .map(|exp| OffsetDateTime::now_utc() + Duration::minutes(1) < exp)
623 .unwrap_or(true)
624 {
625 if cached_token.operation_independent || matches!(operation, Operation::Read) {
626 tracing::trace!("using token from in-memory cache");
627 return Ok(Some(cached_token.token_value.clone()));
628 }
629 } else {
630 cache.remove(url);
632 }
633 }
634
635 let credential_response = credential_action(
636 gctx,
637 sid,
638 Action::Get(operation),
639 headers,
640 &[],
641 require_cred_provider_config,
642 );
643 if let Some(e) = credential_response.as_ref().err() {
644 if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
645 if matches!(e, cargo_credential::Error::NotFound) {
646 return Ok(None);
647 }
648 }
649 }
650 let credential_response = credential_response?;
651
652 let CredentialResponse::Get {
653 token,
654 cache: cache_control,
655 operation_independent,
656 } = credential_response
657 else {
658 bail!("credential provider produced unexpected response for `get` request: {credential_response:?}")
659 };
660 let token = Secret::from(token);
661 tracing::trace!("found token");
662 let expiration = match cache_control {
663 CacheControl::Expires { expiration } => Some(expiration),
664 CacheControl::Session => None,
665 CacheControl::Never | _ => return Ok(Some(token)),
666 };
667
668 cache.insert(
669 url.clone(),
670 CredentialCacheValue {
671 token_value: token.clone(),
672 expiration,
673 operation_independent,
674 },
675 );
676 Ok(Some(token))
677}
678
679pub fn logout(gctx: &GlobalContext, sid: &SourceId) -> CargoResult<()> {
681 let credential_response = credential_action(gctx, sid, Action::Logout, vec![], &[], false);
682 if let Some(e) = credential_response.as_ref().err() {
683 if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
684 if matches!(e, cargo_credential::Error::NotFound) {
685 gctx.shell().status(
686 "Logout",
687 format!(
688 "not currently logged in to `{}`",
689 sid.display_registry_name()
690 ),
691 )?;
692 return Ok(());
693 }
694 }
695 }
696 let credential_response = credential_response?;
697 let CredentialResponse::Logout = credential_response else {
698 bail!("credential provider produced unexpected response for `logout` request: {credential_response:?}")
699 };
700 Ok(())
701}
702
703pub fn login(
705 gctx: &GlobalContext,
706 sid: &SourceId,
707 options: LoginOptions<'_>,
708 args: &[&str],
709) -> CargoResult<()> {
710 let credential_response =
711 credential_action(gctx, sid, Action::Login(options), vec![], args, false)?;
712 let CredentialResponse::Login = credential_response else {
713 bail!("credential provider produced unexpected response for `login` request: {credential_response:?}")
714 };
715 Ok(())
716}