use futures::future::BoxFuture;
use std::{
future::Future,
sync::Arc,
task::{Context, Poll},
};
use tonic::client::GrpcService;
pub trait TokenSource {
type Token: AsRef<str>;
type Fut: Future<Output = Result<Self::Token, Self::Error>> + Send + 'static;
type Error: std::error::Error + Send + Sync + 'static;
fn get_token(&self) -> Self::Fut;
}
impl<F, Token, TokenFut, TokenErr> TokenSource for F
where
F: Fn() -> TokenFut,
TokenFut: Future<Output = Result<Token, TokenErr>> + Send + 'static,
TokenErr: std::error::Error + Send + Sync + 'static,
Token: AsRef<str>,
{
type Error = TokenErr;
type Fut = TokenFut;
type Token = Token;
fn get_token(&self) -> TokenFut {
(self)()
}
}
#[derive(Clone)]
pub struct OAuthTokenSource<C = crate::builder::DefaultConnector> {
oauth: crate::Auth<C>,
scopes: Arc<[String]>,
}
impl<C> std::fmt::Debug for OAuthTokenSource<C> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("OAuthTokenSource")
.field("scopes", &self.scopes)
.field("oauth", &"...")
.finish()
}
}
impl<C> TokenSource for OAuthTokenSource<C>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
type Error = yup_oauth2::Error;
type Fut = BoxFuture<'static, Result<Self::Token, Self::Error>>;
type Token = yup_oauth2::AccessToken;
fn get_token(&self) -> Self::Fut {
let oauth = self.oauth.clone();
let scopes = self.scopes.clone();
Box::pin(async move { oauth.token(&scopes).await })
}
}
pub(crate) fn oauth_grpc<C>(
channel: tonic::transport::Channel,
oauth: Option<crate::Auth<C>>,
scopes: Vec<String>,
) -> AuthGrpcService<tonic::transport::Channel, OAuthTokenSource<C>>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
let token_fn = oauth.map(move |oauth| OAuthTokenSource {
oauth,
scopes: Arc::from(scopes),
});
AuthGrpcService {
inner: channel,
token_fn,
}
}
#[derive(Debug, Clone)]
pub struct AuthGrpcService<Service, TokenFn> {
token_fn: Option<TokenFn>,
inner: Service,
}
impl<Service, TokenFn> AuthGrpcService<Service, TokenFn> {
pub fn new<ReqBody>(service: Service, token_fn: Option<TokenFn>) -> Self
where
Service: GrpcService<ReqBody> + Clone + 'static,
Service::Error: std::error::Error + Send + Sync + 'static,
TokenFn: TokenSource,
{
Self {
inner: service,
token_fn,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AuthGrpcError<ServiceErr, TokenErr>
where
ServiceErr: std::fmt::Debug + std::error::Error + 'static,
TokenErr: std::fmt::Debug + std::error::Error + 'static,
{
#[error("Failed to get authorization token")]
Auth(#[source] TokenErr),
#[error("Auth token formed invalid HTTP header: {1}")]
InvalidToken(#[source] http::header::InvalidHeaderValue, String),
#[error(transparent)]
Grpc(ServiceErr),
}
impl<Service, TokenFn, ReqBody> GrpcService<ReqBody> for AuthGrpcService<Service, TokenFn>
where
Service: GrpcService<ReqBody> + Clone + Send + 'static,
Service::Error: std::error::Error + Send + Sync + 'static,
Service::Future: Send,
TokenFn: TokenSource,
ReqBody: Send + 'static,
{
type Error = AuthGrpcError<Service::Error, TokenFn::Error>;
type Future = BoxFuture<'static, Result<http::Response<Self::ResponseBody>, Self::Error>>;
type ResponseBody = Service::ResponseBody;
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx).map_err(AuthGrpcError::Grpc)
}
fn call(&mut self, mut request: http::Request<ReqBody>) -> Self::Future {
let token_fut = self.token_fn.as_ref().map(|token_fn| token_fn.get_token());
let inner = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, inner);
Box::pin(async move {
if let Some(token_fut) = token_fut {
let token = token_fut.await.map_err(AuthGrpcError::Auth)?;
crate::auth::add_auth_token(&mut request, &token)
.map_err(|e| AuthGrpcError::InvalidToken(e, token.as_ref().to_owned()))?;
}
inner.call(request).await.map_err(AuthGrpcError::Grpc)
})
}
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn auth_token_in_request_header() -> Result<(), Box<dyn std::error::Error>> {
const TOKEN: &str = "this is my token";
#[derive(Clone)]
struct AssertionService;
impl GrpcService<()> for AssertionService {
type Error = std::io::Error;
type Future =
BoxFuture<'static, Result<http::Response<Self::ResponseBody>, Self::Error>>;
type ResponseBody = tonic::body::BoxBody;
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
unimplemented!()
}
fn call(&mut self, request_downstream_of_auth: http::Request<()>) -> Self::Future {
assert_eq!(
request_downstream_of_auth
.headers()
.get(http::header::AUTHORIZATION),
Some(
&http::header::HeaderValue::from_str(&format!("Bearer {}", TOKEN)).unwrap()
),
);
Box::pin(async { Ok(http::Response::new(tonic::body::empty_body())) })
}
}
let mut auth_service = AuthGrpcService::new(
AssertionService,
Some(|| async { Ok::<_, std::io::Error>(TOKEN) }),
);
assert!(auth_service
.call(http::request::Request::new(()))
.await
.is_ok());
Ok(())
}
#[tokio::test]
async fn auth_token_failed_fetch() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Debug, thiserror::Error)]
#[error("this is a test error")]
struct InjectedError;
let mut auth_service = AuthGrpcService::new(
tonic::transport::Endpoint::from_static("localhost").connect_lazy(),
Some(|| async { Err::<String, _>(InjectedError) }),
);
assert!(matches!(
auth_service
.call(http::request::Request::new(tonic::body::empty_body()))
.await,
Err(AuthGrpcError::Auth(InjectedError))
));
Ok(())
}
#[tokio::test]
async fn auth_token_invalid_header() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Debug, thiserror::Error)]
#[error("this is a test error")]
struct InjectedError;
let mut auth_service = AuthGrpcService::new(
tonic::transport::Endpoint::from_static("localhost").connect_lazy(),
Some(|| async { Ok::<_, std::io::Error>("\u{0000}") }),
);
assert!(matches!(
auth_service
.call(http::request::Request::new(tonic::body::empty_body()))
.await,
Err(AuthGrpcError::InvalidToken(
http::header::InvalidHeaderValue { .. },
_
))
));
Ok(())
}
#[tokio::test]
async fn no_auth_token_fn() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Clone)]
struct OkService;
impl GrpcService<()> for OkService {
type Error = std::io::Error;
type Future =
BoxFuture<'static, Result<http::Response<Self::ResponseBody>, Self::Error>>;
type ResponseBody = tonic::body::BoxBody;
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
unimplemented!()
}
fn call(&mut self, request_downstream_of_auth: http::Request<()>) -> Self::Future {
assert_eq!(
request_downstream_of_auth
.headers()
.get(http::header::AUTHORIZATION),
None,
);
Box::pin(async { Ok(http::Response::new(tonic::body::empty_body())) })
}
}
type TokenFn = dyn Fn() -> futures::future::Ready<Result<String, tonic::transport::Error>>;
let token_fn: Option<Box<TokenFn>> = None;
let mut auth_service = AuthGrpcService::new(OkService, token_fn);
let result = auth_service.call(http::request::Request::new(())).await;
assert!(result.is_ok());
Ok(())
}
}