use futures::future::BoxFuture;
use std::{
sync::Arc,
task::{Context, Poll},
};
use tonic::client::GrpcService;
#[derive(Clone)]
pub struct AuthGrpcService<Service, C> {
inner: Service,
auth: Option<crate::Auth<C>>,
scopes: Arc<Vec<String>>,
}
impl<Service, C> AuthGrpcService<Service, C> {
pub fn new<ReqBody>(service: Service, auth: Option<crate::Auth<C>>, scopes: Vec<String>) -> Self
where
Service: GrpcService<ReqBody> + Clone + 'static,
Service::Error: std::error::Error + Send + Sync + 'static,
{
Self {
inner: service,
auth,
scopes: Arc::new(scopes),
}
}
}
#[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 did not generate a token")]
MissingToken,
#[error("Auth token formed invalid HTTP header: {1}")]
InvalidToken(#[source] http::header::InvalidHeaderValue, String),
#[error(transparent)]
Grpc(ServiceErr),
}
impl<Service, C, ReqBody> GrpcService<ReqBody> for AuthGrpcService<Service, C>
where
Service: GrpcService<ReqBody> + Clone + Send + 'static,
Service::Error: std::error::Error + Send + Sync + 'static,
Service::Future: Send,
C: tower::Service<http::Uri> + Clone + Send + Sync + 'static,
C::Response: hyper::client::connect::Connection
+ tokio::io::AsyncRead
+ tokio::io::AsyncWrite
+ Send
+ Unpin
+ 'static,
C::Future: Send + Unpin + 'static,
C::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
ReqBody: Send + 'static,
{
type Error = AuthGrpcError<Service::Error, yup_oauth2::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 auth = self.auth.clone();
let scopes = Arc::clone(&self.scopes);
let token_fut = auth.map(|auth| async move { auth.token(&scopes).await });
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)?;
let token = token.token().ok_or(AuthGrpcError::MissingToken)?;
crate::auth::add_auth_token(&mut request, &token)
.map_err(|e| AuthGrpcError::InvalidToken(e, token.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(
yup_oauth2::AccessTokenAuthenticator::builder(TOKEN.to_owned())
.build()
.await?,
),
vec![],
);
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>> {
let mut auth_service = AuthGrpcService::new(
tonic::transport::Endpoint::from_static("localhost").connect_lazy(),
Some(
yup_oauth2::AuthorizedUserAuthenticator::builder(
yup_oauth2::authorized_user::AuthorizedUserSecret {
client_id: String::new(),
client_secret: String::new(),
refresh_token: String::new(),
key_type: String::new(),
},
)
.build()
.await?,
),
vec![],
);
assert!(matches!(
auth_service
.call(http::request::Request::new(tonic::body::empty_body()))
.await,
Err(AuthGrpcError::Auth(yup_oauth2::Error::AuthError(_)))
));
Ok(())
}
#[tokio::test]
async fn auth_token_invalid_header() -> Result<(), Box<dyn std::error::Error>> {
let mut auth_service = AuthGrpcService::new(
tonic::transport::Endpoint::from_static("localhost").connect_lazy(),
Some(
yup_oauth2::AccessTokenAuthenticator::builder("\u{0000}".to_owned())
.build()
.await?,
),
vec![],
);
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() -> 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())) })
}
}
let mut auth_service =
AuthGrpcService::<_, crate::DefaultConnector>::new(OkService, None, vec![]);
let result = auth_service.call(http::request::Request::new(())).await;
assert!(result.is_ok());
Ok(())
}
}