gauthenticator 0.2.3

Simple API for authenticating with google services Project homepage: https://github.com/isaacadams/bq-rs
Documentation
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::profile::ProfileSchema;

#[derive(Clone, thiserror::Error, Debug)]
pub enum Error {
    #[error("invalid credentials because {0}")]
    InvalidCredentials(String),

    #[error("cannot load credentials from {0}")]
    FailedToLoad(String),

    #[error("cannot find profile with name `{0}`")]
    ProfileNotFound(String),

    #[error("profile has an invalid because {0}")]
    InvalidProfile(String),

    #[error("not found")]
    NotFound,

    #[error("failed to create token: {0}")]
    TokenFailed(String),
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum CredentialsSchema {
    #[serde(rename = "authorized_user")]
    AuthorizedUser(AuthorizedUserFile),
    #[serde(rename = "service_account")]
    ServiceAccount(ServiceAccountFile),
}

impl CredentialsSchema {
    pub fn project_id(&self) -> Option<&str> {
        match self {
            CredentialsSchema::AuthorizedUser(_) => None,
            CredentialsSchema::ServiceAccount(service) => service.project_id.as_deref(),
        }
    }

    pub fn email(&self) -> Option<&str> {
        match self {
            CredentialsSchema::AuthorizedUser(_) => None,
            CredentialsSchema::ServiceAccount(service) => Some(service.client_email.as_str()),
        }
    }

    pub fn kind(&self) -> &str {
        match self {
            CredentialsSchema::AuthorizedUser(_) => "authorized_user",
            CredentialsSchema::ServiceAccount(_) => "service_account",
        }
    }

    pub fn deserialize(json: &str) -> Result<Self, Error> {
        serde_json::from_str(json).map_err(|e| Error::InvalidCredentials(e.to_string()))
    }
}

pub struct GoogleCloudUserDirectory {
    root: PathBuf,
}

impl GoogleCloudUserDirectory {
    pub fn new() -> Result<Self, Error> {
        let mut config = Self::get_user_config_directory()?;
        config.push("gcloud");
        Ok(Self { root: config })
    }

    /// file @ `<user_config>/gcloud/application_default_credentials.json`
    pub fn get_application_default_credentials(&self) -> PathBuf {
        let mut file = self.root.clone();
        file.push("application_default_credentials.json");
        file
    }

    /// file @ `<user_config>/gcloud/configurations/config_default`
    pub fn get_config_default(&self) -> PathBuf {
        let mut file = self.root.clone();
        file.push("configurations");
        file.push("config_default");
        file
    }

    /// file @ `<user_config>/gcloud/legacy_credentials/<email>/adc.json`
    pub fn get_profile_adc(&self, profile: &ProfileSchema) -> PathBuf {
        let mut file = self.root.clone();
        file.push("legacy_credentials");
        file.push(&profile.account);
        file.push("adc.json");
        file
    }

    fn get_user_config_directory() -> Result<PathBuf, Error> {
        let mut path = PathBuf::new();
        if cfg!(windows) {
            let app_data = std::env::var("APPDATA").map_err(|e| {
                Error::FailedToLoad(format!("environment variable $APPDATA because {}", e))
            })?;
            path.push(app_data);
        } else {
            let home = std::env::var("HOME").map_err(|e| {
                Error::FailedToLoad(format!("environment variable $HOME because {}", e))
            })?;
            path.push(home);
            path.push(".config");
        }
        Ok(path)
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AuthorizedUserFile {
    pub client_id: String,
    pub client_secret: String,
    pub refresh_token: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ServiceAccountFile {
    #[serde(rename = "type")]
    /// 1. authorized_user
    /// 2. service_account
    pub key_type: Option<String>,
    pub project_id: Option<String>,
    /// only present when `key_type` is `service_account`
    pub private_key_id: String,
    pub private_key: String,
    pub client_email: String,
    pub client_id: Option<String>,
    pub auth_uri: Option<String>,
    pub token_uri: Option<String>,
    pub auth_provider_x509_cert_url: Option<String>,
    pub client_x509_cert_url: Option<String>,
}

#[cfg(test)]
mod test {
    use super::CredentialsSchema;

    #[test]
    fn authorized_user_deserializes() {
        let json = serde_json::json!({
          "client_id": "12345.apps.googleusercontent.com",
          "client_secret": "d-12345",
          "refresh_token": "1//12345",
          "type": "authorized_user"
        });
        let credentials: CredentialsSchema = serde_json::from_value(json).unwrap();
        let CredentialsSchema::AuthorizedUser(authorized_user) = credentials else {
            panic!("failed to deserialize into property structure");
        };
        assert_eq!(
            authorized_user.client_id,
            "12345.apps.googleusercontent.com"
        );
        assert_eq!(authorized_user.client_secret, "d-12345");
        assert_eq!(authorized_user.refresh_token, "1//12345");
    }

    #[test]
    fn service_account_deserializes() {
        let json = serde_json::json!({
            "type": "service_account",
            "project_id": "test",
            "private_key_id": "26de294916614a5ebdf7a065307ed3ea9941902b",
            "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDemmylrvp1KcOn\n9yTAVVKPpnpYznvBvcAU8Qjwr2fSKylpn7FQI54wCk5VJVom0jHpAmhxDmNiP8yv\nHaqsef+87Oc0n1yZ71/IbeRcHZc2OBB33/LCFqf272kThyJo3qspEqhuAw0e8neg\nLQb4jpm9PsqR8IjOoAtXQSu3j0zkXemMYFy93PWHjVpPEUX16NGfsWH7oxspBHOk\n9JPGJL8VJdbiAoDSDgF0y9RjJY5I52UeHNhMsAkTYs6mIG4kKXt2+T9tAyHw8aho\nwmuytQAfydTflTfTG8abRtliF3nil2taAc5VB07dP1b4dVYy/9r6M8Z0z4XM7aP+\nNdn2TKm3AgMBAAECggEAWi54nqTlXcr2M5l535uRb5Xz0f+Q/pv3ceR2iT+ekXQf\n+mUSShOr9e1u76rKu5iDVNE/a7H3DGopa7ZamzZvp2PYhSacttZV2RbAIZtxU6th\n7JajPAM+t9klGh6wj4jKEcE30B3XVnbHhPJI9TCcUyFZoscuPXt0LLy/z8Uz0v4B\nd5JARwyxDMb53VXwukQ8nNY2jP7WtUig6zwE5lWBPFMbi8GwGkeGZOruAK5sPPwY\nGBAlfofKANI7xKx9UXhRwisB4+/XI1L0Q6xJySv9P+IAhDUI6z6kxR+WkyT/YpG3\nX9gSZJc7qEaxTIuDjtep9GTaoEqiGntjaFBRKoe+VQKBgQDzM1+Ii+REQqrGlUJo\nx7KiVNAIY/zggu866VyziU6h5wjpsoW+2Npv6Dv7nWvsvFodrwe50Y3IzKtquIal\nVd8aa50E72JNImtK/o5Nx6xK0VySjHX6cyKENxHRDnBmNfbALRM+vbD9zMD0lz2q\nmns/RwRGq3/98EqxP+nHgHSr9QKBgQDqUYsFAAfvfT4I75Glc9svRv8IsaemOm07\nW1LCwPnj1MWOhsTxpNF23YmCBupZGZPSBFQobgmHVjQ3AIo6I2ioV6A+G2Xq/JCF\nmzfbvZfqtbbd+nVgF9Jr1Ic5T4thQhAvDHGUN77BpjEqZCQLAnUWJx9x7e2xvuBl\n1A6XDwH/ewKBgQDv4hVyNyIR3nxaYjFd7tQZYHTOQenVffEAd9wzTtVbxuo4sRlR\nNM7JIRXBSvaATQzKSLHjLHqgvJi8LITLIlds1QbNLl4U3UVddJbiy3f7WGTqPFfG\nkLhUF4mgXpCpkMLxrcRU14Bz5vnQiDmQRM4ajS7/kfwue00BZpxuZxst3QKBgQCI\nRI3FhaQXyc0m4zPfdYYVc4NjqfVmfXoC1/REYHey4I1XetbT9Nb/+ow6ew0UbgSC\nUZQjwwJ1m1NYXU8FyovVwsfk9ogJ5YGiwYb1msfbbnv/keVq0c/Ed9+AG9th30qM\nIf93hAfClITpMz2mzXIMRQpLdmQSR4A2l+E4RjkSOwKBgQCB78AyIdIHSkDAnCxz\nupJjhxEhtQ88uoADxRoEga7H/2OFmmPsqfytU4+TWIdal4K+nBCBWRvAX1cU47vH\nJOlSOZI0gRKe0O4bRBQc8GXJn/ubhYSxI02IgkdGrIKpOb5GG10m85ZvqsXw3bKn\nRVHMD0ObF5iORjZUqD0yRitAdg==\n-----END PRIVATE KEY-----\n",
            "client_email": "sa@test.iam.gserviceaccount.com",
            "client_id": "102851967901799660408",
            "auth_uri": "https://accounts.google.com/o/oauth2/auth",
            "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
            "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/yup-test-sa-1%40yup-test-243420.iam.gserviceaccount.com"
        });
        let credentials: CredentialsSchema = serde_json::from_value(json).unwrap();
        let CredentialsSchema::ServiceAccount(service_account) = credentials else {
            panic!("failed to deserialize into property structure");
        };
        assert_eq!(
            service_account.private_key_id,
            "26de294916614a5ebdf7a065307ed3ea9941902b"
        );
        assert_eq!(service_account.client_id.unwrap(), "102851967901799660408");
        assert_eq!(
            service_account.client_email,
            "sa@test.iam.gserviceaccount.com"
        );
    }
}