mirror of
https://github.com/OMGeeky/google-apis-rs.git
synced 2026-01-17 08:52:29 +01:00
Merge branch 'auth-refactor'
This commit is contained in:
1
.github/workflows/rust.yml
vendored
1
.github/workflows/rust.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
168
google-apis-common/src/auth.rs
Normal file
168
google-apis-common/src/auth.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 ###
|
||||
|
||||
@@ -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;
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user