unitycatalog_server/rest/
auth.rs1use 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
14pub const DEFAULT_FORWARDED_USER_HEADER: &str = "x-forwarded-user";
16
17impl<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
41pub trait Authenticator<I: Clone + Send + Sync + 'static>: Send + Sync + 'static {
46 fn authenticate(&self, request: &Request) -> Result<I>;
51}
52
53#[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#[derive(Clone)]
85pub struct ReverseProxyAuthenticator {
86 header: String,
89 on_missing: OnMissingIdentity,
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum OnMissingIdentity {
96 Reject,
99 Anonymous,
102}
103
104impl ReverseProxyAuthenticator {
105 pub fn new() -> Self {
108 Self {
109 header: DEFAULT_FORWARDED_USER_HEADER.to_string(),
110 on_missing: OnMissingIdentity::Reject,
111 }
112 }
113
114 pub fn with_header(mut self, header: impl Into<String>) -> Self {
116 self.header = header.into();
117 self
118 }
119
120 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#[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 pub fn new(inner: S, authenticator: T) -> Self {
173 Self {
174 inner,
175 authenticator,
176 _identity: PhantomData,
177 }
178 }
179
180 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#[derive(Clone)]
216pub struct AuthenticationLayer<T, I = Principal> {
217 authenticator: T,
218 _identity: PhantomData<I>,
219}
220
221impl<T, I> AuthenticationLayer<T, I> {
222 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}