1#![cfg_attr(
51 not(test),
52 deny(
53 clippy::unwrap_used,
54 clippy::expect_used,
55 clippy::panic,
56 clippy::unreachable,
57 clippy::todo,
58 clippy::unimplemented,
59 clippy::indexing_slicing,
60 clippy::string_slice,
61 clippy::arithmetic_side_effects,
62 )
63)]
64
65use std::collections::HashMap;
66use std::fmt;
67use std::future::Future;
68use std::pin::Pin;
69use std::sync::Arc;
70use std::task::{Context, Poll};
71
72use autumn_edge::EdgeKv;
73use tower::{Layer, Service};
74
75use crate::cache::Cache;
76
77pub use autumn_edge::{EdgeIdentity, EdgeRole, EdgeUserId};
78
79#[derive(Clone, Debug, PartialEq, Eq, Hash)]
82pub struct SessionId(String);
83
84pub trait EdgeIdentityProvider: Send + Sync + 'static {
89 type Error: std::error::Error + Send + Sync + 'static;
91 fn resolve(
93 &self,
94 request: &http::Request<axum::body::Body>,
95 ) -> impl Future<Output = Result<Option<EdgeIdentity>, Self::Error>> + Send;
96}
97
98pub struct EdgeIdentityLayer<P>(Arc<P>);
100
101impl<P> Clone for EdgeIdentityLayer<P> {
102 fn clone(&self) -> Self {
103 Self(Arc::clone(&self.0))
104 }
105}
106
107impl<P> EdgeIdentityLayer<P> {
108 #[must_use]
110 pub fn new(provider: P) -> Self {
111 Self(Arc::new(provider))
112 }
113}
114
115impl<P, Inner> Layer<Inner> for EdgeIdentityLayer<P>
116where
117 P: EdgeIdentityProvider,
118{
119 type Service = EdgeIdentityService<P, Inner>;
120 fn layer(&self, inner: Inner) -> Self::Service {
121 EdgeIdentityService {
122 provider: Arc::clone(&self.0),
123 inner,
124 }
125 }
126}
127
128pub struct EdgeIdentityService<P, Inner> {
130 provider: Arc<P>,
131 inner: Inner,
132}
133
134impl<P, Inner: Clone> Clone for EdgeIdentityService<P, Inner> {
135 fn clone(&self) -> Self {
136 Self {
137 provider: Arc::clone(&self.provider),
138 inner: self.inner.clone(),
139 }
140 }
141}
142
143impl<P, Inner> Service<axum::extract::Request> for EdgeIdentityService<P, Inner>
144where
145 P: EdgeIdentityProvider,
146 Inner: Service<axum::extract::Request, Response = axum::response::Response>
147 + Clone
148 + Send
149 + 'static,
150 Inner::Future: Send + 'static,
151 Inner::Error: Send + 'static,
152{
153 type Response = axum::response::Response;
154 type Error = Inner::Error;
155 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
156
157 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
158 self.inner.poll_ready(cx)
159 }
160
161 fn call(&mut self, mut request: axum::extract::Request) -> Self::Future {
162 let provider = Arc::clone(&self.provider);
163 let mut inner = self.inner.clone();
164 std::mem::swap(&mut self.inner, &mut inner);
165 Box::pin(async move {
166 match provider.resolve(&request).await {
167 Ok(Some(identity)) => {
168 request.extensions_mut().insert(identity);
169 }
170 Ok(None) => {}
171 Err(error) => {
172 tracing::warn!(%error, "edge identity infrastructure failure; falling through to origin");
173 }
174 }
175 inner.call(request).await
176 })
177 }
178}
179
180pub trait EdgeIdentityProjector: Send + Sync + 'static {
182 fn project(&self, session: &HashMap<String, String>) -> Option<EdgeIdentity>;
184}
185
186pub struct SessionIdentityProvider<S, P> {
189 store: S,
190 projector: P,
191 cookie_name: String,
192 signing_keys: Option<Arc<crate::security::config::ResolvedSigningKeys>>,
193}
194
195impl<S, P> SessionIdentityProvider<S, P> {
196 #[must_use]
201 pub fn new(
202 store: S,
203 projector: P,
204 cookie_name: impl Into<String>,
205 signing_keys: Option<Arc<crate::security::config::ResolvedSigningKeys>>,
206 ) -> Self {
207 Self {
208 store,
209 projector,
210 cookie_name: cookie_name.into(),
211 signing_keys,
212 }
213 }
214}
215
216impl<S, P> EdgeIdentityProvider for SessionIdentityProvider<S, P>
217where
218 S: crate::session::SessionStore,
219 P: EdgeIdentityProjector,
220{
221 type Error = crate::session::SessionStoreError;
222
223 fn resolve(
224 &self,
225 request: &http::Request<axum::body::Body>,
226 ) -> impl Future<Output = Result<Option<EdgeIdentity>, Self::Error>> + Send {
227 let raw_id = crate::session::session_id_from_headers(
228 request.headers(),
229 &self.cookie_name,
230 self.signing_keys.as_deref(),
231 );
232 async move {
233 let Some(raw_id) = raw_id else {
234 return Ok(None);
235 };
236 let id = SessionId(raw_id);
237 let Some(session) = self.store.load(&id.0).await? else {
238 return Ok(None);
239 };
240 Ok(self.projector.project(&session))
241 }
242 }
243}
244
245pub struct AuthSessionProjector {
247 auth_session_key: String,
248}
249
250impl AuthSessionProjector {
251 #[must_use]
253 pub fn new(auth_session_key: impl Into<String>) -> Self {
254 Self {
255 auth_session_key: auth_session_key.into(),
256 }
257 }
258}
259
260impl EdgeIdentityProjector for AuthSessionProjector {
261 fn project(&self, session: &HashMap<String, String>) -> Option<EdgeIdentity> {
262 session
263 .get(&self.auth_session_key)
264 .map(|id| EdgeIdentity::new(EdgeUserId::new(id), Vec::new()))
265 }
266}
267
268pub struct CacheEdgeKv(Arc<dyn Cache>);
301
302impl CacheEdgeKv {
303 #[must_use]
309 pub const fn new(cache: Arc<dyn Cache>) -> Self {
310 Self(cache)
311 }
312
313 #[must_use]
315 pub fn cache(&self) -> &Arc<dyn Cache> {
316 &self.0
317 }
318}
319
320impl fmt::Debug for CacheEdgeKv {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 f.write_str("CacheEdgeKv(..)")
325 }
326}
327
328impl EdgeKv for CacheEdgeKv {
329 fn get(&self, key: &str) -> Option<Vec<u8>> {
330 crate::cache::get_cached::<Vec<u8>>(self.0.as_ref(), key)
331 }
332}
333
334#[cfg(all(test, feature = "cache-moka"))]
335mod tests {
336 use super::*;
337 use crate::cache::{MokaCache, insert_cached};
338 use crate::session::{MemoryStore, SessionStore};
339
340 #[derive(Debug, thiserror::Error)]
341 #[error("provider failed")]
342 struct TestProviderError;
343
344 struct StaticProvider(Option<EdgeIdentity>);
345
346 impl EdgeIdentityProvider for StaticProvider {
347 type Error = TestProviderError;
348
349 fn resolve(
350 &self,
351 _request: &http::Request<axum::body::Body>,
352 ) -> impl Future<Output = Result<Option<EdgeIdentity>, Self::Error>> + Send {
353 std::future::ready(Ok(self.0.clone()))
354 }
355 }
356
357 #[tokio::test]
358 async fn identity_layer_resolves_before_handler_extraction() {
359 use tower::ServiceExt as _;
360
361 async fn handler(identity: EdgeIdentity) -> String {
362 identity.user_id().as_str().to_owned()
363 }
364
365 let router = axum::Router::new()
366 .route("/", axum::routing::get(handler))
367 .layer(EdgeIdentityLayer::new(StaticProvider(Some(
368 EdgeIdentity::new(EdgeUserId::new("alice"), Vec::new()),
369 ))));
370 let response = router
371 .oneshot(
372 http::Request::builder()
373 .uri("/")
374 .body(axum::body::Body::empty())
375 .expect("request"),
376 )
377 .await
378 .expect("infallible");
379 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
380 .await
381 .expect("body");
382 assert_eq!(body, "alice");
383 }
384
385 #[tokio::test]
386 async fn session_identity_projects_only_normalized_claims() {
387 let store = MemoryStore::new();
388 let mut data = HashMap::new();
389 data.insert("account_id".into(), "user-7".into());
390 data.insert("backend_token".into(), "must-not-cross".into());
391 store.save("sid-secret", data).await.expect("memory save");
392 let keys = Arc::new(crate::security::config::ResolvedSigningKeys::new(
393 b"current-secret".to_vec(),
394 Vec::new(),
395 ));
396 let signature = keys.sign(b"sid-secret");
397 let provider = SessionIdentityProvider::new(
398 store,
399 AuthSessionProjector::new("account_id"),
400 "custom.sid",
401 Some(keys),
402 );
403 let request = http::Request::builder()
404 .header(
405 http::header::COOKIE,
406 format!("custom.sid=sid-secret.{signature}"),
407 )
408 .body(axum::body::Body::empty())
409 .expect("request");
410
411 let identity = provider
412 .resolve(&request)
413 .await
414 .expect("store available")
415 .expect("authenticated");
416 assert_eq!(identity.user_id().as_str(), "user-7");
417 let wire = serde_json::to_string(&identity).expect("identity serializes");
418 assert!(!wire.contains("sid-secret"));
419 assert!(!wire.contains("backend_token"));
420 assert!(!wire.contains("must-not-cross"));
421 }
422
423 #[tokio::test]
424 async fn invalid_signature_and_destroyed_session_are_misses() {
425 let store = MemoryStore::new();
426 let keys = Arc::new(crate::security::config::ResolvedSigningKeys::new(
427 b"secret".to_vec(),
428 Vec::new(),
429 ));
430 let provider = SessionIdentityProvider::new(
431 store,
432 AuthSessionProjector::new("user_id"),
433 "autumn.sid",
434 Some(keys),
435 );
436 let request = http::Request::builder()
437 .header(http::header::COOKIE, "autumn.sid=gone.invalid")
438 .body(axum::body::Body::empty())
439 .expect("request");
440 assert_eq!(
441 provider.resolve(&request).await.expect("store available"),
442 None
443 );
444 }
445
446 #[tokio::test]
447 async fn unsigned_session_matches_unsigned_session_layer_policy() {
448 let store = MemoryStore::new();
449 store
450 .save(
451 "plain-id",
452 HashMap::from([("user_id".into(), "alice".into())]),
453 )
454 .await
455 .expect("memory save");
456 let provider = SessionIdentityProvider::new(
457 store,
458 AuthSessionProjector::new("user_id"),
459 "autumn.sid",
460 None,
461 );
462 let request = http::Request::builder()
463 .header(http::header::COOKIE, "autumn.sid=plain-id")
464 .body(axum::body::Body::empty())
465 .expect("request");
466
467 assert_eq!(
468 provider
469 .resolve(&request)
470 .await
471 .expect("store available")
472 .expect("authenticated")
473 .user_id()
474 .as_str(),
475 "alice"
476 );
477 }
478
479 fn cache_with(key: &str, value: &[u8]) -> Arc<dyn Cache> {
480 let cache = MokaCache::new(16, None);
481 insert_cached(&cache, key, value.to_vec(), None);
482 Arc::new(cache)
483 }
484
485 #[test]
486 fn reads_bytes_written_through_the_ordinary_cache_path() {
487 let kv = CacheEdgeKv::new(cache_with("banner", b"hello"));
488 assert_eq!(kv.get("banner"), Some(b"hello".to_vec()));
489 }
490
491 #[test]
492 fn an_absent_key_is_a_miss() {
493 let kv = CacheEdgeKv::new(cache_with("banner", b"hello"));
494 assert_eq!(kv.get("absent"), None);
495 }
496
497 #[test]
498 fn a_value_of_another_type_is_a_miss_not_a_failure() {
499 let cache = MokaCache::new(16, None);
500 insert_cached(&cache, "count", 7_u64, None);
501 let kv = CacheEdgeKv::new(Arc::new(cache));
502
503 assert_eq!(kv.get("count"), None);
504 }
505
506 #[test]
507 fn a_serializing_backend_round_trips_through_the_raw_bytes_path() {
508 let cache = MokaCache::new(16, None);
513 let json = serde_json::to_vec(&b"hello".to_vec()).expect("Vec<u8> serializes");
514 cache.insert_value("banner", Arc::new(crate::cache::RawCacheBytes(json)));
515 let kv = CacheEdgeKv::new(Arc::new(cache));
516
517 assert_eq!(kv.get("banner"), Some(b"hello".to_vec()));
518 }
519
520 #[test]
521 fn debug_names_the_adapter_without_leaking_contents() {
522 let kv = CacheEdgeKv::new(cache_with("secret", b"s3cr3t"));
523 let rendered = format!("{kv:?}");
524
525 assert!(rendered.contains("CacheEdgeKv"), "{rendered}");
526 assert!(!rendered.contains("s3cr3t"), "{rendered}");
527 }
528}