    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95

    //! Credential provider that uses plaintext tokens in Cargo's config.

    use anyhow::Context as _;
    use cargo_credential::{Action, CacheControl, Credential, CredentialResponse, Error, RegistryInfo};
    use url::Url;

    use crate::{
        core::SourceId,
        ops::RegistryCredentialConfig,
        util::{auth::registry_credential_config_raw, context},
        GlobalContext,
    };

    pub struct TokenCredential<'a> {
        gctx: &'a GlobalContext,
    }

    impl<'a> TokenCredential<'a> {
        pub fn new(gctx: &'a GlobalContext) -> Self {
            Self { gctx }
        }
    }

    impl<'a> Credential for TokenCredential<'a> {
        fn perform(
            &self,
            registry: &RegistryInfo<'_>,
            action: &Action<'_>,
            _args: &[&str],
        ) -> Result<CredentialResponse, Error> {
            let index_url = Url::parse(registry.index_url).context("parsing index url")?;
            let sid = if let Some(name) = registry.name {
                SourceId::for_alt_registry(&index_url, name)
            } else {
                SourceId::for_registry(&index_url)
            }?;
            let previous_token = registry_credential_config_raw(self.gctx, &sid)?.and_then(|c| c.token);

            match action {
                Action::Get(_) => {
                    let token = previous_token.ok_or_else(|| Error::NotFound)?.val;
                    Ok(CredentialResponse::Get {
                        token,
                        cache: CacheControl::Session,
                        operation_independent: true,
                    })
                }
                Action::Login(options) => {
                    // Automatically remove `cargo login` from an inputted token to
                    // allow direct pastes from `registry.host()`/me.
                    let new_token = cargo_credential::read_token(options, registry)?
                        .map(|line| line.replace("cargo login", "").trim().to_string());

                    crates_io::check_token(new_token.as_ref().expose()).map_err(Box::new)?;
                    context::save_credentials(
                        self.gctx,
                        Some(RegistryCredentialConfig::Token(new_token)),
                        &sid,
                    )?;
                    let _ = self.gctx.shell().status(
                        "Login",
                        format!("token for `{}` saved", sid.display_registry_name()),
                    );
                    Ok(CredentialResponse::Login)
                }
                Action::Logout => {
                    if previous_token.is_none() {
                        return Err(Error::NotFound);
                    }
                    let reg_name = sid.display_registry_name();
                    context::save_credentials(self.gctx, None, &sid)?;
                    let _ = self.gctx.shell().status(
                        "Logout",
                        format!("token for `{reg_name}` has been removed from local storage"),
                    );
                    let location = if sid.is_crates_io() {
                        "<https://crates.io/me>".to_string()
                    } else {
                        // The URL for the source requires network access to load the config.
                        // That could be a fairly heavy operation to perform just to provide a
                        // help message, so for now this just provides some generic text.
                        // Perhaps in the future this could have an API to fetch the config if
                        // it is cached, but avoid network access otherwise?
                        format!("the `{reg_name}` website")
                    };
                    eprintln!(
                        "note: This does not revoke the token on the registry server.\n    \
                        If you need to revoke the token, visit {location} and follow the instructions there."
                    );
                    Ok(CredentialResponse::Logout)
                }
                _ => Err(Error::OperationNotSupported),
            }
        }
    }