1use std::collections::HashMap;
15use std::sync::Arc;
16
17use rainier_http::Request;
18use rainier_session::SessionRequestExt;
19use rainier_support::{BoxFuture, Error, Result};
20
21use crate::abilities::Abilities;
22use crate::session::AUTH_KEY;
23use crate::user::{Authenticatable, Credentials, UserProvider};
24
25pub trait Guard<U: Authenticatable>: Send + Sync + 'static {
27 fn name(&self) -> &str;
29
30 fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>>;
36
37 fn abilities<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Abilities>> {
43 let _ = request;
44 Box::pin(async { Ok(Abilities::everything()) })
45 }
46}
47
48pub trait StatefulGuard<U: Authenticatable>: Guard<U> {
56 fn attempt<'a>(
59 &'a self,
60 request: &'a Request,
61 credentials: &'a Credentials,
62 ) -> BoxFuture<'a, Result<Option<U>>>;
63
64 fn login<'a>(&'a self, request: &'a Request, user: &'a U) -> BoxFuture<'a, Result<()>>;
66
67 fn logout<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<()>>;
69}
70
71#[async_trait::async_trait]
74pub trait GuardExt<U: Authenticatable>: Guard<U> {
75 async fn check(&self, request: &Request) -> Result<bool> {
77 Ok(self.user(request).await?.is_some())
78 }
79
80 async fn guest(&self, request: &Request) -> Result<bool> {
82 Ok(!self.check(request).await?)
83 }
84
85 async fn id(&self, request: &Request) -> Result<Option<String>> {
87 Ok(self.user(request).await?.map(|user| user.auth_identifier()))
88 }
89
90 async fn authenticate(&self, request: &Request) -> Result<U> {
92 self.user(request).await?.ok_or_else(|| Error::unauthenticated("Unauthenticated."))
93 }
94}
95
96impl<U: Authenticatable, G: Guard<U> + ?Sized> GuardExt<U> for G {}
97
98pub struct TokenGuard<U: Authenticatable> {
103 name: String,
104 provider: Arc<dyn UserProvider<U>>,
105}
106
107impl<U: Authenticatable> TokenGuard<U> {
108 pub fn new(name: impl Into<String>, provider: Arc<dyn UserProvider<U>>) -> Self {
110 Self { name: name.into(), provider }
111 }
112}
113
114impl<U: Authenticatable> Guard<U> for TokenGuard<U> {
115 fn name(&self) -> &str {
116 &self.name
117 }
118
119 fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>> {
120 Box::pin(async move {
121 let token =
124 request.bearer_token().map(str::to_string).or_else(|| request.input("api_token"));
125
126 match token {
127 Some(token) if !token.is_empty() => self.provider.retrieve_by_token(&token).await,
128 _ => Ok(None),
129 }
130 })
131 }
132
133 fn abilities<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Abilities>> {
134 Box::pin(async move {
135 let token =
136 request.bearer_token().map(str::to_string).or_else(|| request.input("api_token"));
137
138 match token {
139 Some(token) if !token.is_empty() => {
140 self.provider.retrieve_abilities_by_token(&token).await
141 }
142 _ => Ok(Abilities::none()),
145 }
146 })
147 }
148}
149
150pub struct SessionGuard<U: Authenticatable> {
156 name: String,
157 provider: Arc<dyn UserProvider<U>>,
158}
159
160impl<U: Authenticatable> SessionGuard<U> {
161 pub fn new(name: impl Into<String>, provider: Arc<dyn UserProvider<U>>) -> Self {
163 Self { name: name.into(), provider }
164 }
165
166 pub fn provider(&self) -> &Arc<dyn UserProvider<U>> {
168 &self.provider
169 }
170}
171
172impl<U: Authenticatable> Guard<U> for SessionGuard<U> {
173 fn name(&self) -> &str {
174 &self.name
175 }
176
177 fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>> {
178 Box::pin(async move {
179 let Some(session) = request.session() else {
180 tracing::debug!(
184 guard = %self.name,
185 "a session guard ran on a route with no session middleware"
186 );
187 return Ok(None);
188 };
189
190 let Some(user_id) = session.string(AUTH_KEY) else {
191 return Ok(None);
192 };
193
194 self.provider.retrieve_by_id(&user_id).await
195 })
196 }
197}
198
199impl<U: Authenticatable> StatefulGuard<U> for SessionGuard<U> {
200 fn attempt<'a>(
201 &'a self,
202 request: &'a Request,
203 credentials: &'a Credentials,
204 ) -> BoxFuture<'a, Result<Option<U>>> {
205 Box::pin(async move {
206 let Some(user) = self.provider.retrieve_by_credentials(credentials).await? else {
207 return Ok(None);
208 };
209 if !self.provider.validate_credentials(&user, credentials).await? {
210 return Ok(None);
211 }
212
213 self.login(request, &user).await?;
214 Ok(Some(user))
215 })
216 }
217
218 fn login<'a>(&'a self, request: &'a Request, user: &'a U) -> BoxFuture<'a, Result<()>> {
219 Box::pin(async move {
220 let session = request
221 .session()
222 .ok_or_else(|| Error::internal("logging in needs the session middleware"))?;
223
224 session.regenerate();
228 session.put(AUTH_KEY, user.auth_identifier())?;
229 Ok(())
230 })
231 }
232
233 fn logout<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<()>> {
234 Box::pin(async move {
235 if let Some(session) = request.session() {
236 session.invalidate();
240 }
241 Ok(())
242 })
243 }
244}
245
246pub struct AuthManager<U: Authenticatable> {
252 guards: HashMap<String, Arc<dyn Guard<U>>>,
253 default: String,
254}
255
256impl<U: Authenticatable> AuthManager<U> {
257 pub fn new(default: impl Into<String>) -> Self {
259 Self { guards: HashMap::new(), default: default.into() }
260 }
261
262 pub fn register(mut self, guard: Arc<dyn Guard<U>>) -> Self {
264 self.guards.insert(guard.name().to_string(), guard);
265 self
266 }
267
268 pub fn guard(&self, name: &str) -> Result<&Arc<dyn Guard<U>>> {
270 self.guards.get(name).ok_or_else(|| {
271 Error::internal(format!(
272 "no authentication guard named `{name}` — registered guards: {:?}",
273 self.guard_names()
274 ))
275 })
276 }
277
278 pub fn default_guard(&self) -> Result<&Arc<dyn Guard<U>>> {
280 self.guard(&self.default)
281 }
282
283 pub fn default_name(&self) -> &str {
285 &self.default
286 }
287
288 pub fn guard_names(&self) -> Vec<&str> {
290 let mut names: Vec<&str> = self.guards.keys().map(String::as_str).collect();
291 names.sort();
292 names
293 }
294
295 pub async fn user(&self, request: &Request) -> Result<Option<U>> {
297 self.default_guard()?.user(request).await
298 }
299
300 pub async fn user_via(&self, name: &str, request: &Request) -> Result<Option<U>> {
302 self.guard(name)?.user(request).await
303 }
304
305 pub async fn abilities(&self, request: &Request) -> Result<Abilities> {
307 self.default_guard()?.abilities(request).await
308 }
309
310 pub async fn abilities_via(&self, name: &str, request: &Request) -> Result<Abilities> {
312 self.guard(name)?.abilities(request).await
313 }
314}
315
316impl<U: Authenticatable> std::fmt::Debug for AuthManager<U> {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.debug_struct("AuthManager")
319 .field("default", &self.default)
320 .field("guards", &self.guard_names())
321 .finish()
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::session::Session;
329 use rainier_support::Result as R;
330
331 #[derive(Debug, Clone, PartialEq)]
332 struct User {
333 id: u64,
334 email: String,
335 password: String,
336 token: Option<String>,
337 }
338
339 impl Authenticatable for User {
340 fn auth_identifier(&self) -> String {
341 self.id.to_string()
342 }
343 fn auth_password_hash(&self) -> Option<&str> {
344 Some(&self.password)
345 }
346 }
347
348 fn ada() -> User {
349 User {
350 id: 42,
351 email: "ada@example.com".into(),
352 password: "plain:secret".into(),
353 token: Some("tok_123".into()),
354 }
355 }
356
357 struct FakeProvider {
360 user: Option<User>,
361 fail: bool,
362 }
363
364 impl FakeProvider {
365 fn holding(user: User) -> Arc<dyn UserProvider<User>> {
366 Arc::new(Self { user: Some(user), fail: false })
367 }
368 fn empty() -> Arc<dyn UserProvider<User>> {
369 Arc::new(Self { user: None, fail: false })
370 }
371 fn broken() -> Arc<dyn UserProvider<User>> {
372 Arc::new(Self { user: None, fail: true })
373 }
374 }
375
376 #[async_trait::async_trait]
377 impl UserProvider<User> for FakeProvider {
378 async fn retrieve_by_id(&self, id: &str) -> R<Option<User>> {
379 if self.fail {
380 return Err(Error::internal("database is down"));
381 }
382 Ok(self.user.clone().filter(|u| u.auth_identifier() == id))
383 }
384
385 async fn retrieve_by_credentials(&self, credentials: &Credentials) -> R<Option<User>> {
386 if self.fail {
387 return Err(Error::internal("database is down"));
388 }
389 Ok(self.user.clone().filter(|u| credentials.get("email") == Some(&u.email)))
390 }
391
392 async fn validate_credentials(&self, user: &User, credentials: &Credentials) -> R<bool> {
393 Ok(credentials.password_value().is_some_and(|p| user.password == format!("plain:{p}")))
394 }
395
396 async fn retrieve_by_token(&self, token: &str) -> R<Option<User>> {
397 if self.fail {
398 return Err(Error::internal("database is down"));
399 }
400 Ok(self.user.clone().filter(|u| u.token.as_deref() == Some(token)))
401 }
402 }
403
404 fn bearer(token: &str) -> Request {
405 Request::builder().header("authorization", &format!("Bearer {token}")).build()
406 }
407
408 #[tokio::test]
411 async fn a_token_guard_resolves_a_bearer_token() {
412 let guard = TokenGuard::new("api", FakeProvider::holding(ada()));
413
414 let user = guard.user(&bearer("tok_123")).await.unwrap();
415 assert_eq!(user.unwrap().id, 42);
416 assert!(guard.check(&bearer("tok_123")).await.unwrap());
417 assert_eq!(guard.id(&bearer("tok_123")).await.unwrap().as_deref(), Some("42"));
418 }
419
420 #[tokio::test]
421 async fn a_token_guard_rejects_an_unknown_or_absent_token() {
422 let guard = TokenGuard::new("api", FakeProvider::holding(ada()));
423
424 assert!(guard.user(&bearer("wrong")).await.unwrap().is_none());
425 assert!(guard.user(&Request::builder().build()).await.unwrap().is_none());
426 assert!(guard.guest(&Request::builder().build()).await.unwrap());
427 }
428
429 #[tokio::test]
430 async fn a_token_guard_also_accepts_an_api_token_input() {
431 let guard = TokenGuard::new("api", FakeProvider::holding(ada()));
432 let request = Request::builder().uri("/x?api_token=tok_123").build();
433
434 assert!(guard.check(&request).await.unwrap());
435 }
436
437 #[tokio::test]
438 async fn authenticate_fails_with_a_401_for_a_guest() {
439 let guard = TokenGuard::new("api", FakeProvider::empty());
440 let err = guard.authenticate(&Request::builder().build()).await.unwrap_err();
441 assert_eq!(err.status(), 401);
442 }
443
444 #[tokio::test]
445 async fn a_backend_failure_is_an_error_not_a_silent_logout() {
446 let guard = TokenGuard::new("api", FakeProvider::broken());
449 assert!(guard.user(&bearer("tok_123")).await.is_err());
450 assert!(guard.check(&bearer("tok_123")).await.is_err());
451 }
452
453 fn session_guard() -> SessionGuard<User> {
456 SessionGuard::new("web", FakeProvider::holding(ada()))
457 }
458
459 fn with_session() -> Request {
461 Request::builder().build().with_extension(Session::new())
462 }
463
464 #[tokio::test]
465 async fn a_session_guard_logs_in_and_resolves_the_user() {
466 let guard = session_guard();
467 let request = with_session();
468
469 let user = guard
470 .attempt(&request, &Credentials::password("ada@example.com", "secret"))
471 .await
472 .unwrap()
473 .expect("correct credentials should log in");
474
475 assert_eq!(user.id, 42);
476 assert_eq!(guard.user(&request).await.unwrap().unwrap().id, 42);
477 }
478
479 #[tokio::test]
480 async fn logging_in_rotates_the_session_id() {
481 let guard = session_guard();
484 let request = with_session();
485 let before = request.session().unwrap().id();
486
487 guard.login(&request, &ada()).await.unwrap();
488
489 assert_ne!(request.session().unwrap().id(), before);
490 }
491
492 #[tokio::test]
493 async fn a_session_guard_rejects_a_wrong_password() {
494 let guard = session_guard();
495 let request = with_session();
496
497 assert!(guard
498 .attempt(&request, &Credentials::password("ada@example.com", "wrong"))
499 .await
500 .unwrap()
501 .is_none());
502 assert!(guard.guest(&request).await.unwrap(), "a failed attempt must not log anyone in");
503 }
504
505 #[tokio::test]
506 async fn a_session_guard_rejects_an_unknown_user() {
507 let guard = session_guard();
508 let request = with_session();
509
510 assert!(guard
511 .attempt(&request, &Credentials::password("nobody@example.com", "secret"))
512 .await
513 .unwrap()
514 .is_none());
515 }
516
517 #[tokio::test]
518 async fn logging_out_empties_the_session_and_rotates_it() {
519 let guard = session_guard();
520 let request = with_session();
521 guard.login(&request, &ada()).await.unwrap();
522
523 let before = request.session().unwrap().id();
524 request.session().unwrap().put("cart", vec![1u64]).unwrap();
525
526 assert!(guard.check(&request).await.unwrap());
527 guard.logout(&request).await.unwrap();
528
529 assert!(guard.guest(&request).await.unwrap());
530 assert!(
531 !request.session().unwrap().has("cart"),
532 "a logout must not leave the next person at this browser the cart"
533 );
534 assert_ne!(request.session().unwrap().id(), before);
535 }
536
537 #[tokio::test]
538 async fn a_request_with_no_session_is_a_guest_rather_than_an_error() {
539 let guard = session_guard();
542 assert!(guard.guest(&Request::builder().build()).await.unwrap());
543 }
544
545 #[tokio::test]
546 async fn an_empty_session_is_a_guest() {
547 let guard = session_guard();
548 assert!(guard.guest(&with_session()).await.unwrap());
549 }
550
551 #[tokio::test]
552 async fn logging_out_without_a_session_is_not_an_error() {
553 let guard = session_guard();
554 assert!(guard.logout(&Request::builder().build()).await.is_ok());
555 }
556
557 #[tokio::test]
558 async fn logging_in_without_a_session_says_what_is_missing() {
559 let guard = session_guard();
560 let err = guard.login(&Request::builder().build(), &ada()).await.unwrap_err();
561
562 assert!(err.message().contains("session middleware"), "{}", err.message());
563 }
564
565 #[tokio::test]
568 async fn the_manager_resolves_guards_by_name() {
569 let manager = AuthManager::<User>::new("web")
570 .register(Arc::new(session_guard()))
571 .register(Arc::new(TokenGuard::new("api", FakeProvider::holding(ada()))));
572
573 assert_eq!(manager.guard_names(), vec!["api", "web"]);
574 assert_eq!(manager.default_name(), "web");
575 assert_eq!(manager.default_guard().unwrap().name(), "web");
576 assert_eq!(manager.guard("api").unwrap().name(), "api");
577 }
578
579 #[tokio::test]
580 async fn an_unknown_guard_name_lists_the_known_ones() {
581 let manager = AuthManager::<User>::new("web").register(Arc::new(session_guard()));
582
583 let err = manager.guard("nope").err().expect("should fail");
584 assert!(err.message().contains("`nope`"), "{}", err.message());
585 assert!(err.message().contains("web"), "{}", err.message());
586 }
587
588 #[tokio::test]
589 async fn the_manager_authenticates_through_the_chosen_guard() {
590 let manager = AuthManager::<User>::new("api")
591 .register(Arc::new(TokenGuard::new("api", FakeProvider::holding(ada()))));
592
593 let request = bearer("tok_123");
594 assert_eq!(manager.user(&request).await.unwrap().unwrap().id, 42);
595 assert_eq!(manager.user_via("api", &request).await.unwrap().unwrap().id, 42);
596 }
597
598 #[tokio::test]
599 async fn a_missing_default_guard_is_reported_when_used() {
600 let manager = AuthManager::<User>::new("web");
601 assert!(manager.user(&Request::builder().build()).await.is_err());
602 }
603}