//! # Authentication middleware use std::future::{Future, ready, Ready}; use std::pin::Pin; use std::rc::Rc; use actix_identity::RequestIdentity; use actix_web::{ dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpResponse, web, }; use actix_web::body::EitherBody; use actix_web::http::{header, Method}; use askama::Template; use crate::constants::{ADMIN_ROUTES, AUTHENTICATED_ROUTES, AUTHORIZE_URI}; use crate::controllers::base_controller::{FatalErrorPage, redirect_user_for_login}; use crate::data::app_config::AppConfig; use crate::data::session_identity::{SessionIdentity, SessionIdentityData, SessionStatus}; // There are two steps in middleware processing. // 1. Middleware initialization, middleware factory gets called with // next service in chain as parameter. // 2. Middleware's call method gets called with normal request. pub struct AuthMiddleware; // Middleware factory is `Transform` trait // `S` - type of the next service // `B` - type of response's body impl Transform for AuthMiddleware where S: Service, Error=Error> + 'static, S::Future: 'static, B: 'static, { type Response = ServiceResponse>; type Error = Error; type Transform = AuthInnerMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ready(Ok(AuthInnerMiddleware { service: Rc::new(service), })) } } #[derive(Debug)] enum ConnStatus { SignedOut, RegularUser, Admin, } impl ConnStatus { pub fn is_auth(&self) -> bool { !matches!(self, ConnStatus::SignedOut) } pub fn is_admin(&self) -> bool { matches!(self, ConnStatus::Admin) } } pub struct AuthInnerMiddleware { service: Rc, } impl Service for AuthInnerMiddleware where S: Service, Error=Error> + 'static, S::Future: 'static, B: 'static, { type Response = ServiceResponse>; type Error = Error; #[allow(clippy::type_complexity)] type Future = Pin>>>; forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let service = Rc::clone(&self.service); // Forward request Box::pin(async move { let config: &web::Data = req.app_data().expect("AppData undefined!"); // Check if POST request comes from another website (block invalid origins) let origin = req.headers().get(header::ORIGIN); if req.method() == Method::POST { if let Some(o) = origin { if !o.to_str().unwrap_or("bad").eq(&config.website_origin) { log::warn!( "Blocked POST request from invalid origin! Origin given {:?}", o ); return Ok(req.into_response( HttpResponse::Unauthorized() .body("POST request from invalid origin!") .map_into_right_body(), )); } } } if req.path().starts_with("/.git") { return Ok(req.into_response( HttpResponse::Unauthorized() .body("Hey don't touch this!") .map_into_right_body(), )); } let session = match SessionIdentity::deserialize_session_data(req.get_identity()) { Some(SessionIdentityData { status: SessionStatus::SignedIn, is_admin: true, .. }) => ConnStatus::Admin, Some(SessionIdentityData { status: SessionStatus::SignedIn, .. }) => ConnStatus::RegularUser, _ => ConnStatus::SignedOut, }; // Redirect user to login page if !session.is_auth() && (req.path().starts_with(ADMIN_ROUTES) || req.path().starts_with(AUTHENTICATED_ROUTES) || req.path().eq(AUTHORIZE_URI)) { let path = req.uri().to_string(); return Ok(req .into_response(redirect_user_for_login(path)) .map_into_right_body()); } // Restrict access to admin pages if !session.is_admin() && req.path().starts_with(ADMIN_ROUTES) { return Ok(req .into_response( HttpResponse::Unauthorized() .body(FatalErrorPage { message: "You are not allowed to access this resource." }.render().unwrap()), ) .map_into_right_body()); } service .call(req) .await .map(ServiceResponse::map_into_left_body) }) } }