Skip to main content

cargo/util/auth/
mod.rs

1//! Registry authentication support.
2
3use 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/// `[registries.NAME]` tables.
31///
32/// The values here should be kept in sync with `RegistryConfigExtended`
33#[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/// The `[registry]` table, which has more keys than the `[registries.NAME]` tables.
46///
47/// Note: nesting `RegistryConfig` inside this struct and using `serde(flatten)` *should* work
48/// but fails with "invalid type: sequence, expected a value" when attempting to deserialize.
49#[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
76/// Get the list of credential providers for a registry source.
77fn 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            // Enable the PASETO provider
97            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        // If there's a specific provider configured for this registry, use it.
120        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        // Warning for both `token` and `secret-key`, stating which will be ignored
149        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                    // One or both of the below individual warnings will trigger
180                }
181            }
182        }
183
184        // Check if a `token` is configured that will be ignored.
185        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        // Check if a asymmetric token is configured that will be ignored.
202        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        // If we couldn't find a registry-specific provider, use the fallback provider list.
220        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
232/// Get the credential configuration for a `SourceId`.
233pub 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    // Handle crates.io specially, since it uses different configuration keys.
261    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    // Find the SourceId's name by its index URL. If environment variables
269    // are available they will be preferred over configuration values.
270    //
271    // The fundamental problem is that we only know the index url of the registry
272    // for certain. For example, an unnamed registry source can come from the `--index`
273    // command line argument, or from a Cargo.lock file. For this reason, we always
274    // attempt to discover the name by looking it up by the index URL.
275    //
276    // This also allows the authorization token for a registry to be set
277    // without knowing the registry name by using the _INDEX and _TOKEN
278    // environment variables.
279
280    let name = {
281        // Discover names from environment variables.
282        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        // Discover names from the configuration only if none were found in the environment.
298        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    // It's possible to have a registry configured in a Cargo config file,
324    // then override it with configuration from environment variables.
325    // If the name doesn't match, leave a note to help the user understand
326    // the potentially confusing situation.
327    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
345/// Use the `[credential-alias]` table to see if the provider name has been aliased.
346fn 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/// An authorization error from accessing a registry.
391#[derive(Debug)]
392pub struct AuthorizationError {
393    /// Url that was attempted
394    sid: SourceId,
395    /// The `registry.default` config value.
396    default_registry: Option<String>,
397    /// Url where the user could log in.
398    pub login_url: Option<Url>,
399    /// Specific reason indicating what failed
400    reason: AuthorizationErrorReason,
401    /// Should `cargo login` and the `_TOKEN` env var be included when displaying this error?
402    supports_cargo_token_credential_provider: bool,
403    /// Whether the cached token appears to lack an authentication scheme (no space found).
404    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        // Only display the _TOKEN environment variable suggestion if the `cargo:token` credential
415        // provider is available for the source. Otherwise setting the environment variable will
416        // have no effect.
417        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
506/// Store a token in the cache for future calls.
507pub 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
519/// List of credential providers built-in to Cargo.
520/// Keep in sync with the `match` in `credential_action`.
521static 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/// Retrieves a cached instance of `LibSecretCredential`.
531/// Must be cached to avoid repeated load/unload cycles, which are not supported by `glib`.
532#[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    // Unfortunately `get_or_try_init` is not yet stable. This workaround is not threadsafe but
539    // loading libsecret twice will only temporary increment the ref counter, which is decrement
540    // again when `drop` is called.
541    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        // If the available built-in providers are changed, update the `BUILT_IN_PROVIDERS` list.
576        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(&registry, &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
625/// Returns the token to use for the given registry.
626/// If a `login_url` is provided and a token is not available, the
627/// `login_url` will be included in the returned error.
628pub 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
648/// Returns the token to use for the given registry.
649fn 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            // Remove expired token from the cache
671            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
721/// Log out from the given registry.
722pub 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
747/// Log in to the given registry.
748pub 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}