|
| 1 | +use std::path::{Path, PathBuf}; |
| 2 | + |
| 3 | +use serde::{Deserialize, Serialize}; |
| 4 | +use tokio::fs; |
| 5 | + |
| 6 | +use crate::{ |
| 7 | + authentication_manager::ServiceAccount, |
| 8 | + custom_service_account::ApplicationCredentials, |
| 9 | + default_authorized_user::{ConfigDefaultCredentials, UserCredentials}, |
| 10 | + types::HyperClient, |
| 11 | + CustomServiceAccount, Error, |
| 12 | +}; |
| 13 | + |
| 14 | +// Implementation referenced from |
| 15 | +// https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google.go#L158 |
| 16 | +// Currently not implementing external account credentials |
| 17 | +// Currently not implementing impersonating service accounts (coming soon !) |
| 18 | +#[derive(Serialize, Deserialize, Debug)] |
| 19 | +#[serde(tag = "type", rename_all = "snake_case")] |
| 20 | +pub(crate) enum FlexibleCredentialSource { |
| 21 | + // This credential parses the `key.json` file created when running |
| 22 | + // `gcloud iam service-accounts keys create key.json --iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com` |
| 23 | + ServiceAccount(ApplicationCredentials), |
| 24 | + // This credential parses the `~/.config/gcloud/application_default_credentials.json` file |
| 25 | + // created when running `gcloud auth application-default login` |
| 26 | + AuthorizedUser(UserCredentials), |
| 27 | +} |
| 28 | + |
| 29 | +impl FlexibleCredentialSource { |
| 30 | + const USER_CREDENTIALS_PATH: &'static str = |
| 31 | + ".config/gcloud/application_default_credentials.json"; |
| 32 | + |
| 33 | + pub(crate) async fn from_env() -> Result<Option<Self>, Error> { |
| 34 | + let creds_path = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS"); |
| 35 | + if let Some(path) = creds_path { |
| 36 | + tracing::debug!("Reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var"); |
| 37 | + let creds = Self::from_file(PathBuf::from(path)).await?; |
| 38 | + Ok(Some(creds)) |
| 39 | + } else { |
| 40 | + Ok(None) |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + pub(crate) async fn from_default_credentials() -> Result<Self, Error> { |
| 45 | + tracing::debug!("Loading user credentials file"); |
| 46 | + let mut home = dirs_next::home_dir().ok_or(Error::NoHomeDir)?; |
| 47 | + home.push(Self::USER_CREDENTIALS_PATH); |
| 48 | + Self::from_file(home).await |
| 49 | + } |
| 50 | + |
| 51 | + pub(crate) async fn try_into_service_account( |
| 52 | + self, |
| 53 | + client: &HyperClient, |
| 54 | + ) -> Result<Box<dyn ServiceAccount>, Error> { |
| 55 | + match self { |
| 56 | + FlexibleCredentialSource::ServiceAccount(creds) => { |
| 57 | + let service_account = CustomServiceAccount::new(creds)?; |
| 58 | + Ok(Box::new(service_account)) |
| 59 | + } |
| 60 | + FlexibleCredentialSource::AuthorizedUser(creds) => { |
| 61 | + let service_account = |
| 62 | + ConfigDefaultCredentials::from_user_credentials(creds, client).await?; |
| 63 | + Ok(Box::new(service_account)) |
| 64 | + } |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + /// Read service account credentials from the given JSON file |
| 69 | + async fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, Error> { |
| 70 | + let creds_string = fs::read_to_string(&path) |
| 71 | + .await |
| 72 | + .map_err(Error::UserProfilePath)?; |
| 73 | + |
| 74 | + serde_json::from_str::<FlexibleCredentialSource>(&creds_string) |
| 75 | + .map_err(Error::CustomServiceAccountCredentials) |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +#[cfg(test)] |
| 80 | +mod tests { |
| 81 | + use crate::{flexible_credential_source::FlexibleCredentialSource, types}; |
| 82 | + |
| 83 | + #[tokio::test] |
| 84 | + async fn test_parse_application_default_credentials() { |
| 85 | + let test_creds = r#"{ |
| 86 | + "client_id": "***id***.apps.googleusercontent.com", |
| 87 | + "client_secret": "***secret***", |
| 88 | + "quota_project_id": "test_project", |
| 89 | + "refresh_token": "***refresh***", |
| 90 | + "type": "authorized_user" |
| 91 | + }"#; |
| 92 | + |
| 93 | + let cred_source: FlexibleCredentialSource = |
| 94 | + serde_json::from_str(test_creds).expect("Valid creds to parse"); |
| 95 | + |
| 96 | + assert!(matches!( |
| 97 | + cred_source, |
| 98 | + FlexibleCredentialSource::AuthorizedUser(_) |
| 99 | + )); |
| 100 | + |
| 101 | + // Can't test converting this into a service account because it requires actually getting a key |
| 102 | + } |
| 103 | + |
| 104 | + #[tokio::test] |
| 105 | + async fn test_parse_service_account_key() { |
| 106 | + // Don't worry, even though the key is a real private_key, it's not used for anything |
| 107 | + let test_creds = r#" { |
| 108 | + "type": "service_account", |
| 109 | + "project_id": "test_project", |
| 110 | + "private_key_id": "***key_id***", |
| 111 | + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5M5y3WwsRk8NX\npF9fKaZukNspot9Ecmk1PAkupcHLKVhalwPxU4sMNWXgM9H2LTWSvvyOT//rDQpn\n3SGYri/lMhzb4lI8h10E7k6zyFQUPujxkXFBkMOzhIDUgtiiht0WvIw6M8nbaPqI\nxn/aYmPsFhvJfKCthYAt2UUz+D3enI9QjCuhic8iSMnvKT8m0QkOG2eALYGUaLF1\ngRkbV4BiBUGZfXfNEBdux3Wf4kNUau32LA0XotomlvNvf1oH77v5Hc1R/KMMIk5F\nJWVBuAr4jwkN9hwtOozpJ/52wSpddxsZuj+0nP1a3f0UyvrmMnuwszardPK39BoH\nJ+5+HZM3AgMBAAECggEADrHZrXK73hkrVrjkGFjlq8Ayo4sYzAWH84Ff+SONzODq\n8cUpuuw2DDHwc2mpLy9HIO2mfGQ8mhneyX7yO3sWscjYIVpDzCmxZ8LA2+L5SOH0\n+bXglqM14/iPgE0hg0PQJw2u0q9pRM9/kXquilVkOEdIzSPmW95L3Vdv9j+sKQ2A\nOL23l4dsaG4+i1lWRBKiGsLh1kB9FRnm4BzcOxd3WGooy7L1/jo9BoYRss1YABls\nmmyZ9f7r28zjclhpOBkE3OXX0zNbp4yIu1O1Bt9X2p87EOuYqlFA5eEvDbiTPZbk\n6wKEX3BPUkeIo8OaGvsGhHCWx0lv/sDPw/UofycOgQKBgQD4BD059aXEV13Byc5D\nh8LQSejjeM/Vx+YeCFI66biaIOvUs+unyxkH+qxXTuW6AgOgcvrJo93xkyAZ9SeR\nc6Vj9g5mZ5vqSJz5Hg8h8iZBAYtf40qWq0pHcmUIm2Z9LvrG5ZFHU5EEcCtLyBVS\nAv+pLLLf3OsAkJuuqTAgygBbOwKBgQC/KcBa9sUg2u9qIpq020UOW/n4KFWhSJ8h\ngXqqmjOnPqmDc5AnYg1ZdYdqSSgdiK8lJpRL/S2UjYUQp3H+56z0eK/b1iKM51n+\n6D80nIxWeKJ+n7VKI7cBXwc/KokaXgkz0It2UEZSlhPUMImnYcOvGIZ7cMr3Q6mf\n6FwD15UQNQKBgQDyAsDz454DvvS/+noJL1qMAPL9tI+pncwQljIXRqVZ0LIO9hoH\nu4kLXjH5aAWGwhxj3o6VYA9cgSIb8jrQFbbXmexnRMbBkGWMOSavCykE2cr0oEfS\nSgbLPPcVtP4HPWZ72tsubH7fg8zbv7v+MOrkW7eX9mxiOrmPb4yFElfSrQKBgA7y\nMLvr91WuSHG/6uChFDEfN9gTLz7A8tAn03NrQwace5xveKHbpLeN3NyOg7hra2Y4\nMfgO/3VR60l2Dg+kBX3HwdgqUeE6ZWrstaRjaQWJwQqtafs196T/zQ0/QiDxoT6P\n25eQhy8F1N8OPHT9y9Lw0/LqyrOycpyyCh+yx1DRAoGAJ/6dlhyQnwSfMAe3mfRC\noiBQG6FkyoeXHHYcoQ/0cSzwp0BwBlar1Z28P7KTGcUNqV+YfK9nF47eoLaTLCmG\nG5du0Ds6m2Eg0sOBBqXHnw6R1PC878tgT/XokNxIsVlF5qRz88q7Rn0J1lzB7+Tl\n2HSAcyIUcmr0gxlhRmC2Jq4=\n-----END PRIVATE KEY-----\n", |
| 112 | + "client_email": "[email protected]", |
| 113 | + "client_id": "***id***", |
| 114 | + "auth_uri": "https://accounts.google.com/o/oauth2/auth", |
| 115 | + "token_uri": "https://oauth2.googleapis.com/token", |
| 116 | + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", |
| 117 | + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test_account%40test.iam.gserviceaccount.com", |
| 118 | + "universe_domain": "googleapis.com" |
| 119 | + }"#; |
| 120 | + |
| 121 | + let cred_source: FlexibleCredentialSource = |
| 122 | + serde_json::from_str(test_creds).expect("Valid creds to parse"); |
| 123 | + |
| 124 | + assert!(matches!( |
| 125 | + cred_source, |
| 126 | + FlexibleCredentialSource::ServiceAccount(_) |
| 127 | + )); |
| 128 | + |
| 129 | + let client = types::client(); |
| 130 | + let creds = cred_source |
| 131 | + .try_into_service_account(&client) |
| 132 | + .await |
| 133 | + .expect("Valid creds to parse"); |
| 134 | + |
| 135 | + assert_eq!( |
| 136 | + creds |
| 137 | + .project_id(&client) |
| 138 | + .await |
| 139 | + .expect("Project ID to be present"), |
| 140 | + "test_project".to_string(), |
| 141 | + "Project ID should be parsed" |
| 142 | + ); |
| 143 | + } |
| 144 | + |
| 145 | + #[tokio::test] |
| 146 | + async fn test_additional_service_account_keys() { |
| 147 | + // Using test cases from https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google_test.go#L40 |
| 148 | + // We have to use a real private key because we validate private keys on parsing as well. |
| 149 | + let k1 = r#"{ |
| 150 | + "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", |
| 151 | + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5M5y3WwsRk8NX\npF9fKaZukNspot9Ecmk1PAkupcHLKVhalwPxU4sMNWXgM9H2LTWSvvyOT//rDQpn\n3SGYri/lMhzb4lI8h10E7k6zyFQUPujxkXFBkMOzhIDUgtiiht0WvIw6M8nbaPqI\nxn/aYmPsFhvJfKCthYAt2UUz+D3enI9QjCuhic8iSMnvKT8m0QkOG2eALYGUaLF1\ngRkbV4BiBUGZfXfNEBdux3Wf4kNUau32LA0XotomlvNvf1oH77v5Hc1R/KMMIk5F\nJWVBuAr4jwkN9hwtOozpJ/52wSpddxsZuj+0nP1a3f0UyvrmMnuwszardPK39BoH\nJ+5+HZM3AgMBAAECggEADrHZrXK73hkrVrjkGFjlq8Ayo4sYzAWH84Ff+SONzODq\n8cUpuuw2DDHwc2mpLy9HIO2mfGQ8mhneyX7yO3sWscjYIVpDzCmxZ8LA2+L5SOH0\n+bXglqM14/iPgE0hg0PQJw2u0q9pRM9/kXquilVkOEdIzSPmW95L3Vdv9j+sKQ2A\nOL23l4dsaG4+i1lWRBKiGsLh1kB9FRnm4BzcOxd3WGooy7L1/jo9BoYRss1YABls\nmmyZ9f7r28zjclhpOBkE3OXX0zNbp4yIu1O1Bt9X2p87EOuYqlFA5eEvDbiTPZbk\n6wKEX3BPUkeIo8OaGvsGhHCWx0lv/sDPw/UofycOgQKBgQD4BD059aXEV13Byc5D\nh8LQSejjeM/Vx+YeCFI66biaIOvUs+unyxkH+qxXTuW6AgOgcvrJo93xkyAZ9SeR\nc6Vj9g5mZ5vqSJz5Hg8h8iZBAYtf40qWq0pHcmUIm2Z9LvrG5ZFHU5EEcCtLyBVS\nAv+pLLLf3OsAkJuuqTAgygBbOwKBgQC/KcBa9sUg2u9qIpq020UOW/n4KFWhSJ8h\ngXqqmjOnPqmDc5AnYg1ZdYdqSSgdiK8lJpRL/S2UjYUQp3H+56z0eK/b1iKM51n+\n6D80nIxWeKJ+n7VKI7cBXwc/KokaXgkz0It2UEZSlhPUMImnYcOvGIZ7cMr3Q6mf\n6FwD15UQNQKBgQDyAsDz454DvvS/+noJL1qMAPL9tI+pncwQljIXRqVZ0LIO9hoH\nu4kLXjH5aAWGwhxj3o6VYA9cgSIb8jrQFbbXmexnRMbBkGWMOSavCykE2cr0oEfS\nSgbLPPcVtP4HPWZ72tsubH7fg8zbv7v+MOrkW7eX9mxiOrmPb4yFElfSrQKBgA7y\nMLvr91WuSHG/6uChFDEfN9gTLz7A8tAn03NrQwace5xveKHbpLeN3NyOg7hra2Y4\nMfgO/3VR60l2Dg+kBX3HwdgqUeE6ZWrstaRjaQWJwQqtafs196T/zQ0/QiDxoT6P\n25eQhy8F1N8OPHT9y9Lw0/LqyrOycpyyCh+yx1DRAoGAJ/6dlhyQnwSfMAe3mfRC\noiBQG6FkyoeXHHYcoQ/0cSzwp0BwBlar1Z28P7KTGcUNqV+YfK9nF47eoLaTLCmG\nG5du0Ds6m2Eg0sOBBqXHnw6R1PC878tgT/XokNxIsVlF5qRz88q7Rn0J1lzB7+Tl\n2HSAcyIUcmr0gxlhRmC2Jq4=\n-----END PRIVATE KEY-----\n", |
| 152 | + "client_email": "[email protected]", |
| 153 | + "client_id": "gopher.apps.googleusercontent.com", |
| 154 | + "token_uri": "https://accounts.google.com/o/gophers/token", |
| 155 | + "type": "service_account", |
| 156 | + "audience": "https://testservice.googleapis.com/" |
| 157 | + }"#; |
| 158 | + |
| 159 | + let k3 = r#"{ |
| 160 | + "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", |
| 161 | + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5M5y3WwsRk8NX\npF9fKaZukNspot9Ecmk1PAkupcHLKVhalwPxU4sMNWXgM9H2LTWSvvyOT//rDQpn\n3SGYri/lMhzb4lI8h10E7k6zyFQUPujxkXFBkMOzhIDUgtiiht0WvIw6M8nbaPqI\nxn/aYmPsFhvJfKCthYAt2UUz+D3enI9QjCuhic8iSMnvKT8m0QkOG2eALYGUaLF1\ngRkbV4BiBUGZfXfNEBdux3Wf4kNUau32LA0XotomlvNvf1oH77v5Hc1R/KMMIk5F\nJWVBuAr4jwkN9hwtOozpJ/52wSpddxsZuj+0nP1a3f0UyvrmMnuwszardPK39BoH\nJ+5+HZM3AgMBAAECggEADrHZrXK73hkrVrjkGFjlq8Ayo4sYzAWH84Ff+SONzODq\n8cUpuuw2DDHwc2mpLy9HIO2mfGQ8mhneyX7yO3sWscjYIVpDzCmxZ8LA2+L5SOH0\n+bXglqM14/iPgE0hg0PQJw2u0q9pRM9/kXquilVkOEdIzSPmW95L3Vdv9j+sKQ2A\nOL23l4dsaG4+i1lWRBKiGsLh1kB9FRnm4BzcOxd3WGooy7L1/jo9BoYRss1YABls\nmmyZ9f7r28zjclhpOBkE3OXX0zNbp4yIu1O1Bt9X2p87EOuYqlFA5eEvDbiTPZbk\n6wKEX3BPUkeIo8OaGvsGhHCWx0lv/sDPw/UofycOgQKBgQD4BD059aXEV13Byc5D\nh8LQSejjeM/Vx+YeCFI66biaIOvUs+unyxkH+qxXTuW6AgOgcvrJo93xkyAZ9SeR\nc6Vj9g5mZ5vqSJz5Hg8h8iZBAYtf40qWq0pHcmUIm2Z9LvrG5ZFHU5EEcCtLyBVS\nAv+pLLLf3OsAkJuuqTAgygBbOwKBgQC/KcBa9sUg2u9qIpq020UOW/n4KFWhSJ8h\ngXqqmjOnPqmDc5AnYg1ZdYdqSSgdiK8lJpRL/S2UjYUQp3H+56z0eK/b1iKM51n+\n6D80nIxWeKJ+n7VKI7cBXwc/KokaXgkz0It2UEZSlhPUMImnYcOvGIZ7cMr3Q6mf\n6FwD15UQNQKBgQDyAsDz454DvvS/+noJL1qMAPL9tI+pncwQljIXRqVZ0LIO9hoH\nu4kLXjH5aAWGwhxj3o6VYA9cgSIb8jrQFbbXmexnRMbBkGWMOSavCykE2cr0oEfS\nSgbLPPcVtP4HPWZ72tsubH7fg8zbv7v+MOrkW7eX9mxiOrmPb4yFElfSrQKBgA7y\nMLvr91WuSHG/6uChFDEfN9gTLz7A8tAn03NrQwace5xveKHbpLeN3NyOg7hra2Y4\nMfgO/3VR60l2Dg+kBX3HwdgqUeE6ZWrstaRjaQWJwQqtafs196T/zQ0/QiDxoT6P\n25eQhy8F1N8OPHT9y9Lw0/LqyrOycpyyCh+yx1DRAoGAJ/6dlhyQnwSfMAe3mfRC\noiBQG6FkyoeXHHYcoQ/0cSzwp0BwBlar1Z28P7KTGcUNqV+YfK9nF47eoLaTLCmG\nG5du0Ds6m2Eg0sOBBqXHnw6R1PC878tgT/XokNxIsVlF5qRz88q7Rn0J1lzB7+Tl\n2HSAcyIUcmr0gxlhRmC2Jq4=\n-----END PRIVATE KEY-----\n", |
| 162 | + "client_email": "[email protected]", |
| 163 | + "client_id": "gopher.apps.googleusercontent.com", |
| 164 | + "token_uri": "https://accounts.google.com/o/gophers/token", |
| 165 | + "type": "service_account" |
| 166 | + }"#; |
| 167 | + |
| 168 | + let client = types::client(); |
| 169 | + for key in [k1, k3] { |
| 170 | + let cred_source: FlexibleCredentialSource = |
| 171 | + serde_json::from_str(key).expect("Valid creds to parse"); |
| 172 | + |
| 173 | + assert!(matches!( |
| 174 | + cred_source, |
| 175 | + FlexibleCredentialSource::ServiceAccount(_) |
| 176 | + )); |
| 177 | + |
| 178 | + let creds = cred_source |
| 179 | + .try_into_service_account(&client) |
| 180 | + .await |
| 181 | + .expect("Valid creds to parse"); |
| 182 | + |
| 183 | + assert!( |
| 184 | + matches!( |
| 185 | + creds |
| 186 | + .project_id(&client) |
| 187 | + .await |
| 188 | + .expect_err("Project ID to not be present"), |
| 189 | + crate::Error::ProjectIdNotFound, |
| 190 | + ), |
| 191 | + "Project id should not be found here", |
| 192 | + ); |
| 193 | + } |
| 194 | + } |
| 195 | +} |
0 commit comments