diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index edd4805f46..16f6f28d45 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,6 +19,7 @@ jobs: source ~/.profile make test-gen make gen-all-cli cargo-api ARGS=test + make cargo-api ARGS='check --no-default-features' make cargo-api ARGS=doc make docs-all cargo test diff --git a/google-apis-common/Cargo.toml b/google-apis-common/Cargo.toml index e66cf39c19..aa601650a3 100644 --- a/google-apis-common/Cargo.toml +++ b/google-apis-common/Cargo.toml @@ -24,11 +24,9 @@ serde_json = "^ 1.0" base64 = "0.13.0" chrono = { version = "0.4.22", features = ["serde"] } -## TODO: Make yup-oauth2 optional -## yup-oauth2 = { version = "^ 7.0", optional = true } -yup-oauth2 = "^ 7.0" +yup-oauth2 = { version = "^ 7.0", optional = true } itertools = "^ 0.10" -hyper = "^ 0.14" +hyper = { version = "^ 0.14", features = ["client", "http2"] } http = "^0.2" -tokio = "^1.0" +tokio = { version = "^1.0", features = ["time"] } tower-service = "^0.3.1" diff --git a/google-apis-common/src/auth.rs b/google-apis-common/src/auth.rs new file mode 100644 index 0000000000..8bbeb33d54 --- /dev/null +++ b/google-apis-common/src/auth.rs @@ -0,0 +1,168 @@ +//! Authentication for Google API endpoints +//! +//! Allows users to provide custom authentication implementations to suite their needs. +//! +//! By default, [`GetToken`] is implemented for: +//! - [`Authenticator`] : An authenticator which supports a variety of authentication methods +//! - [`String`] : Plain oauth2 token in String format +//! - [`NoToken`] : No token, used for APIs which do not require a token +//! +//! # Usage +//! [`GetToken`] instances are designed to be used with the Hub constructor provided by the +//! generated APIs. +//! +//! If you intend to use the API libraries on client devices, +//! [`Authenticator`] supports a variety of client-side authentication methods, +//! and should be used to provide authentication. +//! +//! If you intend to use the API libraries server-side, with server-side client authentication, +//! use the [`oauth2`] crate and convert the resulting [`AccessToken`] to [`String`]. +//! +//! If you intend to use APIs which do not require authentication, use [`NoToken`]. +//! +//! If you have custom authentication requirements, you can implement [`GetToken`] manually. +//! +//! # Example +//! ```rust +//! use core::future::Future; +//! use core::pin::Pin; +//! +//! use google_apis_common::{GetToken, oauth2}; +//! +//! use http::Uri; +//! use hyper::client::connect::Connection; +//! use tokio::io::{AsyncRead, AsyncWrite}; +//! use tower_service::Service; +//! use oauth2::authenticator::Authenticator; +//! +//! #[derive(Clone)] +//! struct AuthenticatorWithRetry { +//! auth: Authenticator, +//! retries: usize, +//! } +//! +//! impl GetToken for AuthenticatorWithRetry +//! where +//! S: Service + Clone + Send + Sync + 'static, +//! S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static, +//! S::Future: Send + Unpin + 'static, +//! S::Error: Into>, +//! { +//! fn get_token<'a>( +//! &'a self, +//! scopes: &'a [&str], +//! ) -> Pin, Box>> + Send + 'a>> { +//! Box::pin(async move { +//! let mut auth_token = Ok(None); +//! for _ in 0..=self.retries { +//! match self.auth.token(scopes).await { +//! Ok(token) => { +//! auth_token = Ok(Some(token.as_str().to_owned())); +//! break; +//! }, +//! Err(e) => auth_token = Err(e.into()), +//! } +//! } +//! auth_token +//! }) +//! } +//! } +//! ``` +//! [`oauth2`]: https://docs.rs/oauth2/latest/oauth2/ +//! [`AccessToken`]: https://docs.rs/oauth2/latest/oauth2/struct.AccessToken.html +//! [`Authenticator`]: yup_oauth2::authenticator::Authenticator +use std::future::Future; +use std::pin::Pin; + +type GetTokenOutput<'a> = Pin< + Box< + dyn Future, Box>> + + Send + + 'a, + >, +>; + +pub trait GetToken: GetTokenClone + Send + Sync { + /// Called whenever an API call requires authentication via an oauth2 token. + /// Returns `Ok(None)` if a token is not necessary - otherwise, returns an error + /// indicating the reason why a token could not be produced. + fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> GetTokenOutput<'a>; +} + +pub trait GetTokenClone { + fn clone_box(&self) -> Box; +} + +impl GetTokenClone for T +where + T: 'static + GetToken + Clone, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_box() + } +} + +impl GetToken for String { + fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> GetTokenOutput<'a> { + Box::pin(async move { Ok(Some(self.clone())) }) + } +} + +/// In the event that the API endpoint does not require an oauth2 token, `NoToken` should be provided to the hub to avoid specifying an +/// authenticator. +#[derive(Default, Clone)] +pub struct NoToken; + +impl GetToken for NoToken { + fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> GetTokenOutput<'a> { + Box::pin(async move { Ok(None) }) + } +} + +#[cfg(feature = "yup-oauth2")] +mod yup_oauth2_impl { + use super::{GetToken, GetTokenOutput}; + + use http::Uri; + use hyper::client::connect::Connection; + use tokio::io::{AsyncRead, AsyncWrite}; + use tower_service::Service; + use yup_oauth2::authenticator::Authenticator; + + impl GetToken for Authenticator + where + S: Service + Clone + Send + Sync + 'static, + S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static, + S::Future: Send + Unpin + 'static, + S::Error: Into>, + { + fn get_token<'a>(&'a self, scopes: &'a [&str]) -> GetTokenOutput<'a> { + Box::pin(async move { + self.token(scopes) + .await + .map(|t| Some(t.as_str().to_owned())) + .map_err(|e| e.into()) + }) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn dyn_get_token_is_send() { + fn with_send(_x: impl Send) {} + + let mut gt = String::new(); + let dgt: &mut dyn GetToken = &mut gt; + with_send(dgt); + } +} diff --git a/google-apis-common/src/lib.rs b/google-apis-common/src/lib.rs index 022b9a22c0..e66f011465 100644 --- a/google-apis-common/src/lib.rs +++ b/google-apis-common/src/lib.rs @@ -1,12 +1,11 @@ +pub mod auth; pub mod field_mask; pub mod serde; use std::error; use std::error::Error as StdError; use std::fmt::{self, Display}; -use std::future::Future; use std::io::{self, Cursor, Read, Seek, SeekFrom, Write}; -use std::pin::Pin; use std::str::FromStr; use std::time::Duration; @@ -26,9 +25,11 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time::sleep; use tower_service; +pub use auth::{GetToken, NoToken}; pub use chrono; pub use field_mask::FieldMask; pub use serde_with; +#[cfg(feature = "yup-oauth2")] pub use yup_oauth2 as oauth2; const LINE_ENDING: &str = "\r\n"; @@ -113,15 +114,16 @@ pub trait Delegate: Send { None } - // TODO: Remove oauth2::Error /// Called whenever the Authenticator didn't yield a token. The delegate /// may attempt to provide one, or just take it as a general information about the /// impending failure. /// The given Error provides information about why the token couldn't be acquired in the /// first place - fn token(&mut self, err: &oauth2::Error) -> Option { - let _ = err; - None + fn token( + &mut self, + e: Box, + ) -> std::result::Result, Box> { + Err(e) } /// Called during resumable uploads to provide a URL for the impending upload. @@ -236,9 +238,8 @@ pub enum Error { /// Neither through the authenticator, nor through the Delegate. MissingAPIKey, - // TODO: Remove oauth2::Error /// We required a Token, but didn't get one from the Authenticator - MissingToken(oauth2::Error), + MissingToken(Box), /// The delgate instructed to cancel the operation Cancelled, @@ -259,41 +260,34 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Error::Io(ref err) => err.fmt(f), - Error::HttpError(ref err) => err.fmt(f), - Error::UploadSizeLimitExceeded(ref resource_size, ref max_size) => writeln!( + match self { + Error::Io(err) => err.fmt(f), + Error::HttpError(err) => err.fmt(f), + Error::UploadSizeLimitExceeded(resource_size, max_size) => writeln!( f, "The media size {} exceeds the maximum allowed upload size of {}", resource_size, max_size ), Error::MissingAPIKey => { - (writeln!( + writeln!( f, "The application's API key was not found in the configuration" - )) - .ok(); + )?; writeln!( f, "It is used as there are no Scopes defined for this method." ) } - Error::BadRequest(ref message) => { - writeln!(f, "Bad Request: {}", message)?; - Ok(()) - } - // TODO: Remove oauth2::Error - Error::MissingToken(ref err) => { - writeln!(f, "Token retrieval failed with error: {}", err) - } + Error::BadRequest(message) => writeln!(f, "Bad Request: {}", message), + Error::MissingToken(e) => writeln!(f, "Token retrieval failed: {}", e), Error::Cancelled => writeln!(f, "Operation cancelled by delegate"), Error::FieldClash(field) => writeln!( f, "The custom parameter '{}' is already provided natively by the CallBuilder.", field ), - Error::JsonDecodeError(ref json_str, ref err) => writeln!(f, "{}: {}", err, json_str), - Error::Failure(ref response) => { + Error::JsonDecodeError(json_str, err) => writeln!(f, "{}: {}", err, json_str), + Error::Failure(response) => { writeln!(f, "Http status indicates failure: {:?}", response) } } @@ -585,14 +579,12 @@ impl RangeResponseHeader { pub struct ResumableUploadHelper<'a, A: 'a, S> where S: tower_service::Service + Clone + Send + Sync + 'static, - S::Response: hyper::client::connect::Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static, + S::Response: + hyper::client::connect::Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static, S::Future: Send + Unpin + 'static, S::Error: Into>, { - pub client: &'a hyper::client::Client< - S, - hyper::body::Body, - >, + pub client: &'a hyper::client::Client, pub delegate: &'a mut dyn Delegate, pub start_at: Option, pub auth: &'a A, @@ -606,7 +598,8 @@ where impl<'a, A, S> ResumableUploadHelper<'a, A, S> where S: tower_service::Service + Clone + Send + Sync + 'static, - S::Response: hyper::client::connect::Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static, + S::Response: + hyper::client::connect::Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static, S::Future: Send + Unpin + 'static, S::Error: Into>, { @@ -777,86 +770,14 @@ pub async fn get_body_as_string(res_body: &mut hyper::Body) -> String { res_body_string.to_string() } -// TODO: Simplify this to Option -type TokenResult = std::result::Result, oauth2::Error>; - -pub trait GetToken: GetTokenClone + Send + Sync { - /// Called whenever there is the need for an oauth token after - /// the official authenticator implementation didn't provide one, for some reason. - /// If this method returns None as well, the underlying operation will fail - fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> Pin + Send + 'a>> { - Box::pin(async move { Ok(None) }) - } -} - -pub trait GetTokenClone { - fn clone_box(&self) -> Box; -} - -impl GetTokenClone for T -where - T: 'static + GetToken + Clone, -{ - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.clone_box() - } -} - -impl GetToken for String { - fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> Pin + Send + 'a>> { - Box::pin(async move { Ok(Some(self.clone())) }) - } -} - -/// In the event that the API endpoint does not require an oauth2 token, `NoToken` should be provided to the hub to avoid specifying an -/// authenticator. -#[derive(Default, Clone)] -pub struct NoToken; - -impl GetToken for NoToken {} - -// TODO: Make this optional -// #[cfg(feature = "yup-oauth2")] -mod yup_oauth2_impl { - use core::future::Future; - use core::pin::Pin; - - use super::{GetToken, TokenResult}; - - use tower_service::Service; - use yup_oauth2::authenticator::Authenticator; - use tokio::io::{AsyncRead, AsyncWrite}; - use http::Uri; - use hyper::client::connect::Connection; - - - impl GetToken for Authenticator where - S: Service + Clone + Send + Sync + 'static, - S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static, - S::Future: Send + Unpin + 'static, - S::Error: Into> { - fn get_token<'a>(&'a self, scopes: &'a [&str]) -> Pin + Send + 'a>> { - Box::pin(async move { - self.token(scopes).await.map(|t| Some(t.as_str().to_owned())) - }) - } - } -} - #[cfg(test)] mod test_api { use super::*; use std::default::Default; use std::str::FromStr; + use ::serde::{Deserialize, Serialize}; use serde_json as json; - use ::serde::{Serialize, Deserialize}; #[test] fn serde() { @@ -915,13 +836,4 @@ mod test_api { let dlg: &mut dyn Delegate = &mut dd; with_send(dlg); } - - #[test] - fn dyn_get_token_is_send() { - fn with_send(_x: impl Send) {} - - let mut gt = String::new(); - let dgt: &mut dyn GetToken = &mut gt; - with_send(dgt); - } } diff --git a/src/generator/templates/Cargo.toml.mako b/src/generator/templates/Cargo.toml.mako index d696b97f8a..3001397c39 100644 --- a/src/generator/templates/Cargo.toml.mako +++ b/src/generator/templates/Cargo.toml.mako @@ -58,6 +58,8 @@ path = "../${api_name}" version = "${util.crate_version()}" % endif -## TODO: Make yup-oauth2 optional -# [features] -# default = ["yup-oauth2"] +% if not cargo.get("is_executable", False): +[features] +yup-oauth2 = ["google-apis-common/yup-oauth2"] +default = ["yup-oauth2"] +% endif \ No newline at end of file diff --git a/src/generator/templates/api/api.rs.mako b/src/generator/templates/api/api.rs.mako index 9f58e0cdee..7029c36241 100644 --- a/src/generator/templates/api/api.rs.mako +++ b/src/generator/templates/api/api.rs.mako @@ -31,7 +31,7 @@ use tokio::time::sleep; use tower_service; use serde::{Serialize, Deserialize}; -use crate::{client, client::GetToken, client::oauth2, client::serde_with}; +use crate::{client, client::GetToken, client::serde_with}; // ############## // UTILITIES ### diff --git a/src/generator/templates/api/lib.rs.mako b/src/generator/templates/api/lib.rs.mako index f1042232ef..3aced87c47 100644 --- a/src/generator/templates/api/lib.rs.mako +++ b/src/generator/templates/api/lib.rs.mako @@ -49,5 +49,8 @@ pub mod api; // Re-export the hub type and some basic client structs pub use api::${hub_type}; +pub use client::{Result, Error, Delegate, FieldMask}; + // Re-export the yup_oauth2 crate, that is required to call some methods of the hub and the client -pub use client::{Result, Error, Delegate, oauth2, FieldMask}; +#[cfg(feature = "yup-oauth2")] +pub use client::oauth2; \ No newline at end of file diff --git a/src/generator/templates/api/lib/mbuild.mako b/src/generator/templates/api/lib/mbuild.mako index dd19152eb0..7e56b03828 100644 --- a/src/generator/templates/api/lib/mbuild.mako +++ b/src/generator/templates/api/lib/mbuild.mako @@ -711,24 +711,13 @@ else { loop { % if default_scope: let token = match ${auth_call}.get_token(&self.${api.properties.scopes}.iter().map(String::as_str).collect::>()[..]).await { - // TODO: remove Ok / Err branches - Ok(Some(token)) => token.clone(), - Ok(None) => { - let err = oauth2::Error::OtherError(anyhow::Error::msg("unknown error occurred while generating oauth2 token")); - match dlg.token(&err) { - Some(token) => token, - None => { + Ok(token) => token, + Err(e) => { + match dlg.token(e) { + Ok(token) => token, + Err(e) => { ${delegate_finish}(false); - return Err(client::Error::MissingToken(err)) - } - } - } - Err(err) => { - match dlg.token(&err) { - Some(token) => token, - None => { - ${delegate_finish}(false); - return Err(client::Error::MissingToken(err)) + return Err(client::Error::MissingToken(e)); } } } @@ -767,11 +756,13 @@ else { let client = &self.hub.client; dlg.pre_request(); let mut req_builder = hyper::Request::builder().method(${method_name_to_variant(m.httpMethod)}).uri(url.clone().into_string()) - .header(USER_AGENT, self.hub._user_agent.clone())\ - % if default_scope: - .header(AUTHORIZATION, format!("Bearer {}", token.as_str()))\ - % endif -; + .header(USER_AGENT, self.hub._user_agent.clone()); + + % if default_scope: + if let Some(token) = token.as_ref() { + req_builder = req_builder.header(AUTHORIZATION, format!("Bearer {}", token)); + } + % endif % if resumable_media_param: upload_url_from_server = true; @@ -865,7 +856,8 @@ else { start_at: if upload_url_from_server { Some(0) } else { None }, auth: &${auth_call}, user_agent: &self.hub._user_agent, - auth_header: format!("Bearer {}", token.as_str()), + // TODO: Check this assumption + auth_header: format!("Bearer {}", token.ok_or_else(|| client::Error::MissingToken("resumable upload requires token".into()))?.as_str()), url: url_str, reader: &mut reader, media_type: reader_mime_type.clone(),