Skip to main content

cargo/util/credential/
token.rs

1//! Credential provider that uses plaintext tokens in Cargo's config.
2
3use anyhow::Context as _;
4use cargo_credential::{Action, CacheControl, Credential, CredentialResponse, Error, RegistryInfo};
5use url::Url;
6
7use crate::{GlobalContext, context, ops::RegistryCredentialConfig, workspace::SourceId};
8
9pub struct TokenCredential<'a> {
10    gctx: &'a GlobalContext,
11}
12
13impl<'a> TokenCredential<'a> {
14    pub fn new(gctx: &'a GlobalContext) -> Self {
15        Self { gctx }
16    }
17}
18
19impl<'a> Credential for TokenCredential<'a> {
20    fn perform(
21        &self,
22        registry: &RegistryInfo<'_>,
23        action: &Action<'_>,
24        _args: &[&str],
25    ) -> Result<CredentialResponse, Error> {
26        let index_url = Url::parse(registry.index_url).context("parsing index url")?;
27        let sid = if let Some(name) = registry.name {
28            SourceId::for_alt_registry(&index_url, name)
29        } else {
30            SourceId::for_registry(&index_url)
31        }?;
32        let previous_token = self.gctx.registry_credentials(&sid)?.and_then(|c| c.token);
33
34        match action {
35            Action::Get(_) => {
36                let token = previous_token.ok_or_else(|| Error::NotFound)?.val;
37                Ok(CredentialResponse::Get {
38                    token,
39                    cache: CacheControl::Session,
40                    operation_independent: true,
41                })
42            }
43            Action::Login(options) => {
44                // Automatically remove `cargo login` from an inputted token to
45                // allow direct pastes from `registry.host()`/me.
46                let new_token = cargo_credential::read_token(options, registry)?
47                    .map(|line| line.replace("cargo login", "").trim().to_string());
48
49                crates_io::check_token(new_token.as_ref().expose()).map_err(Box::new)?;
50                context::save_credentials(
51                    self.gctx,
52                    Some(RegistryCredentialConfig::Token(new_token)),
53                    &sid,
54                )?;
55                let _ = self.gctx.shell().status(
56                    "Login",
57                    format!("token for `{}` saved", sid.display_registry_name()),
58                );
59                Ok(CredentialResponse::Login)
60            }
61            Action::Logout => {
62                if previous_token.is_none() {
63                    return Err(Error::NotFound);
64                }
65                let reg_name = sid.display_registry_name();
66                context::save_credentials(self.gctx, None, &sid)?;
67                let _ = self.gctx.shell().status(
68                    "Logout",
69                    format!("token for `{reg_name}` has been removed from local storage"),
70                );
71                let location = if sid.is_crates_io() {
72                    "<https://crates.io/me>".to_string()
73                } else {
74                    // The URL for the source requires network access to load the config.
75                    // That could be a fairly heavy operation to perform just to provide a
76                    // help message, so for now this just provides some generic text.
77                    // Perhaps in the future this could have an API to fetch the config if
78                    // it is cached, but avoid network access otherwise?
79                    format!("the `{reg_name}` website")
80                };
81                eprintln!(
82                    "note: This does not revoke the token on the registry server.\n    \
83                    If you need to revoke the token, visit {location} and follow the instructions there."
84                );
85                Ok(CredentialResponse::Logout)
86            }
87            _ => Err(Error::OperationNotSupported),
88        }
89    }
90}