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