From 6f12a1d80e6b3b757b5756805d9a1bc726900784 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:47:50 +0200 Subject: [PATCH] completely rebuilt the auth implementation to support OIDC in the future --- Cargo.lock | 21 +---- Cargo.toml | 1 - crates/auth/Cargo.toml | 12 --- crates/auth/src/error.rs | 36 -------- crates/auth/src/extractor.rs | 40 --------- crates/auth/src/htpasswd.rs | 49 ---------- crates/auth/src/lib.rs | 39 -------- crates/auth/src/none.rs | 21 ----- crates/caldav/Cargo.toml | 1 - .../caldav/src/calendar/methods/mkcalendar.rs | 8 +- .../caldav/src/calendar/methods/report/mod.rs | 9 +- crates/caldav/src/calendar_object/methods.rs | 19 ++-- crates/caldav/src/lib.rs | 42 ++++----- crates/carddav/Cargo.toml | 1 - crates/carddav/src/lib.rs | 9 +- crates/dav/Cargo.toml | 2 +- crates/dav/src/methods/delete.rs | 5 +- crates/dav/src/methods/propfind.rs | 8 +- crates/dav/src/methods/proppatch.rs | 8 +- crates/store/Cargo.toml | 5 +- crates/store/src/auth/middleware.rs | 90 +++++++++++++++++++ crates/store/src/auth/mod.rs | 16 ++++ crates/store/src/auth/static_user_store.rs | 43 +++++++++ crates/store/src/auth/user.rs | 27 ++++++ crates/store/src/auth/user_store.rs | 8 ++ crates/store/src/lib.rs | 2 +- src/app.rs | 21 ++--- src/config.rs | 18 +--- src/main.rs | 8 +- 29 files changed, 257 insertions(+), 312 deletions(-) delete mode 100644 crates/auth/Cargo.toml delete mode 100644 crates/auth/src/error.rs delete mode 100644 crates/auth/src/extractor.rs delete mode 100644 crates/auth/src/htpasswd.rs delete mode 100644 crates/auth/src/lib.rs delete mode 100644 crates/auth/src/none.rs create mode 100644 crates/store/src/auth/middleware.rs create mode 100644 crates/store/src/auth/mod.rs create mode 100644 crates/store/src/auth/static_user_store.rs create mode 100644 crates/store/src/auth/user.rs create mode 100644 crates/store/src/auth/user_store.rs diff --git a/Cargo.lock b/Cargo.lock index 0e0f290..662b739 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1934,7 +1934,6 @@ dependencies = [ "async-trait", "clap", "env_logger", - "rustical_auth", "rustical_caldav", "rustical_carddav", "rustical_frontend", @@ -1946,18 +1945,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "rustical_auth" -version = "0.1.0" -dependencies = [ - "actix-web", - "actix-web-httpauth", - "futures-util", - "password-auth", - "serde", - "thiserror", -] - [[package]] name = "rustical_caldav" version = "0.1.0" @@ -1971,7 +1958,6 @@ dependencies = [ "futures-util", "quick-xml", "roxmltree", - "rustical_auth", "rustical_dav", "rustical_store", "serde", @@ -1993,7 +1979,6 @@ dependencies = [ "futures-util", "quick-xml", "roxmltree", - "rustical_auth", "rustical_dav", "rustical_store", "serde", @@ -2016,7 +2001,7 @@ dependencies = [ "log", "quick-xml", "roxmltree", - "rustical_auth", + "rustical_store", "serde", "strum", "thiserror", @@ -2031,7 +2016,6 @@ dependencies = [ "anyhow", "askama", "askama_actix", - "rustical_auth", "rustical_store", "serde", "thiserror", @@ -2042,11 +2026,14 @@ dependencies = [ name = "rustical_store" version = "0.1.0" dependencies = [ + "actix-web", + "actix-web-httpauth", "anyhow", "async-trait", "chrono", "ical", "lazy_static", + "password-auth", "regex", "rstest", "rstest_reuse", diff --git a/Cargo.toml b/Cargo.toml index 43a4af4..4ade2a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ members = ["crates/*"] [dependencies] rustical_store = { path = "./crates/store/" } -rustical_auth = { path = "./crates/auth/" } rustical_caldav = { path = "./crates/caldav/" } rustical_carddav = { path = "./crates/carddav/" } rustical_frontend = { path = "./crates/frontend/" } diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml deleted file mode 100644 index a4d4a19..0000000 --- a/crates/auth/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "rustical_auth" -version = "0.1.0" -edition = "2021" - -[dependencies] -actix-web = "4.9" -actix-web-httpauth = "0.8" -futures-util = "0.3" -password-auth = "1.0" -serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" diff --git a/crates/auth/src/error.rs b/crates/auth/src/error.rs deleted file mode 100644 index 352f9c4..0000000 --- a/crates/auth/src/error.rs +++ /dev/null @@ -1,36 +0,0 @@ -use actix_web::{http::StatusCode, HttpResponse}; -use thiserror::Error; - -#[derive(Debug, Error, Clone)] -pub enum Error { - #[error("Internal server error")] - InternalError, - #[error("Not found")] - NotFound, - #[error("Bad request")] - BadRequest, - #[error("Unauthorized")] - Unauthorized, -} - -impl actix_web::error::ResponseError for Error { - fn status_code(&self) -> StatusCode { - match *self { - Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR, - Self::NotFound => StatusCode::NOT_FOUND, - Self::BadRequest => StatusCode::BAD_REQUEST, - Self::Unauthorized => StatusCode::UNAUTHORIZED, - } - } - - fn error_response(&self) -> HttpResponse { - match self { - Error::Unauthorized => HttpResponse::build(self.status_code()) - .append_header(("WWW-Authenticate", "Basic")) - // This is an unfortunate workaround for https://github.com/actix/actix-web/issues/1805 - .force_close() - .body(self.to_string()), - _ => HttpResponse::build(self.status_code()).body(self.to_string()), - } - } -} diff --git a/crates/auth/src/extractor.rs b/crates/auth/src/extractor.rs deleted file mode 100644 index 8959633..0000000 --- a/crates/auth/src/extractor.rs +++ /dev/null @@ -1,40 +0,0 @@ -use actix_web::{dev::Payload, web::Data, FromRequest, HttpRequest}; -use std::{ - future::{ready, Ready}, - marker::PhantomData, -}; - -use crate::error::Error; - -use super::{AuthInfo, CheckAuthentication}; - -#[derive(Clone)] -pub struct AuthInfoExtractor { - pub inner: AuthInfo, - pub _provider_type: PhantomData, -} - -impl From for AuthInfoExtractor { - fn from(value: AuthInfo) -> Self { - AuthInfoExtractor { - inner: value, - _provider_type: PhantomData::, - } - } -} - -impl FromRequest for AuthInfoExtractor -where - A: CheckAuthentication, -{ - type Error = Error; - type Future = Ready>; - - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - let result = req.app_data::>().unwrap().validate(req); - ready(result.map(|auth_info| Self { - inner: auth_info, - _provider_type: PhantomData, - })) - } -} diff --git a/crates/auth/src/htpasswd.rs b/crates/auth/src/htpasswd.rs deleted file mode 100644 index 0a80a5e..0000000 --- a/crates/auth/src/htpasswd.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::error::Error; -use actix_web::{http::header::Header, HttpRequest}; -use actix_web_httpauth::headers::authorization::{Authorization, Basic}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use super::{AuthInfo, CheckAuthentication}; - -#[derive(Debug)] -pub struct HtpasswdAuth { - pub config: HtpasswdAuthConfig, -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct HtpasswdAuthUserConfig { - password: String, -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct HtpasswdAuthConfig { - pub users: HashMap, -} - -impl CheckAuthentication for HtpasswdAuth { - fn validate(&self, req: &HttpRequest) -> Result { - if let Ok(auth) = Authorization::::parse(req) { - let user_id = auth.as_ref().user_id(); - // Map None to empty password - let password = auth.as_ref().password().unwrap_or_default(); - - let user_config = if let Some(user_config) = self.config.users.get(user_id) { - user_config - } else { - return Err(crate::error::Error::Unauthorized); - }; - - if let Err(e) = password_auth::verify_password(password, &user_config.password) { - dbg!(e); - return Err(crate::error::Error::Unauthorized); - } - - Ok(AuthInfo { - user_id: user_id.to_string(), - }) - } else { - Err(crate::error::Error::Unauthorized) - } - } -} diff --git a/crates/auth/src/lib.rs b/crates/auth/src/lib.rs deleted file mode 100644 index bc6ea23..0000000 --- a/crates/auth/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -use actix_web::HttpRequest; - -use crate::error::Error; -pub use extractor::AuthInfoExtractor; -pub use htpasswd::{HtpasswdAuth, HtpasswdAuthConfig}; -pub use none::NoneAuth; -pub mod error; -pub mod extractor; -pub mod htpasswd; -pub mod none; - -#[derive(Clone)] -pub struct AuthInfo { - pub user_id: String, -} - -pub trait CheckAuthentication: Send + Sync + 'static { - fn validate(&self, req: &HttpRequest) -> Result - where - Self: Sized; -} - -#[derive(Debug)] -pub enum AuthProvider { - Htpasswd(HtpasswdAuth), - None(NoneAuth), -} - -impl CheckAuthentication for AuthProvider { - fn validate(&self, req: &HttpRequest) -> Result - where - Self: Sized, - { - match self { - Self::Htpasswd(auth) => auth.validate(req), - Self::None(auth) => auth.validate(req), - } - } -} diff --git a/crates/auth/src/none.rs b/crates/auth/src/none.rs deleted file mode 100644 index b02e230..0000000 --- a/crates/auth/src/none.rs +++ /dev/null @@ -1,21 +0,0 @@ -use actix_web::{http::header::Header, HttpRequest}; -use actix_web_httpauth::headers::authorization::{Authorization, Basic}; - -use crate::error::Error; - -use super::{AuthInfo, CheckAuthentication}; - -#[derive(Debug, Clone)] -pub struct NoneAuth; - -impl CheckAuthentication for NoneAuth { - fn validate(&self, req: &HttpRequest) -> Result { - if let Ok(auth) = Authorization::::parse(req) { - Ok(AuthInfo { - user_id: auth.as_ref().user_id().to_string(), - }) - } else { - Err(crate::error::Error::Unauthorized) - } - } -} diff --git a/crates/caldav/Cargo.toml b/crates/caldav/Cargo.toml index 6331187..5eaef4a 100644 --- a/crates/caldav/Cargo.toml +++ b/crates/caldav/Cargo.toml @@ -17,7 +17,6 @@ quick-xml = { version = "0.36", features = [ roxmltree = "0.20" rustical_store = { path = "../store/" } rustical_dav = { path = "../dav/" } -rustical_auth = { path = "../auth/" } serde = { version = "1.0", features = ["serde_derive", "derive"] } serde_json = "1.0" tokio = { version = "1.40", features = ["sync", "full"] } diff --git a/crates/caldav/src/calendar/methods/mkcalendar.rs b/crates/caldav/src/calendar/methods/mkcalendar.rs index f46adef..b8824de 100644 --- a/crates/caldav/src/calendar/methods/mkcalendar.rs +++ b/crates/caldav/src/calendar/methods/mkcalendar.rs @@ -2,7 +2,7 @@ use crate::CalDavContext; use crate::Error; use actix_web::web::{Data, Path}; use actix_web::HttpResponse; -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; +use rustical_store::auth::User; use rustical_store::model::Calendar; use rustical_store::CalendarStore; use serde::{Deserialize, Serialize}; @@ -53,14 +53,14 @@ struct MkcalendarRequest { set: PropElement, } -pub async fn route_mkcalendar( +pub async fn route_mkcalendar( path: Path<(String, String)>, body: String, - auth: AuthInfoExtractor, + user: User, context: Data>, ) -> Result { let (principal, cid) = path.into_inner(); - if principal != auth.inner.user_id { + if principal != user.id { return Err(Error::Unauthorized); } diff --git a/crates/caldav/src/calendar/methods/report/mod.rs b/crates/caldav/src/calendar/methods/report/mod.rs index 39e8194..2f21c3c 100644 --- a/crates/caldav/src/calendar/methods/report/mod.rs +++ b/crates/caldav/src/calendar/methods/report/mod.rs @@ -5,9 +5,8 @@ use actix_web::{ }; use calendar_multiget::{handle_calendar_multiget, CalendarMultigetRequest}; use calendar_query::{handle_calendar_query, CalendarQueryRequest}; -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; use rustical_dav::methods::propfind::ServicePrefix; -use rustical_store::CalendarStore; +use rustical_store::{auth::User, CalendarStore}; use serde::{Deserialize, Serialize}; use sync_collection::{handle_sync_collection, SyncCollectionRequest}; use tokio::sync::RwLock; @@ -32,17 +31,17 @@ pub enum ReportRequest { SyncCollection(SyncCollectionRequest), } -pub async fn route_report_calendar( +pub async fn route_report_calendar( path: Path<(String, String)>, body: String, - auth: AuthInfoExtractor, + user: User, req: HttpRequest, cal_store: Data>, prefix: Data, ) -> Result { let prefix = prefix.into_inner(); let (principal, cid) = path.into_inner(); - if principal != auth.inner.user_id { + if principal != user.id { return Err(Error::Unauthorized); } diff --git a/crates/caldav/src/calendar_object/methods.rs b/crates/caldav/src/calendar_object/methods.rs index 7c06067..0dcba1c 100644 --- a/crates/caldav/src/calendar_object/methods.rs +++ b/crates/caldav/src/calendar_object/methods.rs @@ -5,18 +5,18 @@ use actix_web::http::header::HeaderValue; use actix_web::web::{Data, Path}; use actix_web::HttpRequest; use actix_web::HttpResponse; -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; +use rustical_store::auth::User; use rustical_store::model::CalendarObject; use rustical_store::CalendarStore; -pub async fn get_event( +pub async fn get_event( context: Data>, path: Path<(String, String, String)>, - auth: AuthInfoExtractor, + user: User, ) -> Result { let (principal, cid, mut uid) = path.into_inner(); - if auth.inner.user_id != principal { + if user.id != principal { return Ok(HttpResponse::Unauthorized().body("")); } @@ -26,7 +26,7 @@ pub async fn get_event( .await .get_calendar(&principal, &cid) .await?; - if auth.inner.user_id != calendar.principal { + if user.id != calendar.principal { return Ok(HttpResponse::Unauthorized().body("")); } @@ -46,16 +46,15 @@ pub async fn get_event( .body(event.get_ics().to_owned())) } -pub async fn put_event( +pub async fn put_event( context: Data>, path: Path<(String, String, String)>, body: String, - auth: AuthInfoExtractor, + user: User, req: HttpRequest, ) -> Result { let (principal, cid, mut uid) = path.into_inner(); - let auth_info = auth.inner; - if auth_info.user_id != principal { + if user.id != principal { return Ok(HttpResponse::Unauthorized().body("")); } @@ -65,7 +64,7 @@ pub async fn put_event( .await .get_calendar(&principal, &cid) .await?; - if auth_info.user_id != calendar.principal { + if user.id != calendar.principal { return Ok(HttpResponse::Unauthorized().body("")); } // Incredibly bodged method of normalising the uid but works for a prototype diff --git a/crates/caldav/src/lib.rs b/crates/caldav/src/lib.rs index e56333a..0e58a85 100644 --- a/crates/caldav/src/lib.rs +++ b/crates/caldav/src/lib.rs @@ -5,7 +5,6 @@ use calendar::resource::CalendarResourceService; use calendar_object::resource::CalendarObjectResourceService; use principal::PrincipalResourceService; use root::RootResourceService; -use rustical_auth::CheckAuthentication; use rustical_dav::methods::{ propfind::ServicePrefix, route_delete, route_propfind, route_proppatch, }; @@ -30,10 +29,9 @@ pub fn configure_well_known(cfg: &mut web::ServiceConfig, caldav_root: String) { cfg.service(web::redirect("/caldav", caldav_root).permanent()); } -pub fn configure_dav( +pub fn configure_dav( cfg: &mut web::ServiceConfig, prefix: String, - auth: Arc, store: Arc>, ) { let propfind_method = || web::method(Method::from_str("PROPFIND").unwrap()); @@ -46,7 +44,6 @@ pub fn configure_dav( })) .app_data(Data::new(ServicePrefix(prefix))) .app_data(Data::from(store.clone())) - .app_data(Data::from(auth)) .service( web::resource("{path:.*}") // Without the guard this service would handle all requests @@ -55,20 +52,17 @@ pub fn configure_dav( ) .service( web::resource("") - .route(propfind_method().to(route_propfind::)) - .route(proppatch_method().to(route_proppatch::)), + .route(propfind_method().to(route_propfind::)) + .route(proppatch_method().to(route_proppatch::)), ) .service( web::scope("/user").service( web::scope("/{principal}") .service( web::resource("") + .route(propfind_method().to(route_propfind::>)) .route( - propfind_method().to(route_propfind::>), - ) - .route( - proppatch_method() - .to(route_proppatch::>), + proppatch_method().to(route_proppatch::>), ), ) .service( @@ -76,49 +70,47 @@ pub fn configure_dav( .service( web::resource("") .route( - report_method().to( - calendar::methods::report::route_report_calendar::, - ), + report_method() + .to(calendar::methods::report::route_report_calendar::), ) .route( propfind_method() - .to(route_propfind::>), + .to(route_propfind::>), ) .route( proppatch_method() - .to(route_proppatch::>), + .to(route_proppatch::>), ) .route( web::method(Method::DELETE) - .to(route_delete::>), + .to(route_delete::>), ) .route( - mkcalendar_method().to( - calendar::methods::mkcalendar::route_mkcalendar::, - ), + mkcalendar_method() + .to(calendar::methods::mkcalendar::route_mkcalendar::), ), ) .service( web::resource("/{event}") .route( propfind_method() - .to(route_propfind::>), + .to(route_propfind::>), ) .route( proppatch_method() - .to(route_proppatch::>), + .to(route_proppatch::>), ) .route( web::method(Method::DELETE) - .to(route_delete::>), + .to(route_delete::>), ) .route( web::method(Method::GET) - .to(calendar_object::methods::get_event::), + .to(calendar_object::methods::get_event::), ) .route( web::method(Method::PUT) - .to(calendar_object::methods::put_event::), + .to(calendar_object::methods::put_event::), ), ), ), diff --git a/crates/carddav/Cargo.toml b/crates/carddav/Cargo.toml index f10a2ef..24e55e7 100644 --- a/crates/carddav/Cargo.toml +++ b/crates/carddav/Cargo.toml @@ -17,7 +17,6 @@ quick-xml = { version = "0.36", features = [ roxmltree = "0.20" rustical_store = { path = "../store/" } rustical_dav = { path = "../dav/" } -rustical_auth = { path = "../auth/" } serde = { version = "1.0", features = ["serde_derive", "derive"] } serde_json = "1.0" tokio = { version = "1.40", features = ["sync", "full"] } diff --git a/crates/carddav/src/lib.rs b/crates/carddav/src/lib.rs index 53f0e84..ab1729e 100644 --- a/crates/carddav/src/lib.rs +++ b/crates/carddav/src/lib.rs @@ -1,17 +1,10 @@ use actix_web::{web, HttpResponse, Responder}; -use rustical_auth::CheckAuthentication; -use std::sync::Arc; pub fn configure_well_known(cfg: &mut web::ServiceConfig, carddav_root: String) { cfg.service(web::redirect("/carddav", carddav_root).permanent()); } -pub fn configure_dav( - _cfg: &mut web::ServiceConfig, - _prefix: String, - _auth: Arc, -) { -} +pub fn configure_dav(_cfg: &mut web::ServiceConfig, _prefix: String) {} pub async fn options_handler() -> impl Responder { HttpResponse::Ok() diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 5342058..41ce23d 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -13,7 +13,7 @@ quick-xml = { version = "0.36", features = [ "serde-types", "serialize", ] } -rustical_auth = { path = "../auth/" } +rustical_store = { path = "../store/" } serde = { version = "1.0", features = ["derive"] } strum = "0.26" itertools = "0.13" diff --git a/crates/dav/src/methods/delete.rs b/crates/dav/src/methods/delete.rs index def2bd5..2fb14a4 100644 --- a/crates/dav/src/methods/delete.rs +++ b/crates/dav/src/methods/delete.rs @@ -3,11 +3,12 @@ use actix_web::web::Path; use actix_web::HttpRequest; use actix_web::HttpResponse; use actix_web::Responder; -use rustical_auth::CheckAuthentication; +use rustical_store::auth::User; -pub async fn route_delete( +pub async fn route_delete( path_components: Path, req: HttpRequest, + _user: User, ) -> Result { let path_components = path_components.into_inner(); diff --git a/crates/dav/src/methods/propfind.rs b/crates/dav/src/methods/propfind.rs index 6675ce7..5a9c8d7 100644 --- a/crates/dav/src/methods/propfind.rs +++ b/crates/dav/src/methods/propfind.rs @@ -10,7 +10,7 @@ use actix_web::web::{Data, Path}; use actix_web::HttpRequest; use derive_more::derive::Deref; use log::debug; -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; +use rustical_store::auth::User; use serde::Deserialize; // This is not the final place for this struct @@ -39,12 +39,12 @@ struct PropfindElement { prop: PropfindType, } -pub async fn route_propfind( +pub async fn route_propfind( path_components: Path, body: String, req: HttpRequest, prefix: Data, - auth: AuthInfoExtractor, + user: User, depth: Depth, ) -> Result< MultistatusElement< @@ -86,7 +86,7 @@ pub async fn route_propfind( } } - let resource = resource_service.get_resource(auth.inner.user_id).await?; + let resource = resource_service.get_resource(user.id).await?; let response = resource.propfind(&prefix, req.path(), props).await?; Ok(MultistatusElement { diff --git a/crates/dav/src/methods/proppatch.rs b/crates/dav/src/methods/proppatch.rs index b9f8f48..0f7fe2a 100644 --- a/crates/dav/src/methods/proppatch.rs +++ b/crates/dav/src/methods/proppatch.rs @@ -9,7 +9,7 @@ use crate::Error; use actix_web::http::StatusCode; use actix_web::{web::Path, HttpRequest}; use log::debug; -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; +use rustical_store::auth::User; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -47,11 +47,11 @@ struct PropertyupdateElement { operations: Vec>, } -pub async fn route_proppatch( +pub async fn route_proppatch( path: Path, body: String, req: HttpRequest, - auth: AuthInfoExtractor, + user: User, ) -> Result, PropstatWrapper>, R::Error> { let path_components = path.into_inner(); let href = req.path().to_owned(); @@ -75,7 +75,7 @@ pub async fn route_proppatch( }) .collect(); - let mut resource = resource_service.get_resource(auth.inner.user_id).await?; + let mut resource = resource_service.get_resource(user.id).await?; let mut props_ok = Vec::new(); let mut props_conflict = Vec::new(); diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 5e832f9..03aedc1 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -3,8 +3,6 @@ name = "rustical_store" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] anyhow = { version = "1.0", features = ["backtrace"] } async-trait = "0.1" @@ -27,3 +25,6 @@ lazy_static = "1.5" rstest = "0.23" rstest_reuse = "0.7" thiserror = "1.0" +password-auth = "1.0" +actix-web = "4.9" +actix-web-httpauth = "0.8" diff --git a/crates/store/src/auth/middleware.rs b/crates/store/src/auth/middleware.rs new file mode 100644 index 0000000..16ed877 --- /dev/null +++ b/crates/store/src/auth/middleware.rs @@ -0,0 +1,90 @@ +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + error::ErrorUnauthorized, + http::header::Header, + HttpMessage, +}; +use actix_web_httpauth::headers::authorization::{Authorization, Basic}; +use std::{ + future::{ready, Future, Ready}, + pin::Pin, + sync::Arc, +}; + +use super::AuthenticationProvider; + +pub struct AuthenticationMiddleware { + auth_provider: Arc, +} + +impl AuthenticationMiddleware { + pub fn new(auth_provider: Arc) -> Self { + Self { auth_provider } + } +} + +impl Transform for AuthenticationMiddleware +where + S: Service, Error = actix_web::Error> + 'static, + S::Future: 'static, + B: 'static, + AP: 'static, +{ + type Error = actix_web::Error; + type Response = ServiceResponse; + type InitError = (); + type Transform = InnerAuthenticationMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(InnerAuthenticationMiddleware { + service: Arc::new(service), + auth_provider: Arc::clone(&self.auth_provider), + })) + } +} + +pub struct InnerAuthenticationMiddleware { + service: Arc, + auth_provider: Arc, +} + +impl Service for InnerAuthenticationMiddleware +where + S: Service, Error = actix_web::Error> + 'static, + S::Future: 'static, + AP: AuthenticationProvider + 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + // type Future = Pin>>; + type Future = Pin>>>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let service = Arc::clone(&self.service); + let auth_provider = Arc::clone(&self.auth_provider); + + Box::pin(async move { + if let Ok(auth) = Authorization::::parse(req.request()) { + let user_id = auth.as_ref().user_id(); + let password = auth + .as_ref() + .password() + .ok_or(ErrorUnauthorized("no password"))?; + + let user = auth_provider + .validate_user_token(user_id, password) + .await + .map_err(|_| ErrorUnauthorized(""))? + .ok_or(ErrorUnauthorized(""))?; + + req.extensions_mut().insert(user); + service.call(req).await + } else { + Err(ErrorUnauthorized("")) + } + }) + } +} diff --git a/crates/store/src/auth/mod.rs b/crates/store/src/auth/mod.rs new file mode 100644 index 0000000..6a0e1d9 --- /dev/null +++ b/crates/store/src/auth/mod.rs @@ -0,0 +1,16 @@ +pub mod middleware; +pub mod static_user_store; +pub mod user; +pub mod user_store; +use crate::error::Error; +use async_trait::async_trait; + +#[async_trait] +pub trait AuthenticationProvider { + async fn validate_user_token(&self, user_id: &str, token: &str) -> Result, Error>; +} + +pub use middleware::AuthenticationMiddleware; +pub use static_user_store::{StaticUserStore, StaticUserStoreConfig}; +pub use user::User; + diff --git a/crates/store/src/auth/static_user_store.rs b/crates/store/src/auth/static_user_store.rs new file mode 100644 index 0000000..30c37f3 --- /dev/null +++ b/crates/store/src/auth/static_user_store.rs @@ -0,0 +1,43 @@ +use crate::{auth::User, error::Error}; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::AuthenticationProvider; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct StaticUserStoreConfig { + users: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct StaticUserStore { + pub users: HashMap, +} + +impl StaticUserStore { + pub fn new(config: StaticUserStoreConfig) -> Self { + Self { + users: HashMap::from_iter(config.users.into_iter().map(|user| (user.id.clone(), user))), + } + } +} + +#[async_trait] +impl AuthenticationProvider for StaticUserStore { + async fn validate_user_token(&self, user_id: &str, token: &str) -> Result, Error> { + let user: User = match self.users.get(user_id) { + Some(user) => user.clone(), + None => return Ok(None), + }; + + let password = match &user.password { + Some(password) => password, + None => return Ok(None), + }; + + Ok(password_auth::verify_password(token, password) + .map(|()| user) + .ok()) + } +} diff --git a/crates/store/src/auth/user.rs b/crates/store/src/auth/user.rs new file mode 100644 index 0000000..51fa631 --- /dev/null +++ b/crates/store/src/auth/user.rs @@ -0,0 +1,27 @@ +use actix_web::{error::ErrorUnauthorized, FromRequest, HttpMessage}; +use serde::{Deserialize, Serialize}; +use std::future::{ready, Ready}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct User { + pub id: String, + pub displayname: Option, + pub password: Option, +} + +impl FromRequest for User { + type Error = actix_web::Error; + type Future = Ready>; + + fn from_request( + req: &actix_web::HttpRequest, + _payload: &mut actix_web::dev::Payload, + ) -> Self::Future { + ready( + req.extensions() + .get::() + .cloned() + .ok_or(ErrorUnauthorized("")), + ) + } +} diff --git a/crates/store/src/auth/user_store.rs b/crates/store/src/auth/user_store.rs new file mode 100644 index 0000000..fdc0e4d --- /dev/null +++ b/crates/store/src/auth/user_store.rs @@ -0,0 +1,8 @@ +use crate::{auth::User, error::Error}; +use async_trait::async_trait; + +#[async_trait] +pub trait UserStore: Send + Sync + 'static { + async fn get_user(&self, id: &str) -> Result, Error>; + async fn put_user(&self, user: User) -> Result<(), Error>; +} diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 24849d8..d749032 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -5,4 +5,4 @@ pub mod sqlite_store; pub mod timestamp; pub use calendar_store::CalendarStore; pub use error::Error; - +pub mod auth; diff --git a/src/app.rs b/src/app.rs index 4ac7d2c..d21881d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,15 +2,15 @@ use actix_web::body::MessageBody; use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse}; use actix_web::middleware::{Logger, NormalizePath}; use actix_web::{web, App}; -use rustical_auth::CheckAuthentication; use rustical_frontend::configure_frontend; +use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider}; use rustical_store::CalendarStore; use std::sync::Arc; use tokio::sync::RwLock; -pub fn make_app( +pub fn make_app( cal_store: Arc>, - auth: Arc, + auth_provider: Arc, ) -> App< impl ServiceFactory< ServiceRequest, @@ -23,17 +23,14 @@ pub fn make_app( App::new() .wrap(Logger::new("[%s] %r")) .wrap(NormalizePath::trim()) + .wrap(AuthenticationMiddleware::new(auth_provider)) .service(web::scope("/caldav").configure(|cfg| { - rustical_caldav::configure_dav( - cfg, - "/caldav".to_string(), - auth.clone(), - cal_store.clone(), - ) - })) - .service(web::scope("/carddav").configure(|cfg| { - rustical_carddav::configure_dav(cfg, "/carddav".to_string(), auth.clone()) + rustical_caldav::configure_dav(cfg, "/caldav".to_string(), cal_store.clone()) })) + .service( + web::scope("/carddav") + .configure(|cfg| rustical_carddav::configure_dav(cfg, "/carddav".to_string())), + ) .service( web::scope("/.well-known") .configure(|cfg| rustical_caldav::configure_well_known(cfg, "/caldav".to_string())), // .configure(|cfg| { diff --git a/src/config.rs b/src/config.rs index 24c2be3..6ff766b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ -use rustical_auth::{AuthProvider, HtpasswdAuthConfig}; +use rustical_frontend::FrontendConfig; +use rustical_store::auth::StaticUserStoreConfig; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] @@ -21,19 +22,7 @@ pub enum CalendarStoreConfig { #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "backend", rename_all = "snake_case")] pub enum AuthConfig { - Htpasswd(HtpasswdAuthConfig), - None, -} - -impl From for AuthProvider { - fn from(value: AuthConfig) -> Self { - match value { - AuthConfig::Htpasswd(config) => { - Self::Htpasswd(rustical_auth::htpasswd::HtpasswdAuth { config }) - } - AuthConfig::None => Self::None(rustical_auth::none::NoneAuth), - } - } + Static(StaticUserStoreConfig), } #[derive(Debug, Deserialize, Serialize)] @@ -41,4 +30,5 @@ pub struct Config { pub calendar_store: CalendarStoreConfig, pub auth: AuthConfig, pub http: HttpConfig, + pub frontend: FrontendConfig, } diff --git a/src/main.rs b/src/main.rs index 72c040c..8e977f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use anyhow::Result; use app::make_app; use clap::Parser; use config::{CalendarStoreConfig, SqliteCalendarStoreConfig}; -use rustical_auth::AuthProvider; +use rustical_store::auth::StaticUserStore; use rustical_store::sqlite_store::{create_db_pool, SqliteCalendarStore}; use rustical_store::CalendarStore; use std::fs; @@ -45,9 +45,11 @@ async fn main() -> Result<()> { let cal_store = get_cal_store(args.migrate, &config.calendar_store).await?; - let auth: Arc = Arc::new(config.auth.into()); + let user_store = Arc::new(match config.auth { + config::AuthConfig::Static(config) => StaticUserStore::new(config), + }); - HttpServer::new(move || make_app(cal_store.clone(), auth.clone())) + HttpServer::new(move || make_app(cal_store.clone(), user_store.clone())) .bind((config.http.host, config.http.port))? .run() .await?;