Skip to main content

unitycatalog_server/rest/
auth.rs

1//! Authentication middleware for Delta Sharing server.
2use std::marker::PhantomData;
3use std::task::{Context, Poll};
4
5use axum::extract::Request;
6use axum::response::{IntoResponse, Response};
7use futures_util::{FutureExt, future::BoxFuture};
8use tower::{Layer, Service};
9use unitycatalog_common::Result;
10
11use crate::api::RequestContext;
12use crate::policy::Principal;
13
14/// Extract a [`RequestContext`] from the [`Principal`] that the
15/// [`AuthenticationMiddleware`] inserted into the request extensions.
16///
17/// This is the transport-side counterpart to that middleware: the middleware
18/// writes the `Principal`, this reads it back so handlers can receive a
19/// `RequestContext`. It lives here (rather than next to the `RequestContext`
20/// definition in `crate::api`) to keep the `api` module free of any axum
21/// coupling.
22impl<S: Send + Sync> axum::extract::FromRequestParts<S> for RequestContext {
23    type Rejection = std::convert::Infallible;
24
25    async fn from_request_parts(
26        parts: &mut axum::http::request::Parts,
27        _state: &S,
28    ) -> std::result::Result<Self, Self::Rejection> {
29        let recipient = parts
30            .extensions
31            .get::<Principal>()
32            .cloned()
33            .unwrap_or_else(Principal::anonymous);
34        Ok(RequestContext { recipient })
35    }
36}
37
38/// Authenticator for authenticating requests to a sharing server.
39///
40/// `I` is the identity type inserted into request extensions. It must be
41/// `Clone + Send + Sync + 'static` so it can be stored in axum extensions.
42pub trait Authenticator<I: Clone + Send + Sync + 'static>: Send + Sync + 'static {
43    /// Authenticate a request and return an identity value.
44    ///
45    /// This method should return the identity of the caller, or an error if the
46    /// request is not authenticated or the identity cannot be determined.
47    fn authenticate(&self, request: &Request) -> Result<I>;
48}
49
50/// Authenticator that always marks the recipient as anonymous.
51///
52/// This is the default authenticator used when no authentication is configured.
53/// The server is designed to run behind a reverse proxy (e.g., nginx, Envoy) that
54/// handles authentication and injects the authenticated identity (e.g., via an
55/// `X-Forwarded-User` header). A production `Authenticator` implementation should
56/// read that header and return `Principal::User(name)`.
57///
58/// TODO: implement a `ReverseProxyAuthenticator` that extracts `Principal` from
59/// the `X-Forwarded-User` (or similar) header set by the upstream proxy.
60#[derive(Clone)]
61pub struct AnonymousAuthenticator;
62
63impl Authenticator<Principal> for AnonymousAuthenticator {
64    fn authenticate(&self, _: &Request) -> Result<Principal> {
65        // TODO: extract from reverse proxy middleware (e.g., X-Forwarded-User header)
66        Ok(Principal::anonymous())
67    }
68}
69
70/// Middleware that authenticates requests using the given [`Authenticator`].
71#[derive(Clone)]
72pub struct AuthenticationMiddleware<S, T, I = Principal> {
73    inner: S,
74    authenticator: T,
75    _identity: PhantomData<I>,
76}
77
78#[allow(unused)]
79impl<S, T, I> AuthenticationMiddleware<S, T, I> {
80    /// Create new [`AuthenticationMiddleware`].
81    pub fn new(inner: S, authenticator: T) -> Self {
82        Self {
83            inner,
84            authenticator,
85            _identity: PhantomData,
86        }
87    }
88
89    /// Create a new [`AuthenticationLayer`] with the given [`Authenticator`].
90    ///
91    /// This is a convenience method that is equivalent to calling [`AuthenticationLayer::new`].
92    pub fn layer(authenticator: T) -> AuthenticationLayer<T, I> {
93        AuthenticationLayer::new(authenticator)
94    }
95}
96
97impl<S, T, I> Service<Request> for AuthenticationMiddleware<S, T, I>
98where
99    S: Service<Request, Response = Response> + Send + 'static,
100    S::Future: Send + 'static,
101    T: Authenticator<I>,
102    I: Clone + Send + Sync + 'static,
103{
104    type Response = S::Response;
105    type Error = S::Error;
106    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
107
108    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
109        self.inner.poll_ready(cx)
110    }
111
112    fn call(&mut self, mut req: Request) -> Self::Future {
113        match self.authenticator.authenticate(&req) {
114            Ok(identity) => {
115                req.extensions_mut().insert(identity);
116                self.inner.call(req).boxed()
117            }
118            Err(e) => async { Ok(crate::Error::from(e).into_response()) }.boxed(),
119        }
120    }
121}
122
123/// Layer that applies the [`AuthenticationMiddleware`].
124#[derive(Clone)]
125pub struct AuthenticationLayer<T, I = Principal> {
126    authenticator: T,
127    _identity: PhantomData<I>,
128}
129
130impl<T, I> AuthenticationLayer<T, I> {
131    /// Create a new [`AuthenticationLayer`] with the provided [`Authenticator`].
132    pub fn new(authenticator: T) -> Self {
133        Self {
134            authenticator,
135            _identity: PhantomData,
136        }
137    }
138}
139
140impl<S, T, I> Layer<S> for AuthenticationLayer<T, I>
141where
142    T: Clone + Send + Sync + 'static,
143    I: Clone + Send + Sync + 'static,
144{
145    type Service = AuthenticationMiddleware<S, T, I>;
146
147    fn layer(&self, inner: S) -> Self::Service {
148        AuthenticationMiddleware {
149            inner,
150            authenticator: self.authenticator.clone(),
151            _identity: PhantomData,
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use axum::body::Body;
159    use axum::extract::Request;
160    use axum::http::{StatusCode, header};
161    use tower::{ServiceBuilder, ServiceExt};
162
163    use super::*;
164
165    async fn check_recipient(req: Request) -> Result<Response<Body>> {
166        assert!(matches!(
167            req.extensions().get::<Principal>(),
168            Some(Principal::Anonymous) | Some(Principal::User(_))
169        ));
170        Ok(Response::new(req.into_body()))
171    }
172
173    #[tokio::test]
174    async fn test_authentication_middleware() {
175        let authenticator = AnonymousAuthenticator {};
176        let mut service = ServiceBuilder::new()
177            .layer(AuthenticationLayer::new(authenticator))
178            .service_fn(check_recipient);
179
180        let request = Request::get("/")
181            .header(header::AUTHORIZATION, "Bearer foo")
182            .body(Body::empty())
183            .unwrap();
184
185        let response = service.ready().await.unwrap().call(request).await.unwrap();
186        assert_eq!(response.status(), StatusCode::OK);
187
188        let request = Request::get("/").body(Body::empty()).unwrap();
189        let response = service.ready().await.unwrap().call(request).await.unwrap();
190        assert_eq!(response.status(), StatusCode::OK);
191    }
192}