diff --git a/crates/caldav/src/calendar/methods/report/calendar_multiget.rs b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs index 0358335..8fdc59e 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_multiget.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs @@ -16,7 +16,7 @@ use rustical_dav::{ MultistatusElement, }, }; -use rustical_store::{CalendarObject, CalendarStore}; +use rustical_store::{auth::User, CalendarObject, CalendarStore}; use serde::Deserialize; #[derive(Deserialize, Clone, Debug)] @@ -65,6 +65,7 @@ pub async fn get_objects_calendar_multiget( pub async fn handle_calendar_multiget( cal_multiget: CalendarMultigetRequest, req: HttpRequest, + user: &User, principal: &str, cal_id: &str, cal_store: &C, @@ -88,11 +89,13 @@ pub async fn handle_calendar_multiget( let mut responses = Vec::new(); for object in objects { let path = format!("{}/{}", req.path(), object.get_id()); - responses.push(CalendarObjectResource::from(object).propfind( - &path, - props.clone(), - req.resource_map(), - )?); + responses.push( + CalendarObjectResource { + object, + principal: principal.to_owned(), + } + .propfind(&path, props.clone(), user, req.resource_map())?, + ); } let not_found_responses = not_found diff --git a/crates/caldav/src/calendar/methods/report/calendar_query.rs b/crates/caldav/src/calendar/methods/report/calendar_query.rs index 75d7cae..a2c7f74 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query.rs @@ -5,7 +5,7 @@ use rustical_dav::{ resource::Resource, xml::{multistatus::PropstatWrapper, MultistatusElement}, }; -use rustical_store::{CalendarObject, CalendarStore}; +use rustical_store::{auth::User, CalendarObject, CalendarStore}; use serde::Deserialize; use crate::{ @@ -206,6 +206,7 @@ pub async fn get_objects_calendar_query( pub async fn handle_calendar_query( cal_query: CalendarQueryRequest, req: HttpRequest, + user: &User, principal: &str, cal_id: &str, cal_store: &C, @@ -230,11 +231,13 @@ pub async fn handle_calendar_query( vec![principal, cal_id, object.get_id()], ) .unwrap(); - responses.push(CalendarObjectResource::from(object).propfind( - &path, - props.clone(), - req.resource_map(), - )?); + responses.push( + CalendarObjectResource { + object, + principal: principal.to_owned(), + } + .propfind(&path, props.clone(), user, req.resource_map())?, + ); } Ok(MultistatusElement { diff --git a/crates/caldav/src/calendar/methods/report/mod.rs b/crates/caldav/src/calendar/methods/report/mod.rs index c54ccb5..9c9188f 100644 --- a/crates/caldav/src/calendar/methods/report/mod.rs +++ b/crates/caldav/src/calendar/methods/report/mod.rs @@ -47,16 +47,32 @@ pub async fn route_report_calendar( Ok(match request.clone() { ReportRequest::CalendarQuery(cal_query) => { - handle_calendar_query(cal_query, req, &principal, &cal_id, cal_store.as_ref()).await? + handle_calendar_query( + cal_query, + req, + &user, + &principal, + &cal_id, + cal_store.as_ref(), + ) + .await? } ReportRequest::CalendarMultiget(cal_multiget) => { - handle_calendar_multiget(cal_multiget, req, &principal, &cal_id, cal_store.as_ref()) - .await? + handle_calendar_multiget( + cal_multiget, + req, + &user, + &principal, + &cal_id, + cal_store.as_ref(), + ) + .await? } ReportRequest::SyncCollection(sync_collection) => { handle_sync_collection( sync_collection, req, + &user, &principal, &cal_id, cal_store.as_ref(), diff --git a/crates/caldav/src/calendar/methods/report/sync_collection.rs b/crates/caldav/src/calendar/methods/report/sync_collection.rs index 778e2fb..8a1812f 100644 --- a/crates/caldav/src/calendar/methods/report/sync_collection.rs +++ b/crates/caldav/src/calendar/methods/report/sync_collection.rs @@ -8,6 +8,7 @@ use rustical_dav::{ }, }; use rustical_store::{ + auth::User, synctoken::{format_synctoken, parse_synctoken}, CalendarStore, }; @@ -44,6 +45,7 @@ pub struct SyncCollectionRequest { pub async fn handle_sync_collection( sync_collection: SyncCollectionRequest, req: HttpRequest, + user: &User, principal: &str, cal_id: &str, cal_store: &C, @@ -71,11 +73,13 @@ pub async fn handle_sync_collection( vec![principal, cal_id, &object.get_id()], ) .unwrap(); - responses.push(CalendarObjectResource::from(object).propfind( - &path, - props.clone(), - req.resource_map(), - )?); + responses.push( + CalendarObjectResource { + object, + principal: principal.to_owned(), + } + .propfind(&path, props.clone(), user, req.resource_map())?, + ); } for object_id in deleted_objects { diff --git a/crates/caldav/src/calendar/prop.rs b/crates/caldav/src/calendar/prop.rs index 92415ee..d012a09 100644 --- a/crates/caldav/src/calendar/prop.rs +++ b/crates/caldav/src/calendar/prop.rs @@ -47,55 +47,6 @@ pub struct Resourcetype { collection: (), } -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum UserPrivilege { - Read, - ReadAcl, - Write, - WriteAcl, - WriteContent, - ReadCurrentUserPrivilegeSet, - Bind, - Unbind, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct UserPrivilegeWrapper { - #[serde(rename = "$value")] - privilege: UserPrivilege, -} - -impl From for UserPrivilegeWrapper { - fn from(value: UserPrivilege) -> Self { - Self { privilege: value } - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct UserPrivilegeSet { - privilege: Vec, -} - -impl Default for UserPrivilegeSet { - fn default() -> Self { - Self { - privilege: vec![ - UserPrivilege::Read.into(), - UserPrivilege::ReadAcl.into(), - UserPrivilege::Write.into(), - UserPrivilege::WriteAcl.into(), - UserPrivilege::WriteContent.into(), - UserPrivilege::ReadCurrentUserPrivilegeSet.into(), - UserPrivilege::Bind.into(), - UserPrivilege::Unbind.into(), - ], - } - } -} - #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub enum ReportMethod { diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index 5e2a707..f8bc811 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -2,7 +2,7 @@ use super::methods::mkcalendar::route_mkcalendar; use super::methods::report::route_report_calendar; use super::prop::{ Resourcetype, SupportedCalendarComponent, SupportedCalendarComponentSet, SupportedCalendarData, - SupportedReportSet, UserPrivilegeSet, + SupportedReportSet, }; use crate::calendar_object::resource::CalendarObjectResource; use crate::principal::PrincipalResource; @@ -13,6 +13,7 @@ use actix_web::web; use actix_web::{web::Data, HttpRequest}; use async_trait::async_trait; use derive_more::derive::{From, Into}; +use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; use rustical_store::auth::User; @@ -43,7 +44,7 @@ pub enum CalendarPropName { SupportedCalendarComponentSet, SupportedCalendarData, Getcontenttype, - CurrentUserPrivilegeSet, + // CurrentUserPrivilegeSet, MaxResourceSize, SupportedReportSet, SyncToken, @@ -63,7 +64,7 @@ pub enum CalendarProp { // WebDAV Access Control (RFC 3744) Owner(HrefElement), - CurrentUserPrivilegeSet(UserPrivilegeSet), + // CurrentUserPrivilegeSet(UserPrivilegeSet), // CalDAV (RFC 4791) #[serde(rename = "IC:calendar-color", alias = "calendar-color")] @@ -113,6 +114,7 @@ impl Resource for CalendarResource { fn get_prop( &self, rmap: &ResourceMap, + user: &User, prop: Self::PropName, ) -> Result { Ok(match prop { @@ -156,9 +158,9 @@ impl Resource for CalendarResource { CalendarProp::Getcontenttype("text/calendar;charset=utf-8".to_owned()) } CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10000000), - CalendarPropName::CurrentUserPrivilegeSet => { - CalendarProp::CurrentUserPrivilegeSet(UserPrivilegeSet::default()) - } + // CalendarPropName::CurrentUserPrivilegeSet => { + // CalendarProp::CurrentUserPrivilegeSet(user_privileges.to_owned()) + // } CalendarPropName::SupportedReportSet => { CalendarProp::SupportedReportSet(SupportedReportSet::default()) } @@ -198,7 +200,7 @@ impl Resource for CalendarResource { CalendarProp::SupportedCalendarData(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Getcontenttype(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly), - CalendarProp::CurrentUserPrivilegeSet(_) => Err(rustical_dav::Error::PropReadOnly), + // CalendarProp::CurrentUserPrivilegeSet(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::SyncToken(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Getctag(_) => Err(rustical_dav::Error::PropReadOnly), @@ -237,7 +239,7 @@ impl Resource for CalendarResource { CalendarPropName::SupportedCalendarData => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::Getcontenttype => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly), - CalendarPropName::CurrentUserPrivilegeSet => Err(rustical_dav::Error::PropReadOnly), + // CalendarPropName::CurrentUserPrivilegeSet => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::SyncToken => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::Getctag => Err(rustical_dav::Error::PropReadOnly), @@ -248,6 +250,10 @@ impl Resource for CalendarResource { fn resource_name() -> &'static str { "caldav_calendar" } + + fn get_user_privileges(&self, user: &User) -> Result { + Ok(UserPrivilegeSet::owner_only(self.0.principal == user.id)) + } } #[async_trait(?Send)] @@ -257,10 +263,7 @@ impl ResourceService for CalendarResourceService { type Resource = CalendarResource; type Error = Error; - async fn get_resource(&self, user: User) -> Result { - if self.principal != user.id { - return Err(Error::Unauthorized); - } + async fn get_resource(&self) -> Result { let calendar = self .cal_store .get_calendar(&self.principal, &self.calendar_id) @@ -285,7 +288,10 @@ impl ResourceService for CalendarResourceService { vec![&self.principal, &self.calendar_id, object.get_id()], ) .unwrap(), - object.into(), + CalendarObjectResource { + object, + principal: self.principal.to_owned(), + }, ) }) .collect()) diff --git a/crates/caldav/src/calendar_object/resource.rs b/crates/caldav/src/calendar_object/resource.rs index 9985860..ca3070a 100644 --- a/crates/caldav/src/calendar_object/resource.rs +++ b/crates/caldav/src/calendar_object/resource.rs @@ -1,9 +1,13 @@ use super::methods::{get_event, put_event}; -use crate::Error; +use crate::{principal::PrincipalResource, Error}; use actix_web::{dev::ResourceMap, web::Data, HttpRequest}; use async_trait::async_trait; use derive_more::derive::{From, Into}; -use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; +use rustical_dav::{ + privileges::UserPrivilegeSet, + resource::{InvalidProperty, Resource, ResourceService}, + xml::HrefElement, +}; use rustical_store::{auth::User, CalendarObject, CalendarStore}; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -23,6 +27,9 @@ pub enum CalendarObjectPropName { Getetag, CalendarData, Getcontenttype, + CurrentUserPrincipal, + Owner, + CurrentUserPrivilegeSet, } #[derive(Deserialize, Serialize, Debug, Clone)] @@ -35,6 +42,13 @@ pub enum CalendarObjectProp { // CalDAV (RFC 4791) #[serde(rename = "C:calendar-data")] CalendarData(String), + + // WebDAV Current Principal Extension (RFC 5397) + CurrentUserPrincipal(HrefElement), + + // WebDAV Access Control (RFC 3744) + Owner(HrefElement), + CurrentUserPrivilegeSet(UserPrivilegeSet), #[serde(other)] Invalid, } @@ -46,7 +60,10 @@ impl InvalidProperty for CalendarObjectProp { } #[derive(Clone, From, Into)] -pub struct CalendarObjectResource(CalendarObject); +pub struct CalendarObjectResource { + pub object: CalendarObject, + pub principal: String, +} impl Resource for CalendarObjectResource { type PropName = CalendarObjectPropName; @@ -55,17 +72,29 @@ impl Resource for CalendarObjectResource { fn get_prop( &self, - _rmap: &ResourceMap, + rmap: &ResourceMap, + user: &User, prop: Self::PropName, ) -> Result { Ok(match prop { - CalendarObjectPropName::Getetag => CalendarObjectProp::Getetag(self.0.get_etag()), + CalendarObjectPropName::Getetag => CalendarObjectProp::Getetag(self.object.get_etag()), CalendarObjectPropName::CalendarData => { - CalendarObjectProp::CalendarData(self.0.get_ics().to_owned()) + CalendarObjectProp::CalendarData(self.object.get_ics().to_owned()) } CalendarObjectPropName::Getcontenttype => { CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8".to_owned()) } + CalendarObjectPropName::CurrentUserPrincipal => { + CalendarObjectProp::CurrentUserPrincipal(HrefElement::new( + PrincipalResource::get_principal_url(rmap, &user.id), + )) + } + CalendarObjectPropName::Owner => CalendarObjectProp::Owner( + PrincipalResource::get_principal_url(rmap, &self.principal).into(), + ), + CalendarObjectPropName::CurrentUserPrivilegeSet => { + CalendarObjectProp::CurrentUserPrivilegeSet(self.get_user_privileges(&user)?) + } }) } @@ -73,6 +102,10 @@ impl Resource for CalendarObjectResource { fn resource_name() -> &'static str { "caldav_calendar_object" } + + fn get_user_privileges(&self, user: &User) -> Result { + Ok(UserPrivilegeSet::owner_only(self.principal == user.id)) + } } #[derive(Debug, Clone)] @@ -132,15 +165,15 @@ impl ResourceService for CalendarObjectResourceServic }) } - async fn get_resource(&self, user: User) -> Result { - if self.principal != user.id { - return Err(Error::Unauthorized); - } - let event = self + async fn get_resource(&self) -> Result { + let object = self .cal_store .get_object(&self.principal, &self.cal_id, &self.object_id) .await?; - Ok(event.into()) + Ok(CalendarObjectResource { + object, + principal: self.principal.to_owned(), + }) } async fn save_resource(&self, _file: Self::Resource) -> Result<(), Self::Error> { diff --git a/crates/caldav/src/principal/mod.rs b/crates/caldav/src/principal/mod.rs index 5efeee4..98b58ad 100644 --- a/crates/caldav/src/principal/mod.rs +++ b/crates/caldav/src/principal/mod.rs @@ -4,6 +4,7 @@ use actix_web::dev::ResourceMap; use actix_web::web::Data; use actix_web::HttpRequest; use async_trait::async_trait; +use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; use rustical_store::auth::User; @@ -42,6 +43,10 @@ pub enum PrincipalProp { // WebDAV Current Principal Extension (RFC 5397) CurrentUserPrincipal(HrefElement), + // WebDAV Access Control (RFC 3744) + Owner(HrefElement), + CurrentUserPrivilegeSet(UserPrivilegeSet), + // CalDAV (RFC 4791) #[serde(rename = "C:calendar-home-set")] CalendarHomeSet(HrefElement), @@ -63,12 +68,20 @@ impl InvalidProperty for PrincipalProp { pub enum PrincipalPropName { Resourcetype, CurrentUserPrincipal, + Owner, + CurrentUserPrivilegeSet, #[strum(serialize = "principal-URL")] PrincipalUrl, CalendarHomeSet, CalendarUserAddressSet, } +impl PrincipalResource { + pub fn get_principal_url(rmap: &ResourceMap, principal: &str) -> String { + Self::get_url(rmap, vec![principal]).unwrap() + } +} + impl Resource for PrincipalResource { type PropName = PrincipalPropName; type Prop = PrincipalProp; @@ -77,6 +90,7 @@ impl Resource for PrincipalResource { fn get_prop( &self, rmap: &ResourceMap, + user: &User, prop: Self::PropName, ) -> Result { let principal_href = HrefElement::new(Self::get_url(rmap, vec![&self.principal]).unwrap()); @@ -86,6 +100,12 @@ impl Resource for PrincipalResource { PrincipalPropName::CurrentUserPrincipal => { PrincipalProp::CurrentUserPrincipal(principal_href) } + PrincipalPropName::Owner => PrincipalProp::Owner(HrefElement::new( + PrincipalResource::get_url(rmap, vec![&self.principal]).unwrap(), + )), + PrincipalPropName::CurrentUserPrivilegeSet => { + PrincipalProp::CurrentUserPrivilegeSet(self.get_user_privileges(user)?) + } PrincipalPropName::PrincipalUrl => PrincipalProp::PrincipalUrl(principal_href), PrincipalPropName::CalendarHomeSet => PrincipalProp::CalendarHomeSet(principal_href), PrincipalPropName::CalendarUserAddressSet => { @@ -98,6 +118,10 @@ impl Resource for PrincipalResource { fn resource_name() -> &'static str { "caldav_principal" } + + fn get_user_privileges(&self, user: &User) -> Result { + Ok(UserPrivilegeSet::owner_only(self.principal == user.id)) + } } #[async_trait(?Send)] @@ -123,10 +147,7 @@ impl ResourceService for PrincipalResourceService }) } - async fn get_resource(&self, user: User) -> Result { - if self.principal != user.id { - return Err(Error::Unauthorized); - } + async fn get_resource(&self) -> Result { Ok(PrincipalResource { principal: self.principal.to_owned(), }) diff --git a/crates/caldav/src/root/mod.rs b/crates/caldav/src/root/mod.rs index e14f43a..4dde1bb 100644 --- a/crates/caldav/src/root/mod.rs +++ b/crates/caldav/src/root/mod.rs @@ -3,6 +3,7 @@ use crate::Error; use actix_web::dev::ResourceMap; use actix_web::HttpRequest; use async_trait::async_trait; +use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; use rustical_store::auth::User; @@ -13,8 +14,8 @@ use strum::{EnumString, VariantNames}; #[strum(serialize_all = "kebab-case")] pub enum RootPropName { Resourcetype, - // Defined by RFC 5397 CurrentUserPrincipal, + CurrentUserPrivilegeSet, } #[derive(Deserialize, Serialize, Default, Debug)] @@ -31,6 +32,10 @@ pub enum RootProp { // WebDAV Current Principal Extension (RFC 5397) CurrentUserPrincipal(HrefElement), + + // WebDAV Access Control Protocol (RFC 3477) + CurrentUserPrivilegeSet(UserPrivilegeSet), + #[serde(other)] Invalid, } @@ -42,9 +47,7 @@ impl InvalidProperty for RootProp { } #[derive(Clone)] -pub struct RootResource { - principal: String, -} +pub struct RootResource; impl Resource for RootResource { type PropName = RootPropName; @@ -54,13 +57,17 @@ impl Resource for RootResource { fn get_prop( &self, rmap: &ResourceMap, + user: &User, prop: Self::PropName, ) -> Result { Ok(match prop { RootPropName::Resourcetype => RootProp::Resourcetype(Resourcetype::default()), RootPropName::CurrentUserPrincipal => RootProp::CurrentUserPrincipal(HrefElement::new( - PrincipalResource::get_url(rmap, vec![&self.principal]).unwrap(), + PrincipalResource::get_url(rmap, vec![&user.id]).unwrap(), )), + RootPropName::CurrentUserPrivilegeSet => { + RootProp::CurrentUserPrivilegeSet(self.get_user_privileges(user)?) + } }) } @@ -68,6 +75,10 @@ impl Resource for RootResource { fn resource_name() -> &'static str { "caldav_root" } + + fn get_user_privileges(&self, _user: &User) -> Result { + Ok(UserPrivilegeSet::all()) + } } pub struct RootResourceService; @@ -86,8 +97,8 @@ impl ResourceService for RootResourceService { Ok(Self) } - async fn get_resource(&self, user: User) -> Result { - Ok(RootResource { principal: user.id }) + async fn get_resource(&self) -> Result { + Ok(RootResource) } async fn save_resource(&self, _file: Self::Resource) -> Result<(), Self::Error> { diff --git a/crates/carddav/src/address_object/resource.rs b/crates/carddav/src/address_object/resource.rs index e71101f..34deb03 100644 --- a/crates/carddav/src/address_object/resource.rs +++ b/crates/carddav/src/address_object/resource.rs @@ -1,8 +1,12 @@ -use crate::Error; +use crate::{principal::PrincipalResource, Error}; use actix_web::{dev::ResourceMap, web::Data, HttpRequest}; use async_trait::async_trait; use derive_more::derive::{From, Into}; -use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; +use rustical_dav::{ + privileges::UserPrivilegeSet, + resource::{InvalidProperty, Resource, ResourceService}, + xml::HrefElement, +}; use rustical_store::{auth::User, AddressObject, AddressbookStore}; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -24,6 +28,9 @@ pub enum AddressObjectPropName { Getetag, AddressData, Getcontenttype, + CurrentUserPrincipal, + Owner, + CurrentUserPrivilegeSet, } #[derive(Deserialize, Serialize, Debug, Clone)] @@ -33,6 +40,13 @@ pub enum AddressObjectProp { Getetag(String), Getcontenttype(String), + // WebDAV Current Principal Extension (RFC 5397) + CurrentUserPrincipal(HrefElement), + + // WebDAV Access Control (RFC 3744) + Owner(HrefElement), + CurrentUserPrivilegeSet(UserPrivilegeSet), + // CalDAV (RFC 4791) #[serde(rename = "CARD:address-data")] AddressData(String), @@ -47,7 +61,10 @@ impl InvalidProperty for AddressObjectProp { } #[derive(Clone, From, Into)] -pub struct AddressObjectResource(AddressObject); +pub struct AddressObjectResource { + pub object: AddressObject, + pub principal: String, +} impl Resource for AddressObjectResource { type PropName = AddressObjectPropName; @@ -56,17 +73,27 @@ impl Resource for AddressObjectResource { fn get_prop( &self, - _rmap: &ResourceMap, + rmap: &ResourceMap, + user: &User, prop: Self::PropName, ) -> Result { Ok(match prop { - AddressObjectPropName::Getetag => AddressObjectProp::Getetag(self.0.get_etag()), + AddressObjectPropName::Getetag => AddressObjectProp::Getetag(self.object.get_etag()), AddressObjectPropName::AddressData => { - AddressObjectProp::AddressData(self.0.get_vcf().to_owned()) + AddressObjectProp::AddressData(self.object.get_vcf().to_owned()) } AddressObjectPropName::Getcontenttype => { AddressObjectProp::Getcontenttype("text/vcard;charset=utf-8".to_owned()) } + AddressObjectPropName::CurrentUserPrincipal => AddressObjectProp::CurrentUserPrincipal( + HrefElement::new(PrincipalResource::get_principal_url(rmap, &user.id)), + ), + AddressObjectPropName::Owner => AddressObjectProp::Owner( + PrincipalResource::get_principal_url(rmap, &self.principal).into(), + ), + AddressObjectPropName::CurrentUserPrivilegeSet => { + AddressObjectProp::CurrentUserPrivilegeSet(UserPrivilegeSet::all()) + } }) } @@ -74,6 +101,10 @@ impl Resource for AddressObjectResource { fn resource_name() -> &'static str { "carddav_address_object" } + + fn get_user_privileges(&self, user: &User) -> Result { + Ok(UserPrivilegeSet::owner_only(self.principal == user.id)) + } } #[derive(Debug, Clone)] @@ -133,15 +164,15 @@ impl ResourceService for AddressObjectResourceSer }) } - async fn get_resource(&self, user: User) -> Result { - if self.principal != user.id { - return Err(Error::Unauthorized); - } - let event = self + async fn get_resource(&self) -> Result { + let object = self .addr_store .get_object(&self.principal, &self.cal_id, &self.object_id) .await?; - Ok(event.into()) + Ok(AddressObjectResource { + object, + principal: self.principal.to_owned(), + }) } async fn save_resource(&self, _file: Self::Resource) -> Result<(), Self::Error> { diff --git a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs index 6b38671..1aee58c 100644 --- a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs +++ b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs @@ -16,7 +16,7 @@ use rustical_dav::{ MultistatusElement, }, }; -use rustical_store::{AddressObject, AddressbookStore}; +use rustical_store::{auth::User, AddressObject, AddressbookStore}; use serde::Deserialize; #[derive(Deserialize, Clone, Debug)] @@ -64,6 +64,7 @@ pub async fn get_objects_addressbook_multiget( pub async fn handle_addressbook_multiget( addr_multiget: AddressbookMultigetRequest, req: HttpRequest, + user: &User, principal: &str, cal_id: &str, addr_store: &AS, @@ -92,11 +93,13 @@ pub async fn handle_addressbook_multiget( let mut responses = Vec::new(); for object in objects { let path = format!("{}/{}", req.path(), object.get_id()); - responses.push(AddressObjectResource::from(object).propfind( - &path, - props.clone(), - req.resource_map(), - )?); + responses.push( + AddressObjectResource { + object, + principal: principal.to_owned(), + } + .propfind(&path, props.clone(), user, req.resource_map())?, + ); } let not_found_responses = not_found diff --git a/crates/carddav/src/addressbook/methods/report/mod.rs b/crates/carddav/src/addressbook/methods/report/mod.rs index 34b21ec..be1d956 100644 --- a/crates/carddav/src/addressbook/methods/report/mod.rs +++ b/crates/carddav/src/addressbook/methods/report/mod.rs @@ -47,6 +47,7 @@ pub async fn route_report_addressbook( handle_addressbook_multiget( addr_multiget, req, + &user, &principal, &addressbook_id, addr_store.as_ref(), @@ -57,6 +58,7 @@ pub async fn route_report_addressbook( handle_sync_collection( sync_collection, req, + &user, &principal, &addressbook_id, addr_store.as_ref(), diff --git a/crates/carddav/src/addressbook/methods/report/sync_collection.rs b/crates/carddav/src/addressbook/methods/report/sync_collection.rs index bfae058..7bbab05 100644 --- a/crates/carddav/src/addressbook/methods/report/sync_collection.rs +++ b/crates/carddav/src/addressbook/methods/report/sync_collection.rs @@ -12,6 +12,7 @@ use rustical_dav::{ }, }; use rustical_store::{ + auth::User, synctoken::{format_synctoken, parse_synctoken}, AddressbookStore, }; @@ -42,6 +43,7 @@ pub struct SyncCollectionRequest { pub async fn handle_sync_collection( sync_collection: SyncCollectionRequest, req: HttpRequest, + user: &User, principal: &str, addressbook_id: &str, addr_store: &AS, @@ -69,11 +71,13 @@ pub async fn handle_sync_collection( vec![principal, addressbook_id, &object.get_id()], ) .unwrap(); - responses.push(AddressObjectResource::from(object).propfind( - &path, - props.clone(), - req.resource_map(), - )?); + responses.push( + AddressObjectResource { + object, + principal: principal.to_owned(), + } + .propfind(&path, props.clone(), user, req.resource_map())?, + ); } for object_id in deleted_objects { diff --git a/crates/carddav/src/addressbook/prop.rs b/crates/carddav/src/addressbook/prop.rs index 7c6b9c8..d876983 100644 --- a/crates/carddav/src/addressbook/prop.rs +++ b/crates/carddav/src/addressbook/prop.rs @@ -41,55 +41,6 @@ pub struct Resourcetype { collection: (), } -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum UserPrivilege { - Read, - ReadAcl, - Write, - WriteAcl, - WriteContent, - ReadCurrentUserPrivilegeSet, - Bind, - Unbind, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct UserPrivilegeWrapper { - #[serde(rename = "$value")] - privilege: UserPrivilege, -} - -impl From for UserPrivilegeWrapper { - fn from(value: UserPrivilege) -> Self { - Self { privilege: value } - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct UserPrivilegeSet { - privilege: Vec, -} - -impl Default for UserPrivilegeSet { - fn default() -> Self { - Self { - privilege: vec![ - UserPrivilege::Read.into(), - UserPrivilege::ReadAcl.into(), - UserPrivilege::Write.into(), - UserPrivilege::WriteAcl.into(), - UserPrivilege::WriteContent.into(), - UserPrivilege::ReadCurrentUserPrivilegeSet.into(), - UserPrivilege::Bind.into(), - UserPrivilege::Unbind.into(), - ], - } - } -} - #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub enum ReportMethod { diff --git a/crates/carddav/src/addressbook/resource.rs b/crates/carddav/src/addressbook/resource.rs index de966b9..2f574fb 100644 --- a/crates/carddav/src/addressbook/resource.rs +++ b/crates/carddav/src/addressbook/resource.rs @@ -1,6 +1,6 @@ use super::methods::mkcol::route_mkcol; use super::methods::report::route_report_addressbook; -use super::prop::{Resourcetype, SupportedAddressData, SupportedReportSet, UserPrivilegeSet}; +use super::prop::{Resourcetype, SupportedAddressData, SupportedReportSet}; use crate::address_object::resource::AddressObjectResource; use crate::principal::PrincipalResource; use crate::Error; @@ -10,6 +10,7 @@ use actix_web::web; use actix_web::{web::Data, HttpRequest}; use async_trait::async_trait; use derive_more::derive::{From, Into}; +use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; use rustical_store::auth::User; @@ -99,6 +100,7 @@ impl Resource for AddressbookResource { fn get_prop( &self, rmap: &ResourceMap, + user: &User, prop: Self::PropName, ) -> Result { Ok(match prop { @@ -107,12 +109,15 @@ impl Resource for AddressbookResource { } AddressbookPropName::CurrentUserPrincipal => { AddressbookProp::CurrentUserPrincipal(HrefElement::new( - PrincipalResource::get_url(rmap, vec![&self.0.principal]).unwrap(), + PrincipalResource::get_principal_url(rmap, &self.0.principal), )) } - AddressbookPropName::Owner => AddressbookProp::Owner(HrefElement::new( - PrincipalResource::get_url(rmap, vec![&self.0.principal]).unwrap(), - )), + AddressbookPropName::Owner => AddressbookProp::Owner( + PrincipalResource::get_principal_url(rmap, &self.0.principal).into(), + ), + AddressbookPropName::CurrentUserPrivilegeSet => { + AddressbookProp::CurrentUserPrivilegeSet(UserPrivilegeSet::all()) + } AddressbookPropName::Displayname => { AddressbookProp::Displayname(self.0.displayname.clone()) } @@ -120,9 +125,6 @@ impl Resource for AddressbookResource { AddressbookProp::Getcontenttype("text/vcard;charset=utf-8".to_owned()) } AddressbookPropName::MaxResourceSize => AddressbookProp::MaxResourceSize(10000000), - AddressbookPropName::CurrentUserPrivilegeSet => { - AddressbookProp::CurrentUserPrivilegeSet(UserPrivilegeSet::default()) - } AddressbookPropName::SupportedReportSet => { AddressbookProp::SupportedReportSet(SupportedReportSet::default()) } @@ -188,6 +190,10 @@ impl Resource for AddressbookResource { fn resource_name() -> &'static str { "carddav_addressbook" } + + fn get_user_privileges(&self, user: &User) -> Result { + Ok(UserPrivilegeSet::owner_only(self.0.principal == user.id)) + } } #[async_trait(?Send)] @@ -197,10 +203,7 @@ impl ResourceService for AddressbookResourceServi type Resource = AddressbookResource; type Error = Error; - async fn get_resource(&self, user: User) -> Result { - if self.principal != user.id { - return Err(Error::Unauthorized); - } + async fn get_resource(&self) -> Result { let addressbook = self .addr_store .get_addressbook(&self.principal, &self.addressbook_id) @@ -225,7 +228,10 @@ impl ResourceService for AddressbookResourceServi vec![&self.principal, &self.addressbook_id, object.get_id()], ) .unwrap(), - object.into(), + AddressObjectResource { + object, + principal: self.principal.to_owned(), + }, ) }) .collect()) diff --git a/crates/carddav/src/principal/mod.rs b/crates/carddav/src/principal/mod.rs index a01f7a4..c239905 100644 --- a/crates/carddav/src/principal/mod.rs +++ b/crates/carddav/src/principal/mod.rs @@ -4,6 +4,7 @@ use actix_web::dev::ResourceMap; use actix_web::web::Data; use actix_web::HttpRequest; use async_trait::async_trait; +use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; use rustical_store::auth::User; @@ -68,6 +69,12 @@ pub enum PrincipalPropName { PrincipalAddress, } +impl PrincipalResource { + pub fn get_principal_url(rmap: &ResourceMap, principal: &str) -> String { + Self::get_url(rmap, vec![principal]).unwrap() + } +} + impl Resource for PrincipalResource { type PropName = PrincipalPropName; type Prop = PrincipalProp; @@ -76,9 +83,10 @@ impl Resource for PrincipalResource { fn get_prop( &self, rmap: &ResourceMap, + user: &User, prop: Self::PropName, ) -> Result { - let principal_href = HrefElement::new(Self::get_url(rmap, vec![&self.principal]).unwrap()); + let principal_href = HrefElement::new(Self::get_principal_url(rmap, &self.principal)); Ok(match prop { PrincipalPropName::Resourcetype => PrincipalProp::Resourcetype(Resourcetype::default()), @@ -97,6 +105,10 @@ impl Resource for PrincipalResource { fn resource_name() -> &'static str { "carddav_principal" } + + fn get_user_privileges(&self, user: &User) -> Result { + Ok(UserPrivilegeSet::owner_only(self.principal == user.id)) + } } #[async_trait(?Send)] @@ -122,10 +134,7 @@ impl ResourceService for PrincipalResourceService< }) } - async fn get_resource(&self, user: User) -> Result { - if self.principal != user.id { - return Err(Error::Unauthorized); - } + async fn get_resource(&self) -> Result { Ok(PrincipalResource { principal: self.principal.to_owned(), }) diff --git a/crates/carddav/src/root/mod.rs b/crates/carddav/src/root/mod.rs index 41b4e0d..ec4d3ee 100644 --- a/crates/carddav/src/root/mod.rs +++ b/crates/carddav/src/root/mod.rs @@ -3,6 +3,7 @@ use crate::Error; use actix_web::dev::ResourceMap; use actix_web::HttpRequest; use async_trait::async_trait; +use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; use rustical_store::auth::User; @@ -13,8 +14,8 @@ use strum::{EnumString, VariantNames}; #[strum(serialize_all = "kebab-case")] pub enum RootPropName { Resourcetype, - // Defined by RFC 5397 CurrentUserPrincipal, + CurrentUserPrivilegeSet, } #[derive(Deserialize, Serialize, Default, Debug)] @@ -31,7 +32,11 @@ pub enum RootProp { // WebDAV Current Principal Extension (RFC 5397) CurrentUserPrincipal(HrefElement), - #[serde(other)] + + // WebDAV Access Control Protocol (RFC 3477) + CurrentUserPrivilegeSet(UserPrivilegeSet), + + #[serde(untagged)] Invalid, } @@ -42,9 +47,7 @@ impl InvalidProperty for RootProp { } #[derive(Clone)] -pub struct RootResource { - principal: String, -} +pub struct RootResource; impl Resource for RootResource { type PropName = RootPropName; @@ -54,13 +57,17 @@ impl Resource for RootResource { fn get_prop( &self, rmap: &ResourceMap, + user: &User, prop: Self::PropName, ) -> Result { Ok(match prop { RootPropName::Resourcetype => RootProp::Resourcetype(Resourcetype::default()), - RootPropName::CurrentUserPrincipal => RootProp::CurrentUserPrincipal(HrefElement::new( - PrincipalResource::get_url(rmap, vec![&self.principal]).unwrap(), - )), + RootPropName::CurrentUserPrincipal => RootProp::CurrentUserPrincipal( + PrincipalResource::get_principal_url(rmap, &user.id).into(), + ), + RootPropName::CurrentUserPrivilegeSet => { + RootProp::CurrentUserPrivilegeSet(self.get_user_privileges(user)?) + } }) } @@ -68,6 +75,10 @@ impl Resource for RootResource { fn resource_name() -> &'static str { "carddav_root" } + + fn get_user_privileges(&self, _user: &User) -> Result { + Ok(UserPrivilegeSet::all()) + } } pub struct RootResourceService; @@ -86,8 +97,8 @@ impl ResourceService for RootResourceService { Ok(Self) } - async fn get_resource(&self, user: User) -> Result { - Ok(RootResource { principal: user.id }) + async fn get_resource(&self) -> Result { + Ok(RootResource) } async fn save_resource(&self, _file: Self::Resource) -> Result<(), Self::Error> { diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index 7245c56..753b78f 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -2,6 +2,7 @@ pub mod depth_header; pub mod error; pub mod methods; pub mod namespace; +pub mod privileges; pub mod resource; pub mod xml; diff --git a/crates/dav/src/methods/delete.rs b/crates/dav/src/methods/delete.rs index 2fb14a4..c77df51 100644 --- a/crates/dav/src/methods/delete.rs +++ b/crates/dav/src/methods/delete.rs @@ -1,4 +1,7 @@ +use crate::privileges::UserPrivilege; +use crate::resource::Resource; use crate::resource::ResourceService; +use crate::Error; use actix_web::web::Path; use actix_web::HttpRequest; use actix_web::HttpResponse; @@ -8,7 +11,7 @@ use rustical_store::auth::User; pub async fn route_delete( path_components: Path, req: HttpRequest, - _user: User, + user: User, ) -> Result { let path_components = path_components.into_inner(); @@ -19,6 +22,13 @@ pub async fn route_delete( .unwrap_or(false); let resource_service = R::new(&req, path_components.clone()).await?; + let resource = resource_service.get_resource().await?; + let privileges = resource.get_user_privileges(&user)?; + if !privileges.has(&UserPrivilege::Write) { + // TODO: Actually the spec wants us to look whether we have unbind access in the parent + // collection + return Err(Error::Unauthorized.into()); + } resource_service.delete_resource(!no_trash).await?; Ok(HttpResponse::Ok().body("")) diff --git a/crates/dav/src/methods/propfind.rs b/crates/dav/src/methods/propfind.rs index f1d3e50..17334a3 100644 --- a/crates/dav/src/methods/propfind.rs +++ b/crates/dav/src/methods/propfind.rs @@ -1,4 +1,5 @@ use crate::depth_header::Depth; +use crate::privileges::UserPrivilege; use crate::resource::Resource; use crate::resource::ResourceService; use crate::xml::multistatus::PropstatWrapper; @@ -52,6 +53,12 @@ pub async fn route_propfind( > { let resource_service = R::new(&req, path_components.into_inner()).await?; + let resource = resource_service.get_resource().await?; + let privileges = resource.get_user_privileges(&user)?; + if !privileges.has(&UserPrivilege::Read) { + return Err(Error::Unauthorized.into()); + } + // A request body is optional. If empty we MUST return all props let propfind: PropfindElement = if !body.is_empty() { quick_xml::de::from_str(&body).map_err(Error::XmlDeserializationError)? @@ -75,12 +82,16 @@ pub async fn route_propfind( let mut member_responses = Vec::new(); if depth != Depth::Zero { for (path, member) in resource_service.get_members(req.resource_map()).await? { - member_responses.push(member.propfind(&path, props.clone(), req.resource_map())?); + member_responses.push(member.propfind( + &path, + props.clone(), + &user, + req.resource_map(), + )?); } } - let resource = resource_service.get_resource(user).await?; - let response = resource.propfind(req.path(), props, req.resource_map())?; + let response = resource.propfind(req.path(), props, &user, req.resource_map())?; Ok(MultistatusElement { responses: vec![response], diff --git a/crates/dav/src/methods/proppatch.rs b/crates/dav/src/methods/proppatch.rs index 1e47689..efca333 100644 --- a/crates/dav/src/methods/proppatch.rs +++ b/crates/dav/src/methods/proppatch.rs @@ -1,3 +1,4 @@ +use crate::privileges::UserPrivilege; use crate::resource::InvalidProperty; use crate::resource::Resource; use crate::resource::ResourceService; @@ -76,7 +77,11 @@ pub async fn route_proppatch( }) .collect(); - let mut resource = resource_service.get_resource(user).await?; + let mut resource = resource_service.get_resource().await?; + let privileges = resource.get_user_privileges(&user)?; + if !privileges.has(&UserPrivilege::Write) { + return Err(Error::Unauthorized.into()); + } let mut props_ok = Vec::new(); let mut props_conflict = Vec::new(); diff --git a/crates/dav/src/privileges.rs b/crates/dav/src/privileges.rs new file mode 100644 index 0000000..d0b15a1 --- /dev/null +++ b/crates/dav/src/privileges.rs @@ -0,0 +1,78 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, Hash, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum UserPrivilege { + Read, + Write, + WriteProperties, + WriteContent, + ReadAcl, + ReadCurrentUserPrivilegeSet, + WriteAcl, + All, +} + +impl Serialize for UserPrivilegeSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + #[derive(Serialize)] + #[serde(rename_all = "kebab-case")] + pub struct UserPrivilegeWrapper<'a> { + #[serde(rename = "$value")] + privilege: &'a UserPrivilege, + } + #[derive(Serialize)] + #[serde(rename_all = "kebab-case")] + pub struct FakeUserPrivilegeSet<'a> { + #[serde(rename = "privilege")] + privileges: Vec>, + } + FakeUserPrivilegeSet { + privileges: self + .privileges + .iter() + .map(|privilege| UserPrivilegeWrapper { privilege }) + .collect(), + } + .serialize(serializer) + } +} + +// TODO: implement Deserialize once we need it +#[derive(Debug, Clone, Deserialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct UserPrivilegeSet { + privileges: HashSet, +} + +impl UserPrivilegeSet { + pub fn has(&self, privilege: &UserPrivilege) -> bool { + self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All) + } + + pub fn all() -> Self { + Self { + privileges: HashSet::from([UserPrivilege::All]), + } + } + + pub fn owner_only(is_owner: bool) -> Self { + if is_owner { + Self::all() + } else { + Self::default() + } + } +} + +impl From<[UserPrivilege; N]> for UserPrivilegeSet { + fn from(privileges: [UserPrivilege; N]) -> Self { + Self { + privileges: HashSet::from(privileges), + } + } +} diff --git a/crates/dav/src/resource.rs b/crates/dav/src/resource.rs index be955e5..92e4ec0 100644 --- a/crates/dav/src/resource.rs +++ b/crates/dav/src/resource.rs @@ -1,4 +1,5 @@ use crate::methods::{route_delete, route_propfind, route_proppatch}; +use crate::privileges::UserPrivilegeSet; use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper}; use crate::xml::{multistatus::ResponseElement, TagList}; use crate::Error; @@ -25,8 +26,12 @@ pub trait Resource: Clone { Self::PropName::VARIANTS } - fn get_prop(&self, rmap: &ResourceMap, prop: Self::PropName) - -> Result; + fn get_prop( + &self, + rmap: &ResourceMap, + user: &User, + prop: Self::PropName, + ) -> Result; fn set_prop(&mut self, _prop: Self::Prop) -> Result<(), crate::Error> { Err(crate::Error::PropReadOnly) @@ -53,10 +58,13 @@ pub trait Resource: Clone { .to_owned()) } + fn get_user_privileges(&self, user: &User) -> Result; + fn propfind( &self, path: &str, mut props: Vec<&str>, + user: &User, rmap: &ResourceMap, ) -> Result>, Self::Error> { if props.contains(&"propname") { @@ -104,7 +112,7 @@ pub trait Resource: Clone { let prop_responses = valid_props .into_iter() - .map(|prop| self.get_prop(rmap, prop)) + .map(|prop| self.get_prop(rmap, user, prop)) .collect::, Self::Error>>()?; let mut propstats = vec![PropstatWrapper::Normal(PropstatElement { @@ -154,7 +162,7 @@ pub trait ResourceService: Sized + 'static { Ok(vec![]) } - async fn get_resource(&self, user: User) -> Result; + async fn get_resource(&self) -> Result; async fn save_resource(&self, file: Self::Resource) -> Result<(), Self::Error>; async fn delete_resource(&self, _use_trashbin: bool) -> Result<(), Self::Error> { Err(crate::Error::Unauthorized.into()) diff --git a/crates/dav/src/xml/mod.rs b/crates/dav/src/xml/mod.rs index 1e48f1e..6940de7 100644 --- a/crates/dav/src/xml/mod.rs +++ b/crates/dav/src/xml/mod.rs @@ -2,13 +2,14 @@ pub mod multistatus; pub mod tag_list; pub mod tag_name; +use derive_more::derive::From; pub use multistatus::MultistatusElement; pub use tag_list::TagList; pub use tag_name::TagName; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, From)] pub struct HrefElement { pub href: String, }