1use std::sync::Arc;
24
25use atd_runtime::secrets::{BearerIdentity, BrokerError, TokenBroker};
26use axum::http::{HeaderMap, StatusCode};
27
28#[derive(Debug)]
38#[non_exhaustive]
39pub enum BearerOutcome {
40 Anonymous,
43 Validated(BearerIdentity),
45
46 HeaderMissing,
49 BearerEmpty,
52 NoBrokerConfigured,
57 Unknown,
59 Expired,
61 Revoked(String),
63 BrokerNotConfigured,
67 Lookup(String),
72 Internal(String),
74}
75
76impl BearerOutcome {
77 pub fn http_status(&self) -> StatusCode {
81 match self {
82 Self::Validated(_) | Self::Anonymous => StatusCode::OK,
83 Self::BearerEmpty => StatusCode::BAD_REQUEST,
84 Self::HeaderMissing | Self::Unknown | Self::Expired | Self::Revoked(_) => {
85 StatusCode::UNAUTHORIZED
86 }
87 Self::NoBrokerConfigured | Self::BrokerNotConfigured => StatusCode::NOT_IMPLEMENTED,
88 Self::Lookup(_) => StatusCode::SERVICE_UNAVAILABLE,
89 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
90 }
91 }
92
93 pub fn www_authenticate(&self) -> Option<&'static str> {
96 match self {
97 Self::HeaderMissing => Some(r#"Bearer"#),
98 Self::Unknown => Some(r#"Bearer error="invalid_token""#),
99 Self::Expired => Some(r#"Bearer error="invalid_token", error_description="expired""#),
100 Self::Revoked(_) => {
101 Some(r#"Bearer error="invalid_token", error_description="revoked""#)
102 }
103 _ => None,
104 }
105 }
106
107 pub fn retry_after(&self) -> Option<u64> {
110 match self {
111 Self::Lookup(_) => Some(5),
112 _ => None,
113 }
114 }
115
116 pub fn rejection_message(&self) -> Option<String> {
119 match self {
120 Self::Validated(_) | Self::Anonymous => None,
121 Self::HeaderMissing => Some("Authorization: Bearer required".into()),
122 Self::BearerEmpty => Some("bearer is empty after `Bearer ` prefix".into()),
123 Self::NoBrokerConfigured => {
124 Some("token broker not configured for HTTP bearer auth".into())
125 }
126 Self::Unknown => Some("bearer not recognised".into()),
127 Self::Expired => Some("bearer expired".into()),
128 Self::Revoked(msg) => Some(format!("bearer revoked: {msg}")),
129 Self::BrokerNotConfigured => {
130 Some("broker does not support bearer auth (override resolve_bearer)".into())
131 }
132 Self::Lookup(msg) => Some(format!("broker lookup failed (transient): {msg}")),
133 Self::Internal(msg) => Some(format!("broker internal error: {msg}")),
134 }
135 }
136
137 pub fn is_admitted(&self) -> bool {
140 matches!(self, Self::Validated(_) | Self::Anonymous)
141 }
142}
143
144pub async fn resolve_bearer(
148 headers: &HeaderMap,
149 broker: Option<&Arc<dyn TokenBroker>>,
150 require_bearer: bool,
151) -> BearerOutcome {
152 let raw = headers
153 .get("authorization")
154 .and_then(|v| v.to_str().ok())
155 .and_then(|h| h.strip_prefix("Bearer "));
156
157 let Some(token) = raw else {
158 return if require_bearer {
159 BearerOutcome::HeaderMissing
160 } else {
161 BearerOutcome::Anonymous
162 };
163 };
164
165 if token.trim().is_empty() {
168 return BearerOutcome::BearerEmpty;
169 }
170
171 let Some(broker) = broker else {
172 return BearerOutcome::NoBrokerConfigured;
173 };
174
175 match broker.resolve_bearer(token).await {
176 Ok(Some(identity)) => BearerOutcome::Validated(identity),
177 Ok(None) => BearerOutcome::Unknown,
178 Err(BrokerError::NotConfigured) => BearerOutcome::BrokerNotConfigured,
179 Err(BrokerError::Expired) => BearerOutcome::Expired,
180 Err(BrokerError::Revoked(msg)) => BearerOutcome::Revoked(msg),
181 Err(BrokerError::Lookup(msg)) => BearerOutcome::Lookup(msg),
182 Err(BrokerError::Internal(msg)) => BearerOutcome::Internal(msg),
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use atd_runtime::secrets::{ResolveBearerFuture, ResolveFuture};
190 use axum::http::HeaderValue;
191
192 fn headers_with_bearer(token: &str) -> HeaderMap {
193 let mut h = HeaderMap::new();
194 h.insert(
195 "authorization",
196 HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
197 );
198 h
199 }
200
201 #[tokio::test]
202 async fn missing_header_anonymous_mode_returns_anonymous() {
203 let h = HeaderMap::new();
204 let outcome = resolve_bearer(&h, None, false).await;
205 assert!(matches!(outcome, BearerOutcome::Anonymous));
206 assert_eq!(outcome.http_status(), StatusCode::OK);
207 assert!(outcome.is_admitted());
208 }
209
210 #[tokio::test]
211 async fn missing_header_strict_mode_returns_header_missing() {
212 let h = HeaderMap::new();
213 let outcome = resolve_bearer(&h, None, true).await;
214 assert!(matches!(outcome, BearerOutcome::HeaderMissing));
215 assert_eq!(outcome.http_status(), StatusCode::UNAUTHORIZED);
216 assert_eq!(outcome.www_authenticate(), Some("Bearer"));
217 assert!(outcome.retry_after().is_none());
218 assert!(!outcome.is_admitted());
219 }
220
221 #[tokio::test]
222 async fn bearer_present_but_empty_returns_bearer_empty() {
223 let mut h = HeaderMap::new();
224 h.insert("authorization", HeaderValue::from_static("Bearer "));
225 let outcome = resolve_bearer(&h, None, true).await;
226 assert!(matches!(outcome, BearerOutcome::BearerEmpty));
227 assert_eq!(outcome.http_status(), StatusCode::BAD_REQUEST);
228 }
229
230 #[tokio::test]
231 async fn bearer_without_broker_returns_no_broker_configured() {
232 let h = headers_with_bearer("xyz");
233 let outcome = resolve_bearer(&h, None, true).await;
234 assert!(matches!(outcome, BearerOutcome::NoBrokerConfigured));
235 assert_eq!(outcome.http_status(), StatusCode::NOT_IMPLEMENTED);
236 assert!(outcome.www_authenticate().is_none());
237 }
238
239 struct FixedErrBroker(BrokerError);
242
243 impl std::fmt::Debug for FixedErrBroker {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 f.debug_struct("FixedErrBroker").finish()
246 }
247 }
248
249 impl TokenBroker for FixedErrBroker {
250 fn resolve<'a>(&'a self, _caller_id: Option<&'a str>) -> ResolveFuture<'a> {
251 Box::pin(async { Ok(None) })
252 }
253 fn resolve_bearer<'a>(&'a self, _bearer: &'a str) -> ResolveBearerFuture<'a> {
254 let err = match &self.0 {
255 BrokerError::NotConfigured => BrokerError::NotConfigured,
256 BrokerError::Expired => BrokerError::Expired,
257 BrokerError::Revoked(s) => BrokerError::Revoked(s.clone()),
258 BrokerError::Lookup(s) => BrokerError::Lookup(s.clone()),
259 BrokerError::Internal(s) => BrokerError::Internal(s.clone()),
260 };
261 Box::pin(async move { Err(err) })
262 }
263 }
264
265 fn arc_broker(err: BrokerError) -> Arc<dyn TokenBroker> {
266 Arc::new(FixedErrBroker(err))
267 }
268
269 #[tokio::test]
270 async fn broker_returns_none_maps_to_unknown_401_invalid_token() {
271 struct NoneBroker;
272 impl std::fmt::Debug for NoneBroker {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 f.debug_struct("NoneBroker").finish()
275 }
276 }
277 impl TokenBroker for NoneBroker {
278 fn resolve<'a>(&'a self, _caller_id: Option<&'a str>) -> ResolveFuture<'a> {
279 Box::pin(async { Ok(None) })
280 }
281 fn resolve_bearer<'a>(&'a self, _bearer: &'a str) -> ResolveBearerFuture<'a> {
282 Box::pin(async { Ok(None) })
283 }
284 }
285 let h = headers_with_bearer("anything");
286 let broker: Arc<dyn TokenBroker> = Arc::new(NoneBroker);
287 let outcome = resolve_bearer(&h, Some(&broker), true).await;
288 assert!(matches!(outcome, BearerOutcome::Unknown));
289 assert_eq!(outcome.http_status(), StatusCode::UNAUTHORIZED);
290 assert_eq!(
291 outcome.www_authenticate(),
292 Some(r#"Bearer error="invalid_token""#)
293 );
294 }
295
296 #[tokio::test]
297 async fn broker_expired_maps_to_401_with_error_description_expired() {
298 let h = headers_with_bearer("anything");
299 let broker = arc_broker(BrokerError::Expired);
300 let outcome = resolve_bearer(&h, Some(&broker), true).await;
301 assert!(matches!(outcome, BearerOutcome::Expired));
302 assert_eq!(outcome.http_status(), StatusCode::UNAUTHORIZED);
303 assert_eq!(
304 outcome.www_authenticate(),
305 Some(r#"Bearer error="invalid_token", error_description="expired""#)
306 );
307 }
308
309 #[tokio::test]
310 async fn broker_revoked_maps_to_401_with_error_description_revoked() {
311 let h = headers_with_bearer("anything");
312 let broker = arc_broker(BrokerError::Revoked("user revoked".into()));
313 let outcome = resolve_bearer(&h, Some(&broker), true).await;
314 match &outcome {
315 BearerOutcome::Revoked(msg) => assert_eq!(msg, "user revoked"),
316 other => panic!("expected Revoked, got {other:?}"),
317 }
318 assert_eq!(outcome.http_status(), StatusCode::UNAUTHORIZED);
319 assert_eq!(
320 outcome.www_authenticate(),
321 Some(r#"Bearer error="invalid_token", error_description="revoked""#)
322 );
323 }
324
325 #[tokio::test]
326 async fn broker_lookup_maps_to_503_with_retry_after_5() {
327 let h = headers_with_bearer("anything");
328 let broker = arc_broker(BrokerError::Lookup("sqlite locked".into()));
329 let outcome = resolve_bearer(&h, Some(&broker), true).await;
330 assert!(matches!(outcome, BearerOutcome::Lookup(_)));
331 assert_eq!(outcome.http_status(), StatusCode::SERVICE_UNAVAILABLE);
332 assert_eq!(outcome.retry_after(), Some(5));
333 assert!(outcome.www_authenticate().is_none());
336 }
337
338 #[tokio::test]
339 async fn broker_internal_maps_to_500_no_headers() {
340 let h = headers_with_bearer("anything");
341 let broker = arc_broker(BrokerError::Internal("oh no".into()));
342 let outcome = resolve_bearer(&h, Some(&broker), true).await;
343 assert!(matches!(outcome, BearerOutcome::Internal(_)));
344 assert_eq!(outcome.http_status(), StatusCode::INTERNAL_SERVER_ERROR);
345 assert!(outcome.www_authenticate().is_none());
346 assert!(outcome.retry_after().is_none());
347 }
348
349 #[tokio::test]
350 async fn broker_not_configured_maps_to_501() {
351 let h = headers_with_bearer("anything");
352 let broker = arc_broker(BrokerError::NotConfigured);
353 let outcome = resolve_bearer(&h, Some(&broker), true).await;
354 assert!(matches!(outcome, BearerOutcome::BrokerNotConfigured));
355 assert_eq!(outcome.http_status(), StatusCode::NOT_IMPLEMENTED);
356 }
357
358 struct FixedOkBroker(BearerIdentity);
361 impl std::fmt::Debug for FixedOkBroker {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 f.debug_struct("FixedOkBroker").finish()
364 }
365 }
366 impl TokenBroker for FixedOkBroker {
367 fn resolve<'a>(&'a self, _caller_id: Option<&'a str>) -> ResolveFuture<'a> {
368 Box::pin(async { Ok(None) })
369 }
370 fn resolve_bearer<'a>(&'a self, _bearer: &'a str) -> ResolveBearerFuture<'a> {
371 let id = self.0.clone();
372 Box::pin(async move { Ok(Some(id)) })
373 }
374 }
375
376 #[tokio::test]
377 async fn good_bearer_returns_validated_identity() {
378 let id = BearerIdentity {
379 caller_id: "agent-A".into(),
380 granted_capabilities: vec!["records:read".into()],
381 secrets: None,
382 expires_at: None,
383 cache_until: None,
384 };
385 let h = headers_with_bearer("good-token");
386 let broker: Arc<dyn TokenBroker> = Arc::new(FixedOkBroker(id));
387 let outcome = resolve_bearer(&h, Some(&broker), true).await;
388 match &outcome {
389 BearerOutcome::Validated(id) => {
390 assert_eq!(id.caller_id, "agent-A");
391 assert_eq!(id.granted_capabilities, vec!["records:read"]);
392 }
393 other => panic!("expected Validated, got {other:?}"),
394 }
395 assert_eq!(outcome.http_status(), StatusCode::OK);
396 assert!(outcome.is_admitted());
397 }
398}