diff --git a/Cargo.toml b/Cargo.toml index b817a33..d96fc90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" tokio = { version = "1.1", features = ["fs"] } url = "2" +which = "4.2" async-trait = "0.1" thiserror = "1.0" dirs-next = "2.0" diff --git a/src/error.rs b/src/error.rs index cb4ff9c..2add56d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,16 +6,17 @@ pub enum Error { /// /// Application can authenticate against GCP using: /// - /// - Defaul service account - available inside GCP platform using GCP Instance Metadata server + /// - Default service account - available inside GCP platform using GCP Instance Metadata server /// - Service account file - provided using `GOOGLE_APPLICATION_CREDENTIALS` with path + /// - GCloud authorized user - retrieved using `gcloud auth` command /// /// All authentication methods have been tested and none succeeded. - /// Service account file can be donwloaded from GCP in json format. + /// Service account file can be downloaded from GCP in json format. #[error("No available authentication method was discovered")] - NoAuthMethod(Box, Box, Box), + NoAuthMethod(Box, Box, Box, Box), - /// Error in underlaying RustTLS library. - /// Might signal problem with establishin secure connection using trusted certificates + /// Error in underlying RustTLS library. + /// Might signal problem with establishing secure connection using trusted certificates #[error("TLS error")] TLSError(rustls::TLSError), @@ -23,14 +24,14 @@ pub enum Error { #[error("Could not establish connection with OAuth server")] OAuthConnectionError(hyper::Error), - /// Error when parsin response from OAuth server - #[error("Could not parse OAuth server reponse")] + /// Error when parsing response from OAuth server + #[error("Could not parse OAuth server response")] OAuthParsingError(serde_json::error::Error), /// Variable `GOOGLE_APPLICATION_CREDENTIALS` could not be found in the current environment /// /// GOOGLE_APPLICATION_CREDENTIALS is used for providing path to json file with applications credentials. - /// File can be donwoloaded in GCP Console when creating service account. + /// File can be downloaded in GCP Console when creating service account. #[error("Path to custom auth credentials was not provided in `GOOGLE_APPLICATION_CREDENTIALS` env variable")] ApplicationProfileMissing, @@ -43,7 +44,7 @@ pub enum Error { /// Wrong format of custom application profile /// /// Application profile is downloaded from GCP console and is stored in filesystem on the server. - /// Full path is passed to library by seeting `GOOGLE_APPLICATION_CREDENTIALS` variable with path as a value. + /// Full path is passed to library by setting `GOOGLE_APPLICATION_CREDENTIALS` variable with path as a value. #[error("Application profile provided in `GOOGLE_APPLICATION_CREDENTIALS` was not parsable")] ApplicationProfileFormat(serde_json::error::Error), @@ -63,7 +64,7 @@ pub enum Error { ConnectionError(hyper::Error), /// Could not parse response from server - #[error("Could not parse server reponse")] + #[error("Could not parse server response")] ParsingError(serde_json::error::Error), /// Could not connect to server @@ -74,7 +75,7 @@ pub enum Error { #[error("Couldn't choose signing scheme")] SignerSchemeError, - /// Could not initalize signer + /// Could not initialize signer #[error("Couldn't initialize signer")] SignerInit, @@ -94,6 +95,18 @@ pub enum Error { #[error("Project ID is invalid UTF-8")] ProjectIdNonUtf8, + /// GCloud executable not found + #[error("GCloud executable not found in $PATH")] + GCloudNotFound, + + /// GCloud returned an error status + #[error("GCloud returned a non OK status")] + GCloudError, + + /// GCloud output couldn't be parsed + #[error("Failed to parse output of GCloud")] + GCloudParseError, + /// Represents all other cases of `std::io::Error`. #[error(transparent)] IOError(#[from] std::io::Error), diff --git a/src/gcloud_authorized_user.rs b/src/gcloud_authorized_user.rs new file mode 100644 index 0000000..f82dfd6 --- /dev/null +++ b/src/gcloud_authorized_user.rs @@ -0,0 +1,51 @@ +use crate::authentication_manager::ServiceAccount; +use crate::error::Error; +use crate::error::Error::{ + GCloudError, GCloudNotFound, GCloudParseError, NoProjectId, ParsingError, +}; +use crate::types::HyperClient; +use crate::Token; +use async_trait::async_trait; +use serde_json::json; +use std::path::PathBuf; +use std::process::Command; +use which::which; + +#[derive(Debug)] +pub(crate) struct GCloudAuthorizedUser { + gcloud: PathBuf, +} + +impl GCloudAuthorizedUser { + pub(crate) async fn new() -> Result { + which("gcloud") + .map_err(|_| GCloudNotFound) + .map(|path| Self { gcloud: path }) + } +} + +#[async_trait] +impl ServiceAccount for GCloudAuthorizedUser { + async fn project_id(&self, _: &HyperClient) -> Result { + Err(NoProjectId) + } + + fn get_token(&self, _scopes: &[&str]) -> Option { + None + } + + async fn refresh_token(&self, _client: &HyperClient, _scopes: &[&str]) -> Result { + let mut command = Command::new(&self.gcloud); + command.args(&["auth", "print-access-token", "--quiet"]); + + match command.output() { + Ok(output) if output.status.success() => String::from_utf8(output.stdout) + .map_err(|_| GCloudParseError) + .and_then(|access_token| { + serde_json::from_value::(json!({ "access_token": access_token.trim() })) + .map_err(ParsingError) + }), + _ => Err(GCloudError), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 910f025..2c14c62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ //! The downloaded JSON file should be provided without any further modification. //! 2. Invoking the library inside GCP environment fetches the default service account for the service and //! the application is authenticated using that particular account -//! 3. Application default credentials. Local user authetincation for development purposes created using `gcloud auth` application. +//! 3. Application default credentials. Local user authentication for development purposes created using `gcloud auth` application. //! 4. If none of the above can be used an error occurs //! //! The tokens are single-use and as such they shouldn't be cached and for each use a new token should be requested. @@ -33,7 +33,7 @@ //! `GOOGLE_APPLICATION_CREDENTIALS` environment variable. //! //! ```async -//! // GOOGLE_APPLICATION_CREDENTIALS environtment variable is set-up +//! // GOOGLE_APPLICATION_CREDENTIALS environment variable is set-up //! let authentication_manager = gcp_auth::init().await?; //! let token = authentication_manager.get_token().await?; //! ``` @@ -46,7 +46,7 @@ //! ``` //! //! # Local user authentication -//! This authentication method allows developers to authenticate again GCP services when developign locally. +//! This authentication method allows developers to authenticate again GCP services when developing locally. //! The method is intended only for development. Credentials can be set-up using `gcloud auth` utility. //! Credentials are read from file `~/.config/gcloud/application_default_credentials.json`. //! @@ -66,6 +66,7 @@ mod custom_service_account; mod default_authorized_user; mod default_service_account; mod error; +mod gcloud_authorized_user; mod jwt; mod types; mod util; @@ -114,6 +115,13 @@ async fn get_authentication_manager( service_account: Box::new(service_account), }); } + let gcloud = gcloud_authorized_user::GCloudAuthorizedUser::new().await; + if let Ok(service_account) = gcloud { + return Ok(AuthenticationManager { + client: client.clone(), + service_account: Box::new(service_account), + }); + } let default = default_service_account::DefaultServiceAccount::new(&client).await; if let Ok(service_account) = default { return Ok(AuthenticationManager { @@ -130,6 +138,7 @@ async fn get_authentication_manager( } Err(Error::NoAuthMethod( Box::new(custom.unwrap_err()), + Box::new(gcloud.unwrap_err()), Box::new(default.unwrap_err()), Box::new(user.unwrap_err()), )) diff --git a/src/types.rs b/src/types.rs index 162a7e6..7c9b03c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; pub struct Token { access_token: String, #[serde( + default, deserialize_with = "deserialize_time", rename(deserialize = "expires_in") )]