Merge branch 'auth-refactor'

This commit is contained in:
Sebastian Thiel
2022-10-18 15:50:11 +08:00
8 changed files with 222 additions and 146 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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<S> {
//! auth: Authenticator<S>,
//! retries: usize,
//! }
//!
//! impl<S> GetToken for AuthenticatorWithRetry<S>
//! where
//! S: Service<Uri> + Clone + Send + Sync + 'static,
//! S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
//! S::Future: Send + Unpin + 'static,
//! S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
//! {
//! fn get_token<'a>(
//! &'a self,
//! scopes: &'a [&str],
//! ) -> Pin<Box<dyn Future<Output = Result<Option<String>, Box<dyn std::error::Error + Send + Sync>>> + 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<Output = Result<Option<String>, Box<dyn std::error::Error + Send + Sync>>>
+ 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<dyn GetToken>;
}
impl<T> GetTokenClone for T
where
T: 'static + GetToken + Clone,
{
fn clone_box(&self) -> Box<dyn GetToken> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn GetToken> {
fn clone(&self) -> Box<dyn GetToken> {
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<S> GetToken for Authenticator<S>
where
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
{
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);
}
}

View File

@@ -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<String> {
let _ = err;
None
fn token(
&mut self,
e: Box<dyn StdError + Send + Sync>,
) -> std::result::Result<Option<String>, Box<dyn StdError + Send + Sync>> {
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<dyn StdError + Send + Sync>),
/// 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<Uri> + 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<Box<dyn StdError + Send + Sync>>,
{
pub client: &'a hyper::client::Client<
S,
hyper::body::Body,
>,
pub client: &'a hyper::client::Client<S, hyper::body::Body>,
pub delegate: &'a mut dyn Delegate,
pub start_at: Option<u64>,
pub auth: &'a A,
@@ -606,7 +598,8 @@ where
impl<'a, A, S> ResumableUploadHelper<'a, A, S>
where
S: tower_service::Service<Uri> + 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<Box<dyn StdError + Send + Sync>>,
{
@@ -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<String>
type TokenResult = std::result::Result<Option<String>, 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<Box<dyn Future<Output=TokenResult> + Send + 'a>> {
Box::pin(async move { Ok(None) })
}
}
pub trait GetTokenClone {
fn clone_box(&self) -> Box<dyn GetToken>;
}
impl<T> GetTokenClone for T
where
T: 'static + GetToken + Clone,
{
fn clone_box(&self) -> Box<dyn GetToken> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn GetToken> {
fn clone(&self) -> Box<dyn GetToken> {
self.clone_box()
}
}
impl GetToken for String {
fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> Pin<Box<dyn Future<Output=TokenResult> + 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<S> GetToken for Authenticator<S> where
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn std::error::Error + Send + Sync>> {
fn get_token<'a>(&'a self, scopes: &'a [&str]) -> Pin<Box<dyn Future<Output=TokenResult> + 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);
}
}

View File

@@ -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

View File

@@ -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 ###

View File

@@ -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;

View File

@@ -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::<Vec<_>>()[..]).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(),