use std::collections::HashMap;
use std::sync::Arc;
use rainier_http::Request;
use rainier_session::SessionRequestExt;
use rainier_support::{BoxFuture, Error, Result};
use crate::abilities::Abilities;
use crate::session::AUTH_KEY;
use crate::user::{Authenticatable, Credentials, UserProvider};
pub trait Guard<U: Authenticatable>: Send + Sync + 'static {
fn name(&self) -> &str;
fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>>;
fn abilities<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Abilities>> {
let _ = request;
Box::pin(async { Ok(Abilities::everything()) })
}
}
pub trait StatefulGuard<U: Authenticatable>: Guard<U> {
fn attempt<'a>(
&'a self,
request: &'a Request,
credentials: &'a Credentials,
) -> BoxFuture<'a, Result<Option<U>>>;
fn login<'a>(&'a self, request: &'a Request, user: &'a U) -> BoxFuture<'a, Result<()>>;
fn logout<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<()>>;
}
#[async_trait::async_trait]
pub trait GuardExt<U: Authenticatable>: Guard<U> {
async fn check(&self, request: &Request) -> Result<bool> {
Ok(self.user(request).await?.is_some())
}
async fn guest(&self, request: &Request) -> Result<bool> {
Ok(!self.check(request).await?)
}
async fn id(&self, request: &Request) -> Result<Option<String>> {
Ok(self.user(request).await?.map(|user| user.auth_identifier()))
}
async fn authenticate(&self, request: &Request) -> Result<U> {
self.user(request).await?.ok_or_else(|| Error::unauthenticated("Unauthenticated."))
}
}
impl<U: Authenticatable, G: Guard<U> + ?Sized> GuardExt<U> for G {}
pub struct TokenGuard<U: Authenticatable> {
name: String,
provider: Arc<dyn UserProvider<U>>,
}
impl<U: Authenticatable> TokenGuard<U> {
pub fn new(name: impl Into<String>, provider: Arc<dyn UserProvider<U>>) -> Self {
Self { name: name.into(), provider }
}
}
impl<U: Authenticatable> Guard<U> for TokenGuard<U> {
fn name(&self) -> &str {
&self.name
}
fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>> {
Box::pin(async move {
let token =
request.bearer_token().map(str::to_string).or_else(|| request.input("api_token"));
match token {
Some(token) if !token.is_empty() => self.provider.retrieve_by_token(&token).await,
_ => Ok(None),
}
})
}
fn abilities<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Abilities>> {
Box::pin(async move {
let token =
request.bearer_token().map(str::to_string).or_else(|| request.input("api_token"));
match token {
Some(token) if !token.is_empty() => {
self.provider.retrieve_abilities_by_token(&token).await
}
_ => Ok(Abilities::none()),
}
})
}
}
pub struct SessionGuard<U: Authenticatable> {
name: String,
provider: Arc<dyn UserProvider<U>>,
}
impl<U: Authenticatable> SessionGuard<U> {
pub fn new(name: impl Into<String>, provider: Arc<dyn UserProvider<U>>) -> Self {
Self { name: name.into(), provider }
}
pub fn provider(&self) -> &Arc<dyn UserProvider<U>> {
&self.provider
}
}
impl<U: Authenticatable> Guard<U> for SessionGuard<U> {
fn name(&self) -> &str {
&self.name
}
fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>> {
Box::pin(async move {
let Some(session) = request.session() else {
tracing::debug!(
guard = %self.name,
"a session guard ran on a route with no session middleware"
);
return Ok(None);
};
let Some(user_id) = session.string(AUTH_KEY) else {
return Ok(None);
};
self.provider.retrieve_by_id(&user_id).await
})
}
}
impl<U: Authenticatable> StatefulGuard<U> for SessionGuard<U> {
fn attempt<'a>(
&'a self,
request: &'a Request,
credentials: &'a Credentials,
) -> BoxFuture<'a, Result<Option<U>>> {
Box::pin(async move {
let Some(user) = self.provider.retrieve_by_credentials(credentials).await? else {
return Ok(None);
};
if !self.provider.validate_credentials(&user, credentials).await? {
return Ok(None);
}
self.login(request, &user).await?;
Ok(Some(user))
})
}
fn login<'a>(&'a self, request: &'a Request, user: &'a U) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
let session = request
.session()
.ok_or_else(|| Error::internal("logging in needs the session middleware"))?;
session.regenerate();
session.put(AUTH_KEY, user.auth_identifier())?;
Ok(())
})
}
fn logout<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
if let Some(session) = request.session() {
session.invalidate();
}
Ok(())
})
}
}
pub struct AuthManager<U: Authenticatable> {
guards: HashMap<String, Arc<dyn Guard<U>>>,
default: String,
}
impl<U: Authenticatable> AuthManager<U> {
pub fn new(default: impl Into<String>) -> Self {
Self { guards: HashMap::new(), default: default.into() }
}
pub fn register(mut self, guard: Arc<dyn Guard<U>>) -> Self {
self.guards.insert(guard.name().to_string(), guard);
self
}
pub fn guard(&self, name: &str) -> Result<&Arc<dyn Guard<U>>> {
self.guards.get(name).ok_or_else(|| {
Error::internal(format!(
"no authentication guard named `{name}` — registered guards: {:?}",
self.guard_names()
))
})
}
pub fn default_guard(&self) -> Result<&Arc<dyn Guard<U>>> {
self.guard(&self.default)
}
pub fn default_name(&self) -> &str {
&self.default
}
pub fn guard_names(&self) -> Vec<&str> {
let mut names: Vec<&str> = self.guards.keys().map(String::as_str).collect();
names.sort();
names
}
pub async fn user(&self, request: &Request) -> Result<Option<U>> {
self.default_guard()?.user(request).await
}
pub async fn user_via(&self, name: &str, request: &Request) -> Result<Option<U>> {
self.guard(name)?.user(request).await
}
pub async fn abilities(&self, request: &Request) -> Result<Abilities> {
self.default_guard()?.abilities(request).await
}
pub async fn abilities_via(&self, name: &str, request: &Request) -> Result<Abilities> {
self.guard(name)?.abilities(request).await
}
}
impl<U: Authenticatable> std::fmt::Debug for AuthManager<U> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthManager")
.field("default", &self.default)
.field("guards", &self.guard_names())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::Session;
use rainier_support::Result as R;
#[derive(Debug, Clone, PartialEq)]
struct User {
id: u64,
email: String,
password: String,
token: Option<String>,
}
impl Authenticatable for User {
fn auth_identifier(&self) -> String {
self.id.to_string()
}
fn auth_password_hash(&self) -> Option<&str> {
Some(&self.password)
}
}
fn ada() -> User {
User {
id: 42,
email: "ada@example.com".into(),
password: "plain:secret".into(),
token: Some("tok_123".into()),
}
}
struct FakeProvider {
user: Option<User>,
fail: bool,
}
impl FakeProvider {
fn holding(user: User) -> Arc<dyn UserProvider<User>> {
Arc::new(Self { user: Some(user), fail: false })
}
fn empty() -> Arc<dyn UserProvider<User>> {
Arc::new(Self { user: None, fail: false })
}
fn broken() -> Arc<dyn UserProvider<User>> {
Arc::new(Self { user: None, fail: true })
}
}
#[async_trait::async_trait]
impl UserProvider<User> for FakeProvider {
async fn retrieve_by_id(&self, id: &str) -> R<Option<User>> {
if self.fail {
return Err(Error::internal("database is down"));
}
Ok(self.user.clone().filter(|u| u.auth_identifier() == id))
}
async fn retrieve_by_credentials(&self, credentials: &Credentials) -> R<Option<User>> {
if self.fail {
return Err(Error::internal("database is down"));
}
Ok(self.user.clone().filter(|u| credentials.get("email") == Some(&u.email)))
}
async fn validate_credentials(&self, user: &User, credentials: &Credentials) -> R<bool> {
Ok(credentials.password_value().is_some_and(|p| user.password == format!("plain:{p}")))
}
async fn retrieve_by_token(&self, token: &str) -> R<Option<User>> {
if self.fail {
return Err(Error::internal("database is down"));
}
Ok(self.user.clone().filter(|u| u.token.as_deref() == Some(token)))
}
}
fn bearer(token: &str) -> Request {
Request::builder().header("authorization", &format!("Bearer {token}")).build()
}
#[tokio::test]
async fn a_token_guard_resolves_a_bearer_token() {
let guard = TokenGuard::new("api", FakeProvider::holding(ada()));
let user = guard.user(&bearer("tok_123")).await.unwrap();
assert_eq!(user.unwrap().id, 42);
assert!(guard.check(&bearer("tok_123")).await.unwrap());
assert_eq!(guard.id(&bearer("tok_123")).await.unwrap().as_deref(), Some("42"));
}
#[tokio::test]
async fn a_token_guard_rejects_an_unknown_or_absent_token() {
let guard = TokenGuard::new("api", FakeProvider::holding(ada()));
assert!(guard.user(&bearer("wrong")).await.unwrap().is_none());
assert!(guard.user(&Request::builder().build()).await.unwrap().is_none());
assert!(guard.guest(&Request::builder().build()).await.unwrap());
}
#[tokio::test]
async fn a_token_guard_also_accepts_an_api_token_input() {
let guard = TokenGuard::new("api", FakeProvider::holding(ada()));
let request = Request::builder().uri("/x?api_token=tok_123").build();
assert!(guard.check(&request).await.unwrap());
}
#[tokio::test]
async fn authenticate_fails_with_a_401_for_a_guest() {
let guard = TokenGuard::new("api", FakeProvider::empty());
let err = guard.authenticate(&Request::builder().build()).await.unwrap_err();
assert_eq!(err.status(), 401);
}
#[tokio::test]
async fn a_backend_failure_is_an_error_not_a_silent_logout() {
let guard = TokenGuard::new("api", FakeProvider::broken());
assert!(guard.user(&bearer("tok_123")).await.is_err());
assert!(guard.check(&bearer("tok_123")).await.is_err());
}
fn session_guard() -> SessionGuard<User> {
SessionGuard::new("web", FakeProvider::holding(ada()))
}
fn with_session() -> Request {
Request::builder().build().with_extension(Session::new())
}
#[tokio::test]
async fn a_session_guard_logs_in_and_resolves_the_user() {
let guard = session_guard();
let request = with_session();
let user = guard
.attempt(&request, &Credentials::password("ada@example.com", "secret"))
.await
.unwrap()
.expect("correct credentials should log in");
assert_eq!(user.id, 42);
assert_eq!(guard.user(&request).await.unwrap().unwrap().id, 42);
}
#[tokio::test]
async fn logging_in_rotates_the_session_id() {
let guard = session_guard();
let request = with_session();
let before = request.session().unwrap().id();
guard.login(&request, &ada()).await.unwrap();
assert_ne!(request.session().unwrap().id(), before);
}
#[tokio::test]
async fn a_session_guard_rejects_a_wrong_password() {
let guard = session_guard();
let request = with_session();
assert!(guard
.attempt(&request, &Credentials::password("ada@example.com", "wrong"))
.await
.unwrap()
.is_none());
assert!(guard.guest(&request).await.unwrap(), "a failed attempt must not log anyone in");
}
#[tokio::test]
async fn a_session_guard_rejects_an_unknown_user() {
let guard = session_guard();
let request = with_session();
assert!(guard
.attempt(&request, &Credentials::password("nobody@example.com", "secret"))
.await
.unwrap()
.is_none());
}
#[tokio::test]
async fn logging_out_empties_the_session_and_rotates_it() {
let guard = session_guard();
let request = with_session();
guard.login(&request, &ada()).await.unwrap();
let before = request.session().unwrap().id();
request.session().unwrap().put("cart", vec![1u64]).unwrap();
assert!(guard.check(&request).await.unwrap());
guard.logout(&request).await.unwrap();
assert!(guard.guest(&request).await.unwrap());
assert!(
!request.session().unwrap().has("cart"),
"a logout must not leave the next person at this browser the cart"
);
assert_ne!(request.session().unwrap().id(), before);
}
#[tokio::test]
async fn a_request_with_no_session_is_a_guest_rather_than_an_error() {
let guard = session_guard();
assert!(guard.guest(&Request::builder().build()).await.unwrap());
}
#[tokio::test]
async fn an_empty_session_is_a_guest() {
let guard = session_guard();
assert!(guard.guest(&with_session()).await.unwrap());
}
#[tokio::test]
async fn logging_out_without_a_session_is_not_an_error() {
let guard = session_guard();
assert!(guard.logout(&Request::builder().build()).await.is_ok());
}
#[tokio::test]
async fn logging_in_without_a_session_says_what_is_missing() {
let guard = session_guard();
let err = guard.login(&Request::builder().build(), &ada()).await.unwrap_err();
assert!(err.message().contains("session middleware"), "{}", err.message());
}
#[tokio::test]
async fn the_manager_resolves_guards_by_name() {
let manager = AuthManager::<User>::new("web")
.register(Arc::new(session_guard()))
.register(Arc::new(TokenGuard::new("api", FakeProvider::holding(ada()))));
assert_eq!(manager.guard_names(), vec!["api", "web"]);
assert_eq!(manager.default_name(), "web");
assert_eq!(manager.default_guard().unwrap().name(), "web");
assert_eq!(manager.guard("api").unwrap().name(), "api");
}
#[tokio::test]
async fn an_unknown_guard_name_lists_the_known_ones() {
let manager = AuthManager::<User>::new("web").register(Arc::new(session_guard()));
let err = manager.guard("nope").err().expect("should fail");
assert!(err.message().contains("`nope`"), "{}", err.message());
assert!(err.message().contains("web"), "{}", err.message());
}
#[tokio::test]
async fn the_manager_authenticates_through_the_chosen_guard() {
let manager = AuthManager::<User>::new("api")
.register(Arc::new(TokenGuard::new("api", FakeProvider::holding(ada()))));
let request = bearer("tok_123");
assert_eq!(manager.user(&request).await.unwrap().unwrap().id, 42);
assert_eq!(manager.user_via("api", &request).await.unwrap().unwrap().id, 42);
}
#[tokio::test]
async fn a_missing_default_guard_is_reported_when_used() {
let manager = AuthManager::<User>::new("web");
assert!(manager.user(&Request::builder().build()).await.is_err());
}
}