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::{Error, Result};
10
11use crate::api::RequestContext;
12use crate::policy::Principal;
13
14/// The default header a reverse proxy uses to forward the authenticated user.
15pub const DEFAULT_FORWARDED_USER_HEADER: &str = "x-forwarded-user";
16
17/// Extract a [`RequestContext`] from the [`Principal`] that the
18/// [`AuthenticationMiddleware`] inserted into the request extensions.
19///
20/// This is the transport-side counterpart to that middleware: the middleware
21/// writes the `Principal`, this reads it back so handlers can receive a
22/// `RequestContext`. It lives here (rather than next to the `RequestContext`
23/// definition in `crate::api`) to keep the `api` module free of any axum
24/// coupling.
25impl<S: Send + Sync> axum::extract::FromRequestParts<S> for RequestContext {
26    type Rejection = std::convert::Infallible;
27
28    async fn from_request_parts(
29        parts: &mut axum::http::request::Parts,
30        _state: &S,
31    ) -> std::result::Result<Self, Self::Rejection> {
32        let recipient = parts
33            .extensions
34            .get::<Principal>()
35            .cloned()
36            .unwrap_or_else(Principal::anonymous);
37        Ok(RequestContext { recipient })
38    }
39}
40
41/// Authenticator for authenticating requests to a sharing server.
42///
43/// `I` is the identity type inserted into request extensions. It must be
44/// `Clone + Send + Sync + 'static` so it can be stored in axum extensions.
45pub trait Authenticator<I: Clone + Send + Sync + 'static>: Send + Sync + 'static {
46    /// Authenticate a request and return an identity value.
47    ///
48    /// This method should return the identity of the caller, or an error if the
49    /// request is not authenticated or the identity cannot be determined.
50    fn authenticate(&self, request: &Request) -> Result<I>;
51}
52
53/// Authenticator that always marks the recipient as anonymous.
54///
55/// This is the default authenticator used when no authentication is configured.
56/// The server is designed to run behind a reverse proxy (e.g., nginx, Envoy) that
57/// handles authentication and injects the authenticated identity (e.g., via an
58/// `X-Forwarded-User` header). A production `Authenticator` implementation should
59/// read that header and return `Principal::User(name)` — see
60/// [`ReverseProxyAuthenticator`].
61#[derive(Clone)]
62pub struct AnonymousAuthenticator;
63
64impl Authenticator<Principal> for AnonymousAuthenticator {
65    fn authenticate(&self, _: &Request) -> Result<Principal> {
66        Ok(Principal::anonymous())
67    }
68}
69
70/// Authenticator that reads the caller's identity from a header set by a
71/// trusted upstream reverse proxy (e.g. nginx / Envoy / an OAuth2 proxy that
72/// forwards `X-Forwarded-User`).
73///
74/// The proxy is responsible for actually authenticating the request; this
75/// authenticator only *trusts and extracts* the forwarded identity, so it must
76/// only be enabled when the server is not directly reachable — otherwise a
77/// client could forge the header. It exists so that features which vend
78/// credentials on the caller's behalf (notably the same-origin storage
79/// byte-proxy) attribute access to a real user instead of the over-privileged
80/// [`Principal::anonymous`].
81///
82/// When the header is absent, [`on_missing`](Self::with_on_missing) decides
83/// whether to reject the request (`401`) or fall back to anonymous.
84#[derive(Clone)]
85pub struct ReverseProxyAuthenticator {
86    /// Header carrying the forwarded user (matched case-insensitively, as HTTP
87    /// header names are). Defaults to [`DEFAULT_FORWARDED_USER_HEADER`].
88    header: String,
89    /// What to do when the header is absent.
90    on_missing: OnMissingIdentity,
91}
92
93/// Behavior when the forwarded-identity header is absent from a request.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum OnMissingIdentity {
96    /// Reject the request with an authentication error (`401`). The safe
97    /// default for a deployment that expects every request to carry identity.
98    Reject,
99    /// Treat the request as [`Principal::anonymous`]. Useful for a mixed
100    /// deployment where some routes are intentionally unauthenticated.
101    Anonymous,
102}
103
104impl ReverseProxyAuthenticator {
105    /// Build an authenticator reading [`DEFAULT_FORWARDED_USER_HEADER`],
106    /// rejecting requests that lack it.
107    pub fn new() -> Self {
108        Self {
109            header: DEFAULT_FORWARDED_USER_HEADER.to_string(),
110            on_missing: OnMissingIdentity::Reject,
111        }
112    }
113
114    /// Override the header the identity is read from.
115    pub fn with_header(mut self, header: impl Into<String>) -> Self {
116        self.header = header.into();
117        self
118    }
119
120    /// Set the behavior when the header is absent (default:
121    /// [`OnMissingIdentity::Reject`]).
122    pub fn with_on_missing(mut self, on_missing: OnMissingIdentity) -> Self {
123        self.on_missing = on_missing;
124        self
125    }
126}
127
128impl Default for ReverseProxyAuthenticator {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134impl Authenticator<Principal> for ReverseProxyAuthenticator {
135    fn authenticate(&self, request: &Request) -> Result<Principal> {
136        match request.headers().get(&self.header) {
137            Some(value) => {
138                let name = value.to_str().map_err(|_| {
139                    Error::unauthenticated(format!("`{}` header is not valid UTF-8", self.header))
140                })?;
141                let name = name.trim();
142                if name.is_empty() {
143                    return Err(Error::unauthenticated(format!(
144                        "`{}` header is empty",
145                        self.header
146                    )));
147                }
148                Ok(Principal::user(name))
149            }
150            None => match self.on_missing {
151                OnMissingIdentity::Anonymous => Ok(Principal::anonymous()),
152                OnMissingIdentity::Reject => Err(Error::unauthenticated(format!(
153                    "missing `{}` header",
154                    self.header
155                ))),
156            },
157        }
158    }
159}
160
161/// Middleware that authenticates requests using the given [`Authenticator`].
162#[derive(Clone)]
163pub struct AuthenticationMiddleware<S, T, I = Principal> {
164    inner: S,
165    authenticator: T,
166    _identity: PhantomData<I>,
167}
168
169#[allow(unused)]
170impl<S, T, I> AuthenticationMiddleware<S, T, I> {
171    /// Create new [`AuthenticationMiddleware`].
172    pub fn new(inner: S, authenticator: T) -> Self {
173        Self {
174            inner,
175            authenticator,
176            _identity: PhantomData,
177        }
178    }
179
180    /// Create a new [`AuthenticationLayer`] with the given [`Authenticator`].
181    ///
182    /// This is a convenience method that is equivalent to calling [`AuthenticationLayer::new`].
183    pub fn layer(authenticator: T) -> AuthenticationLayer<T, I> {
184        AuthenticationLayer::new(authenticator)
185    }
186}
187
188impl<S, T, I> Service<Request> for AuthenticationMiddleware<S, T, I>
189where
190    S: Service<Request, Response = Response> + Send + 'static,
191    S::Future: Send + 'static,
192    T: Authenticator<I>,
193    I: Clone + Send + Sync + 'static,
194{
195    type Response = S::Response;
196    type Error = S::Error;
197    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
198
199    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
200        self.inner.poll_ready(cx)
201    }
202
203    fn call(&mut self, mut req: Request) -> Self::Future {
204        match self.authenticator.authenticate(&req) {
205            Ok(identity) => {
206                req.extensions_mut().insert(identity);
207                self.inner.call(req).boxed()
208            }
209            Err(e) => async { Ok(crate::Error::from(e).into_response()) }.boxed(),
210        }
211    }
212}
213
214/// Layer that applies the [`AuthenticationMiddleware`].
215#[derive(Clone)]
216pub struct AuthenticationLayer<T, I = Principal> {
217    authenticator: T,
218    _identity: PhantomData<I>,
219}
220
221impl<T, I> AuthenticationLayer<T, I> {
222    /// Create a new [`AuthenticationLayer`] with the provided [`Authenticator`].
223    pub fn new(authenticator: T) -> Self {
224        Self {
225            authenticator,
226            _identity: PhantomData,
227        }
228    }
229}
230
231impl<S, T, I> Layer<S> for AuthenticationLayer<T, I>
232where
233    T: Clone + Send + Sync + 'static,
234    I: Clone + Send + Sync + 'static,
235{
236    type Service = AuthenticationMiddleware<S, T, I>;
237
238    fn layer(&self, inner: S) -> Self::Service {
239        AuthenticationMiddleware {
240            inner,
241            authenticator: self.authenticator.clone(),
242            _identity: PhantomData,
243        }
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use axum::body::Body;
250    use axum::extract::Request;
251    use axum::http::{StatusCode, header};
252    use tower::{ServiceBuilder, ServiceExt};
253
254    use super::*;
255
256    async fn check_recipient(req: Request) -> Result<Response<Body>> {
257        assert!(matches!(
258            req.extensions().get::<Principal>(),
259            Some(Principal::Anonymous) | Some(Principal::User(_))
260        ));
261        Ok(Response::new(req.into_body()))
262    }
263
264    #[tokio::test]
265    async fn test_authentication_middleware() {
266        let authenticator = AnonymousAuthenticator {};
267        let mut service = ServiceBuilder::new()
268            .layer(AuthenticationLayer::new(authenticator))
269            .service_fn(check_recipient);
270
271        let request = Request::get("/")
272            .header(header::AUTHORIZATION, "Bearer foo")
273            .body(Body::empty())
274            .unwrap();
275
276        let response = service.ready().await.unwrap().call(request).await.unwrap();
277        assert_eq!(response.status(), StatusCode::OK);
278
279        let request = Request::get("/").body(Body::empty()).unwrap();
280        let response = service.ready().await.unwrap().call(request).await.unwrap();
281        assert_eq!(response.status(), StatusCode::OK);
282    }
283
284    #[test]
285    fn reverse_proxy_extracts_forwarded_user() {
286        let auth = ReverseProxyAuthenticator::new();
287        let req = Request::get("/")
288            .header(DEFAULT_FORWARDED_USER_HEADER, "alice")
289            .body(Body::empty())
290            .unwrap();
291        assert_eq!(auth.authenticate(&req).unwrap(), Principal::user("alice"));
292    }
293
294    #[test]
295    fn reverse_proxy_honors_custom_header_and_trims() {
296        let auth = ReverseProxyAuthenticator::new().with_header("x-user");
297        let req = Request::get("/")
298            .header("x-user", "  bob  ")
299            .body(Body::empty())
300            .unwrap();
301        assert_eq!(auth.authenticate(&req).unwrap(), Principal::user("bob"));
302    }
303
304    #[test]
305    fn reverse_proxy_rejects_missing_header_by_default() {
306        let auth = ReverseProxyAuthenticator::new();
307        let req = Request::get("/").body(Body::empty()).unwrap();
308        assert!(auth.authenticate(&req).is_err());
309    }
310
311    #[test]
312    fn reverse_proxy_rejects_empty_header() {
313        let auth = ReverseProxyAuthenticator::new();
314        let req = Request::get("/")
315            .header(DEFAULT_FORWARDED_USER_HEADER, "   ")
316            .body(Body::empty())
317            .unwrap();
318        assert!(auth.authenticate(&req).is_err());
319    }
320
321    #[test]
322    fn reverse_proxy_falls_back_to_anonymous_when_configured() {
323        let auth = ReverseProxyAuthenticator::new().with_on_missing(OnMissingIdentity::Anonymous);
324        let req = Request::get("/").body(Body::empty()).unwrap();
325        assert_eq!(auth.authenticate(&req).unwrap(), Principal::anonymous());
326    }
327
328    #[tokio::test]
329    async fn reverse_proxy_missing_identity_maps_to_401() {
330        let mut service = ServiceBuilder::new()
331            .layer(AuthenticationLayer::new(ReverseProxyAuthenticator::new()))
332            .service_fn(check_recipient);
333        let request = Request::get("/").body(Body::empty()).unwrap();
334        let response = service.ready().await.unwrap().call(request).await.unwrap();
335        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
336    }
337}