use std::sync::Arc;
use rainier_http::{FromRequest, IntoResponse, Request, Response};
use rainier_middleware::{Middleware, MiddlewareStack, Next};
use rainier_support::{BoxedFuture, Error, Result};
use crate::abilities::Abilities;
use crate::guard::AuthManager;
use crate::user::Authenticatable;
#[derive(Debug)]
pub struct AuthenticatedUser<U>(pub Arc<U>);
impl<U> Clone for AuthenticatedUser<U> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<U> AuthenticatedUser<U> {
pub fn get(&self) -> &U {
&self.0
}
}
impl<U> std::ops::Deref for AuthenticatedUser<U> {
type Target = U;
fn deref(&self) -> &U {
&self.0
}
}
impl<U: Send + Sync + 'static> FromRequest for AuthenticatedUser<U> {
fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
Box::pin(async move {
request
.extension::<AuthenticatedUser<U>>()
.cloned()
.ok_or_else(|| Error::unauthenticated("Unauthenticated."))
})
}
}
pub struct Authenticate<U: Authenticatable> {
auth: Arc<AuthManager<U>>,
guard: Option<String>,
}
impl<U: Authenticatable> Authenticate<U> {
pub fn new(auth: Arc<AuthManager<U>>) -> Self {
Self { auth, guard: None }
}
pub fn with_guard(auth: Arc<AuthManager<U>>, guard: impl Into<String>) -> Self {
Self { auth, guard: Some(guard.into()) }
}
pub fn resolved() -> MiddlewareStack {
MiddlewareStack::new().resolved(|auth: Arc<AuthManager<U>>| Self::new(auth))
}
pub fn resolved_with_guard(guard: impl Into<String>) -> MiddlewareStack {
let guard = guard.into();
MiddlewareStack::new()
.resolved(move |auth: Arc<AuthManager<U>>| Self::with_guard(auth, guard.clone()))
}
}
#[async_trait::async_trait]
impl<U: Authenticatable> Middleware for Authenticate<U> {
async fn handle(&self, mut request: Request, next: Next) -> Response {
let resolved = match &self.guard {
Some(name) => self.auth.user_via(name, &request).await,
None => self.auth.user(&request).await,
};
match resolved {
Ok(Some(user)) => {
let abilities = match &self.guard {
Some(name) => self.auth.abilities_via(name, &request).await,
None => self.auth.abilities(&request).await,
};
match abilities {
Ok(abilities) => {
request.extensions_mut().insert(TokenAbilities(abilities));
}
Err(e) => {
tracing::error!(error = %e, "could not read the token's abilities");
return e.into_response();
}
}
request.extensions_mut().insert(AuthenticatedUser(Arc::new(user)));
next.run(request).await
}
Ok(None) => Error::unauthenticated("Unauthenticated.").into_response(),
Err(e) => {
tracing::error!(error = %e, "the authentication guard failed");
e.into_response()
}
}
}
fn name(&self) -> &'static str {
"Authenticate"
}
}
#[derive(Debug, Clone)]
pub struct TokenAbilities(pub Abilities);
impl std::ops::Deref for TokenAbilities {
type Target = Abilities;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait::async_trait]
impl FromRequest for TokenAbilities {
fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
Box::pin(async move {
request.extension::<TokenAbilities>().cloned().ok_or_else(|| {
Error::internal(
"no token abilities on the request — is this route behind the `auth` \
middleware?",
)
})
})
}
}
pub trait AbilitiesRequestExt {
fn token_abilities(&self) -> Option<&Abilities>;
fn token_can(&self, ability: &str) -> bool;
}
impl AbilitiesRequestExt for Request {
fn token_abilities(&self) -> Option<&Abilities> {
self.extension::<TokenAbilities>().map(|abilities| &abilities.0)
}
fn token_can(&self, ability: &str) -> bool {
self.token_abilities().is_some_and(|abilities| abilities.can(ability))
}
}
#[derive(Debug, Clone)]
pub struct RequireAbility {
required: Vec<String>,
all: bool,
}
impl RequireAbility {
pub fn any(abilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self { required: abilities.into_iter().map(Into::into).collect(), all: false }
}
pub fn all(abilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self { required: abilities.into_iter().map(Into::into).collect(), all: true }
}
pub fn required(&self) -> &[String] {
&self.required
}
fn satisfied_by(&self, abilities: &Abilities) -> bool {
let required = self.required.iter().map(String::as_str);
if self.all {
abilities.can_all(required)
} else {
abilities.can_any(required)
}
}
}
#[async_trait::async_trait]
impl Middleware for RequireAbility {
async fn handle(&self, request: Request, next: Next) -> Response {
let Some(abilities) = request.token_abilities() else {
tracing::error!(
required = ?self.required,
"`RequireAbility` ran without `Authenticate` before it, so the request has no \
abilities to check — refusing"
);
return Error::unauthorized("This action is unauthorized.").into_response();
};
if !self.satisfied_by(abilities) {
return Error::unauthorized(format!(
"This token is not allowed to {}.",
self.required.join(" or ")
))
.into_response();
}
next.run(request).await
}
fn name(&self) -> &'static str {
"RequireAbility"
}
}
pub struct RedirectIfAuthenticated<U: Authenticatable> {
auth: Arc<AuthManager<U>>,
to: String,
}
impl<U: Authenticatable> RedirectIfAuthenticated<U> {
pub fn new(auth: Arc<AuthManager<U>>, to: impl Into<String>) -> Self {
Self { auth, to: to.into() }
}
}
#[async_trait::async_trait]
impl<U: Authenticatable> Middleware for RedirectIfAuthenticated<U> {
async fn handle(&self, request: Request, next: Next) -> Response {
match self.auth.user(&request).await {
Ok(Some(_)) => rainier_http::Redirect::to(self.to.clone()).into_response(),
Ok(None) | Err(_) => next.run(request).await,
}
}
fn name(&self) -> &'static str {
"RedirectIfAuthenticated"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::guard::TokenGuard;
use crate::user::{Credentials, UserProvider};
use rainier_http::StatusCode;
use rainier_middleware::Pipeline;
use rainier_support::Result;
#[derive(Debug, Clone, PartialEq)]
struct User {
id: u64,
}
impl Authenticatable for User {
fn auth_identifier(&self) -> String {
self.id.to_string()
}
fn auth_password_hash(&self) -> Option<&str> {
None
}
}
struct Provider {
fail: bool,
}
#[async_trait::async_trait]
impl UserProvider<User> for Provider {
async fn retrieve_by_id(&self, _: &str) -> Result<Option<User>> {
Ok(None)
}
async fn retrieve_by_credentials(&self, _: &Credentials) -> Result<Option<User>> {
Ok(None)
}
async fn validate_credentials(&self, _: &User, _: &Credentials) -> Result<bool> {
Ok(false)
}
async fn retrieve_by_token(&self, token: &str) -> Result<Option<User>> {
if self.fail {
return Err(Error::internal("database is down"));
}
Ok((token == "good").then_some(User { id: 7 }))
}
}
fn manager(fail: bool) -> Arc<AuthManager<User>> {
Arc::new(
AuthManager::new("api")
.register(Arc::new(TokenGuard::new("api", Arc::new(Provider { fail })))),
)
}
async fn reveal_user(request: Request) -> Response {
match request.extension::<AuthenticatedUser<User>>() {
Some(user) => Response::text(user.id.to_string()),
None => Response::text("anonymous"),
}
}
async fn body_of(response: Response) -> String {
String::from_utf8(response.into_http().into_body().collect().await.unwrap().to_vec())
.unwrap()
}
fn bearer(token: &str) -> Request {
Request::builder().header("authorization", &format!("Bearer {token}")).build()
}
#[tokio::test]
async fn an_authenticated_request_reaches_the_handler_with_its_user() {
let response = Pipeline::new()
.through(Authenticate::new(manager(false)))
.then(reveal_user)
.run(bearer("good"))
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(body_of(response).await, "7");
}
#[tokio::test]
async fn an_unauthenticated_request_is_rejected_before_the_handler() {
let response = Pipeline::new()
.through(Authenticate::new(manager(false)))
.then(|_: Request| async { panic!("the handler must not run") })
.run(bearer("bad"))
.await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn a_guard_failure_is_a_500_not_a_401() {
let response = Pipeline::new()
.through(Authenticate::new(manager(true)))
.then(reveal_user)
.run(bearer("good"))
.await;
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn a_named_guard_can_be_selected() {
let middleware = Authenticate::with_guard(manager(false), "api");
let response =
Pipeline::new().through(middleware).then(reveal_user).run(bearer("good")).await;
assert_eq!(body_of(response).await, "7");
}
#[tokio::test]
async fn an_unknown_named_guard_fails_loudly() {
let middleware = Authenticate::with_guard(manager(false), "nope");
let response =
Pipeline::new().through(middleware).then(reveal_user).run(bearer("good")).await;
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn the_guest_middleware_redirects_an_authenticated_user() {
let response = Pipeline::new()
.through(RedirectIfAuthenticated::new(manager(false), "/dashboard"))
.then(|_: Request| async { Response::text("login form") })
.run(bearer("good"))
.await;
assert_eq!(response.status(), StatusCode::FOUND);
assert_eq!(response.header("location"), Some("/dashboard"));
}
#[tokio::test]
async fn the_guest_middleware_lets_a_guest_through() {
let response = Pipeline::new()
.through(RedirectIfAuthenticated::new(manager(false), "/dashboard"))
.then(|_: Request| async { Response::text("login form") })
.run(Request::builder().build())
.await;
assert_eq!(body_of(response).await, "login form");
}
#[tokio::test]
async fn the_guest_middleware_fails_open_to_the_login_page() {
let response = Pipeline::new()
.through(RedirectIfAuthenticated::new(manager(true), "/dashboard"))
.then(|_: Request| async { Response::text("login form") })
.run(bearer("good"))
.await;
assert_eq!(body_of(response).await, "login form");
}
}