pub use adk_core::RequestContext;
use async_trait::async_trait;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use std::convert::Infallible;
#[async_trait]
pub trait RequestContextExtractor: Send + Sync {
async fn extract(
&self,
parts: &axum::http::request::Parts,
) -> Result<RequestContext, RequestContextError>;
}
#[derive(Debug, thiserror::Error)]
pub enum RequestContextError {
#[error("missing authorization header")]
MissingAuth,
#[error("invalid token: {0}")]
InvalidToken(String),
#[error("extraction failed: {0}")]
ExtractionFailed(String),
}
#[derive(Debug, Clone, Default)]
pub struct AuthenticatedCaller(pub Option<RequestContext>);
impl AuthenticatedCaller {
pub fn user_id(&self) -> Option<&str> {
self.0.as_ref().map(|context| context.user_id.as_str())
}
}
impl<S: Send + Sync> FromRequestParts<S> for AuthenticatedCaller {
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let context = parts
.extensions
.get::<Option<RequestContext>>()
.cloned()
.flatten()
.or_else(|| parts.extensions.get::<RequestContext>().cloned());
Ok(Self(context))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn context(user_id: &str) -> RequestContext {
RequestContext {
user_id: user_id.to_string(),
scopes: vec![],
metadata: Default::default(),
}
}
async fn extract(parts: &mut Parts) -> Option<String> {
let caller = AuthenticatedCaller::from_request_parts(parts, &()).await.unwrap();
caller.user_id().map(str::to_string)
}
#[tokio::test]
async fn reads_the_auth_layer_extension() {
let (mut parts, ()) = axum::http::Request::new(()).into_parts();
parts.extensions.insert::<Option<RequestContext>>(Some(context("alice")));
assert_eq!(extract(&mut parts).await, Some("alice".to_string()));
}
#[tokio::test]
async fn reads_a_bare_request_context_extension() {
let (mut parts, ()) = axum::http::Request::new(()).into_parts();
parts.extensions.insert(context("bob"));
assert_eq!(extract(&mut parts).await, Some("bob".to_string()));
}
#[tokio::test]
async fn is_none_without_authentication() {
let (mut parts, ()) = axum::http::Request::new(()).into_parts();
assert_eq!(extract(&mut parts).await, None);
parts.extensions.insert::<Option<RequestContext>>(None);
assert_eq!(extract(&mut parts).await, None);
}
}