From 095c501a66ea1ffd2924fedbaabd3f4f48ec306c Mon Sep 17 00:00:00 2001 From: mdecimus Date: Fri, 9 May 2025 15:47:09 +0200 Subject: [PATCH] WebDAV permissions and logging (closes #1362) --- Cargo.lock | 2 + crates/common/src/auth/access_token.rs | 8 +- crates/dav-proto/Cargo.toml | 2 + crates/dav-proto/src/lib.rs | 92 +++++- crates/dav-proto/src/parser/mod.rs | 20 +- crates/dav-proto/src/schema/response.rs | 79 +++++ crates/dav/src/calendar/copy_move.rs | 6 +- crates/dav/src/calendar/delete.rs | 25 +- crates/dav/src/calendar/freebusy.rs | 4 +- crates/dav/src/calendar/get.rs | 6 +- crates/dav/src/calendar/mkcol.rs | 12 +- crates/dav/src/calendar/proppatch.rs | 6 +- crates/dav/src/calendar/query.rs | 10 +- crates/dav/src/calendar/update.rs | 8 +- crates/dav/src/card/copy_move.rs | 6 +- crates/dav/src/card/delete.rs | 8 +- crates/dav/src/card/get.rs | 6 +- crates/dav/src/card/mkcol.rs | 7 +- crates/dav/src/card/proppatch.rs | 6 +- crates/dav/src/card/query.rs | 4 +- crates/dav/src/card/update.rs | 8 +- crates/dav/src/common/acl.rs | 8 +- crates/dav/src/common/lock.rs | 8 +- crates/dav/src/common/mod.rs | 12 +- crates/dav/src/common/propfind.rs | 50 ++- crates/dav/src/file/copy_move.rs | 33 +- crates/dav/src/file/delete.rs | 6 +- crates/dav/src/file/get.rs | 11 +- crates/dav/src/file/mkcol.rs | 6 +- crates/dav/src/file/proppatch.rs | 6 +- crates/dav/src/file/update.rs | 8 +- crates/dav/src/lib.rs | 24 ++ crates/dav/src/principal/matching.rs | 4 +- crates/dav/src/request.rs | 421 +++++++++++++++++------- crates/directory/src/core/mod.rs | 46 +++ crates/directory/src/core/principal.rs | 40 +++ crates/directory/src/lib.rs | 48 +++ crates/groupware/src/calendar/dates.rs | 14 +- crates/groupware/src/lib.rs | 9 + crates/http/src/request.rs | 5 +- crates/imap/src/op/acl.rs | 4 +- crates/managesieve/src/op/mod.rs | 4 +- crates/trc/src/event/description.rs | 71 ++++ crates/trc/src/event/level.rs | 2 + crates/trc/src/lib.rs | 32 ++ crates/trc/src/serializers/binary.rs | 40 ++- tests/src/directory/internal.rs | 25 +- tests/src/webdav/basic.rs | 5 +- tests/src/webdav/copy_move.rs | 22 ++ tests/src/webdav/mod.rs | 15 +- 50 files changed, 1031 insertions(+), 273 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d169c820..97711089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1759,6 +1759,7 @@ version = "0.11.7" dependencies = [ "calcard", "chrono", + "compact_str", "hashify", "hyper 1.6.0", "mail-parser", @@ -1766,6 +1767,7 @@ dependencies = [ "rkyv 0.8.10", "serde", "serde_json", + "trc", ] [[package]] diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 63ba9aea..299d778a 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -462,9 +462,9 @@ impl AccessToken { self.permissions.get(permission.id()) } - pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> { + pub fn assert_has_permission(&self, permission: Permission) -> trc::Result { if self.has_permission(permission) { - Ok(()) + Ok(true) } else { Err(trc::SecurityEvent::Unauthorized .into_err() @@ -518,6 +518,10 @@ impl AccessToken { }) } + pub fn has_account_access(&self, to_account_id: u32) -> bool { + self.is_member(to_account_id) || self.access_to.iter().any(|(id, _)| *id == to_account_id) + } + pub fn assert_has_access( &self, to_account_id: Id, diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index 94f3629e..f6169858 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -4,6 +4,7 @@ version = "0.11.7" edition = "2021" [dependencies] +trc = { path = "../trc" } hashify = "0.2.6" quick-xml = "0.37.2" calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] } @@ -11,6 +12,7 @@ mail-parser = "0.10.2" hyper = "1.6.0" rkyv = { version = "0.8.10", features = ["little_endian"] } chrono = { version = "0.4.40", features = ["serde"], optional = true } +compact_str = "0.9.0" [dev-dependencies] calcard = { path = "/Users/me/code/calcard", features = ["serde", "rkyv"] } diff --git a/crates/dav-proto/src/lib.rs b/crates/dav-proto/src/lib.rs index 6367d73b..d5841c06 100644 --- a/crates/dav-proto/src/lib.rs +++ b/crates/dav-proto/src/lib.rs @@ -4,6 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::{CompactString, ToCompactString}; +use trc::Value; + pub mod parser; pub mod requests; pub mod responses; @@ -50,7 +53,7 @@ pub struct ResourceState> { pub state_token: T, } -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] pub enum Return { Minimal, Representation, @@ -91,16 +94,87 @@ pub enum Depth { None, } +impl From<&RequestHeaders<'_>> for Value { + fn from(headers: &RequestHeaders<'_>) -> Self { + let mut values = Vec::with_capacity(4); + if headers.depth != Depth::None { + values.push(Value::String(CompactString::const_new("Depth"))); + values.push(match headers.depth { + Depth::Zero => Value::Int(0), + Depth::One => Value::Int(1), + Depth::Infinity => Value::String(CompactString::const_new("infinity")), + Depth::None => Value::None, + }); + } + if headers.timeout != Timeout::None { + values.push(Value::String(CompactString::const_new("Timeout"))); + values.push(match headers.timeout { + Timeout::Infinite => Value::String(CompactString::const_new("infinite")), + Timeout::Second(n) => Value::Int(n as i64), + Timeout::None => Value::None, + }); + } + for (name, header_value) in [ + ("Content-Type", headers.content_type), + ("Destination", headers.destination), + ("Lock-Token", headers.lock_token), + ] { + if let Some(value) = header_value { + values.push(CompactString::const_new(name).into()); + values.push(value.to_compact_string().into()); + } + } + for (name, is_set) in [ + ("Overwrite", headers.overwrite_fail), + ("No-Timezones", headers.no_timezones), + ("Depth-No-Root", headers.depth_no_root), + ] { + if is_set { + values.push(CompactString::const_new(name).into()); + } + } + for if_ in &headers.if_ { + values.push(CompactString::const_new("If").into()); + let mut if_values = Vec::with_capacity(if_.list.len() * 2 + 1); + if let Some(resource) = if_.resource { + if_values.push(Value::String(resource.to_compact_string())); + } + for condition in &if_.list { + match condition { + Condition::StateToken { is_not, token } => { + if *is_not { + if_values.push(Value::String(CompactString::const_new("!State-Token"))); + } else { + if_values.push(Value::String(CompactString::const_new("State-Token"))); + } + if_values.push(Value::String(token.to_compact_string())); + } + Condition::ETag { is_not, tag } => { + if *is_not { + if_values.push(Value::String(CompactString::const_new("!ETag"))); + } else { + if_values.push(Value::String(CompactString::const_new("ETag"))); + } + if_values.push(Value::String(tag.to_compact_string())); + } + Condition::Exists { is_not } => { + if *is_not { + if_values.push(Value::String(CompactString::const_new("!Exists"))); + } else { + if_values.push(Value::String(CompactString::const_new("Exists"))); + } + } + } + } + values.push(Value::Array(if_values)); + } + + Value::Array(values) + } +} + /* - Allow: OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, COPY, MOVE - Allow: MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, ACL - DAV: 1, 2, 3, access-control, extended-mkcol -calendar-no-timezone - - -TODO: - Implemented: diff --git a/crates/dav-proto/src/parser/mod.rs b/crates/dav-proto/src/parser/mod.rs index ca10f3fc..aa79a521 100644 --- a/crates/dav-proto/src/parser/mod.rs +++ b/crates/dav-proto/src/parser/mod.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::borrow::Cow; +use std::{ + borrow::Cow, + fmt::{Display, Formatter}, +}; use quick_xml::events::BytesStart; use tokenizer::Tokenizer; @@ -152,3 +155,18 @@ impl Default for RawElement<'_> { RawElement(BytesStart::new("")) } } + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Error::Xml(err) => write!(f, "XML error: {}", err), + Error::UnexpectedToken { expected, found } => { + write!(f, "Unexpected token: {found:?}")?; + if let Some(expected) = expected { + write!(f, ", expected: {expected:?}")?; + } + Ok(()) + } + } + } +} diff --git a/crates/dav-proto/src/schema/response.rs b/crates/dav-proto/src/schema/response.rs index 77e626a8..d6661bd1 100644 --- a/crates/dav-proto/src/schema/response.rs +++ b/crates/dav-proto/src/schema/response.rs @@ -288,6 +288,85 @@ impl MultiStatus { } } +impl BaseCondition { + pub fn display_name(&self) -> &'static str { + match self { + BaseCondition::NoConflictingLock(_) => "NoConflictingLock", + BaseCondition::CannotModifyProtectedProperty => "CannotModifyProtectedProperty", + BaseCondition::LockTokenSubmitted(_) => "LockTokenSubmitted", + BaseCondition::LockTokenMatchesRequestUri => "LockTokenMatchesRequestUri", + BaseCondition::NoExternalEntities => "NoExternalEntities", + BaseCondition::PreservedLiveProperties => "PreservedLiveProperties", + BaseCondition::PropFindFiniteDepth => "PropFindFiniteDepth", + BaseCondition::ResourceMustBeNull => "ResourceMustBeNull", + BaseCondition::NeedPrivileges(_) => "NeedPrivileges", + BaseCondition::NoAceConflict => "NoAceConflict", + BaseCondition::NoProtectedAceConflict => "NoProtectedAceConflict", + BaseCondition::NoInheritedAceConflict => "NoInheritedAceConflict", + BaseCondition::LimitedNumberOfAces => "LimitedNumberOfAces", + BaseCondition::DenyBeforeGrant => "DenyBeforeGrant", + BaseCondition::GrantOnly => "GrantOnly", + BaseCondition::NoInvert => "NoInvert", + BaseCondition::NoAbstract => "NoAbstract", + BaseCondition::NotSupportedPrivilege => "NotSupportedPrivilege", + BaseCondition::MissingRequiredPrincipal => "MissingRequiredPrincipal", + BaseCondition::RecognizedPrincipal => "RecognizedPrincipal", + BaseCondition::AllowedPrincipal => "AllowedPrincipal", + BaseCondition::NumberOfMatchesWithinLimit => "NumberOfMatchesWithinLimit", + BaseCondition::QuotaNotExceeded => "QuotaNotExceeded", + BaseCondition::ValidResourceType => "ValidResourceType", + BaseCondition::ValidSyncToken => "ValidSyncToken", + } + } +} + +impl CalCondition { + pub fn display_name(&self) -> &'static str { + match self { + CalCondition::CalendarCollectionLocationOk => "CalendarCollectionLocationOk", + CalCondition::ValidCalendarData => "ValidCalendarData", + CalCondition::ValidFilter => "ValidFilter", + CalCondition::ValidCalendarObjectResource => "ValidCalendarObjectResource", + CalCondition::ValidTimezone => "ValidTimezone", + CalCondition::NoUidConflict(_) => "NoUidConflict", + CalCondition::InitializeCalendarCollection => "InitializeCalendarCollection", + CalCondition::SupportedCalendarData => "SupportedCalendarData", + CalCondition::SupportedFilter(_) => "SupportedFilter", + CalCondition::SupportedCollation(_) => "SupportedCollation", + CalCondition::MinDateTime => "MinDateTime", + CalCondition::MaxDateTime => "MaxDateTime", + CalCondition::MaxResourceSize(_) => "MaxResourceSize", + CalCondition::MaxInstances => "MaxInstances", + CalCondition::MaxAttendeesPerInstance => "MaxAttendeesPerInstance", + } + } +} + +impl CardCondition { + pub fn display_name(&self) -> &'static str { + match self { + CardCondition::SupportedAddressData => "SupportedAddressData", + CardCondition::SupportedAddressDataConversion => "SupportedAddressDataConversion", + CardCondition::SupportedFilter(_) => "SupportedFilter", + CardCondition::SupportedCollation(_) => "SupportedCollation", + CardCondition::ValidAddressData => "ValidAddressData", + CardCondition::NoUidConflict(_) => "NoUidConflict", + CardCondition::MaxResourceSize(_) => "MaxResourceSize", + CardCondition::AddressBookCollectionLocationOk => "AddressBookCollectionLocationOk", + } + } +} + +impl Condition { + pub fn display_name(&self) -> &'static str { + match self { + Condition::Base(base) => base.display_name(), + Condition::Cal(cal) => cal.display_name(), + Condition::Card(card) => card.display_name(), + } + } +} + #[cfg(test)] mod serde_impl { use super::Status; diff --git a/crates/dav/src/calendar/copy_move.rs b/crates/dav/src/calendar/copy_move.rs index eb729992..8c534d37 100644 --- a/crates/dav/src/calendar/copy_move.rs +++ b/crates/dav/src/calendar/copy_move.rs @@ -35,7 +35,7 @@ pub(crate) trait CalendarCopyMoveRequestHandler: Sync + Send { fn handle_calendar_copy_move_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_move: bool, ) -> impl Future> + Send; } @@ -44,7 +44,7 @@ impl CalendarCopyMoveRequestHandler for Server { async fn handle_calendar_copy_move_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_move: bool, ) -> crate::Result { // Validate source @@ -110,7 +110,7 @@ impl CalendarCopyMoveRequestHandler for Server { let to_resource = to_resources.by_path(destination_resource_name); self.validate_headers( access_token, - &headers, + headers, vec![ ResourceState { account_id: from_account_id, diff --git a/crates/dav/src/calendar/delete.rs b/crates/dav/src/calendar/delete.rs index cfac27b7..86eefc61 100644 --- a/crates/dav/src/calendar/delete.rs +++ b/crates/dav/src/calendar/delete.rs @@ -4,6 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{ + DavError, DavMethod, + common::{ + ETag, + lock::{LockRequestHandler, ResourceState}, + uri::DavUriResource, + }, +}; use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::RequestHeaders; use groupware::{ @@ -20,20 +28,11 @@ use jmap_proto::types::{ use store::write::BatchBuilder; use trc::AddContext; -use crate::{ - DavError, DavMethod, - common::{ - ETag, - lock::{LockRequestHandler, ResourceState}, - uri::DavUriResource, - }, -}; - pub(crate) trait CalendarDeleteRequestHandler: Sync + Send { fn handle_calendar_delete_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, ) -> impl Future> + Send; } @@ -41,7 +40,7 @@ impl CalendarDeleteRequestHandler for Server { async fn handle_calendar_delete_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, ) -> crate::Result { // Validate URI let resource = self @@ -91,7 +90,7 @@ impl CalendarDeleteRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: Collection::Calendar, @@ -139,7 +138,7 @@ impl CalendarDeleteRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: Collection::CalendarEvent, diff --git a/crates/dav/src/calendar/freebusy.rs b/crates/dav/src/calendar/freebusy.rs index 38db6403..5163ebd3 100644 --- a/crates/dav/src/calendar/freebusy.rs +++ b/crates/dav/src/calendar/freebusy.rs @@ -40,7 +40,7 @@ pub(crate) trait CalendarFreebusyRequestHandler: Sync + Send { fn handle_calendar_freebusy_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: FreeBusyQuery, ) -> impl Future> + Send; } @@ -49,7 +49,7 @@ impl CalendarFreebusyRequestHandler for Server { async fn handle_calendar_freebusy_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: FreeBusyQuery, ) -> crate::Result { // Validate URI diff --git a/crates/dav/src/calendar/get.rs b/crates/dav/src/calendar/get.rs index 2c981c64..7b919c94 100644 --- a/crates/dav/src/calendar/get.rs +++ b/crates/dav/src/calendar/get.rs @@ -28,7 +28,7 @@ pub(crate) trait CalendarGetRequestHandler: Sync + Send { fn handle_calendar_get_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_head: bool, ) -> impl Future> + Send; } @@ -37,7 +37,7 @@ impl CalendarGetRequestHandler for Server { async fn handle_calendar_get_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_head: bool, ) -> crate::Result { // Validate URI @@ -90,7 +90,7 @@ impl CalendarGetRequestHandler for Server { let etag = event_.etag(); self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: Collection::CalendarEvent, diff --git a/crates/dav/src/calendar/mkcol.rs b/crates/dav/src/calendar/mkcol.rs index dc762125..bee59262 100644 --- a/crates/dav/src/calendar/mkcol.rs +++ b/crates/dav/src/calendar/mkcol.rs @@ -9,7 +9,10 @@ use dav_proto::{ RequestHeaders, Return, schema::{Namespace, request::MkCol, response::MkColResponse}, }; -use groupware::{cache::GroupwareCache, calendar::{Calendar, CalendarPreferences}}; +use groupware::{ + cache::GroupwareCache, + calendar::{Calendar, CalendarPreferences}, +}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::collection::{Collection, SyncCollection}; @@ -31,7 +34,7 @@ pub(crate) trait CalendarMkColRequestHandler: Sync + Send { fn handle_calendar_mkcol_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: Option, ) -> impl Future> + Send; } @@ -40,7 +43,7 @@ impl CalendarMkColRequestHandler for Server { async fn handle_calendar_mkcol_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: Option, ) -> crate::Result { // Validate URI @@ -59,7 +62,6 @@ impl CalendarMkColRequestHandler for Server { .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())? - .by_path(name) .is_some() { @@ -69,7 +71,7 @@ impl CalendarMkColRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, diff --git a/crates/dav/src/calendar/proppatch.rs b/crates/dav/src/calendar/proppatch.rs index c53fd2f9..26783bb1 100644 --- a/crates/dav/src/calendar/proppatch.rs +++ b/crates/dav/src/calendar/proppatch.rs @@ -42,7 +42,7 @@ pub(crate) trait CalendarPropPatchRequestHandler: Sync + Send { fn handle_calendar_proppatch_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: PropertyUpdate, ) -> impl Future> + Send; @@ -68,7 +68,7 @@ impl CalendarPropPatchRequestHandler for Server { async fn handle_calendar_proppatch_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, mut request: PropertyUpdate, ) -> crate::Result { // Validate URI @@ -120,7 +120,7 @@ impl CalendarPropPatchRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection, diff --git a/crates/dav/src/calendar/query.rs b/crates/dav/src/calendar/query.rs index 07afa87c..addf2464 100644 --- a/crates/dav/src/calendar/query.rs +++ b/crates/dav/src/calendar/query.rs @@ -43,7 +43,7 @@ pub(crate) trait CalendarQueryRequestHandler: Sync + Send { fn handle_calendar_query_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: CalendarQuery, ) -> impl Future> + Send; } @@ -52,7 +52,7 @@ impl CalendarQueryRequestHandler for Server { async fn handle_calendar_query_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: CalendarQuery, ) -> crate::Result { // Validate URI @@ -232,7 +232,11 @@ impl CalendarQueryHandler { .data .expand(default_tz, max_time_range) .unwrap_or_else(|| { - let todo = "log error"; + trc::event!( + Calendar(trc::CalendarEvent::RuleExpansionError), + Reason = "chrono error", + Details = event.data.event.to_string(), + ); vec![] }) }) diff --git a/crates/dav/src/calendar/update.rs b/crates/dav/src/calendar/update.rs index 56def266..d494fef6 100644 --- a/crates/dav/src/calendar/update.rs +++ b/crates/dav/src/calendar/update.rs @@ -45,7 +45,7 @@ pub(crate) trait CalendarUpdateRequestHandler: Sync + Send { fn handle_calendar_update_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, bytes: Vec, is_patch: bool, ) -> impl Future> + Send; @@ -55,7 +55,7 @@ impl CalendarUpdateRequestHandler for Server { async fn handle_calendar_update_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, bytes: Vec, _is_patch: bool, ) -> crate::Result { @@ -124,7 +124,7 @@ impl CalendarUpdateRequestHandler for Server { match self .validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: Collection::CalendarEvent, @@ -209,7 +209,7 @@ impl CalendarUpdateRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, diff --git a/crates/dav/src/card/copy_move.rs b/crates/dav/src/card/copy_move.rs index 80e484ac..471e2536 100644 --- a/crates/dav/src/card/copy_move.rs +++ b/crates/dav/src/card/copy_move.rs @@ -35,7 +35,7 @@ pub(crate) trait CardCopyMoveRequestHandler: Sync + Send { fn handle_card_copy_move_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_move: bool, ) -> impl Future> + Send; } @@ -44,7 +44,7 @@ impl CardCopyMoveRequestHandler for Server { async fn handle_card_copy_move_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_move: bool, ) -> crate::Result { // Validate source @@ -110,7 +110,7 @@ impl CardCopyMoveRequestHandler for Server { let to_resource = to_resources.by_path(destination_resource_name); self.validate_headers( access_token, - &headers, + headers, vec![ ResourceState { account_id: from_account_id, diff --git a/crates/dav/src/card/delete.rs b/crates/dav/src/card/delete.rs index 124bef74..ba2117fc 100644 --- a/crates/dav/src/card/delete.rs +++ b/crates/dav/src/card/delete.rs @@ -33,7 +33,7 @@ pub(crate) trait CardDeleteRequestHandler: Sync + Send { fn handle_card_delete_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, ) -> impl Future> + Send; } @@ -41,7 +41,7 @@ impl CardDeleteRequestHandler for Server { async fn handle_card_delete_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, ) -> crate::Result { // Validate URI let resource = self @@ -91,7 +91,7 @@ impl CardDeleteRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: Collection::AddressBook, @@ -143,7 +143,7 @@ impl CardDeleteRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: Collection::ContactCard, diff --git a/crates/dav/src/card/get.rs b/crates/dav/src/card/get.rs index 29796596..c755a6b3 100644 --- a/crates/dav/src/card/get.rs +++ b/crates/dav/src/card/get.rs @@ -28,7 +28,7 @@ pub(crate) trait CardGetRequestHandler: Sync + Send { fn handle_card_get_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_head: bool, ) -> impl Future> + Send; } @@ -37,7 +37,7 @@ impl CardGetRequestHandler for Server { async fn handle_card_get_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_head: bool, ) -> crate::Result { // Validate URI @@ -86,7 +86,7 @@ impl CardGetRequestHandler for Server { let etag = card_.etag(); self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: Collection::ContactCard, diff --git a/crates/dav/src/card/mkcol.rs b/crates/dav/src/card/mkcol.rs index e3e2b607..53bbe78f 100644 --- a/crates/dav/src/card/mkcol.rs +++ b/crates/dav/src/card/mkcol.rs @@ -29,7 +29,7 @@ pub(crate) trait CardMkColRequestHandler: Sync + Send { fn handle_card_mkcol_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: Option, ) -> impl Future> + Send; } @@ -38,7 +38,7 @@ impl CardMkColRequestHandler for Server { async fn handle_card_mkcol_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: Option, ) -> crate::Result { // Validate URI @@ -57,7 +57,6 @@ impl CardMkColRequestHandler for Server { .fetch_dav_resources(access_token, account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())? - .by_path(name) .is_some() { @@ -67,7 +66,7 @@ impl CardMkColRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, diff --git a/crates/dav/src/card/proppatch.rs b/crates/dav/src/card/proppatch.rs index c2fac0f4..706792f0 100644 --- a/crates/dav/src/card/proppatch.rs +++ b/crates/dav/src/card/proppatch.rs @@ -40,7 +40,7 @@ pub(crate) trait CardPropPatchRequestHandler: Sync + Send { fn handle_card_proppatch_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: PropertyUpdate, ) -> impl Future> + Send; @@ -65,7 +65,7 @@ impl CardPropPatchRequestHandler for Server { async fn handle_card_proppatch_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, mut request: PropertyUpdate, ) -> crate::Result { // Validate URI @@ -117,7 +117,7 @@ impl CardPropPatchRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection, diff --git a/crates/dav/src/card/query.rs b/crates/dav/src/card/query.rs index 596df88e..ac3c63fa 100644 --- a/crates/dav/src/card/query.rs +++ b/crates/dav/src/card/query.rs @@ -34,7 +34,7 @@ pub(crate) trait CardQueryRequestHandler: Sync + Send { fn handle_card_query_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: AddressbookQuery, ) -> impl Future> + Send; } @@ -43,7 +43,7 @@ impl CardQueryRequestHandler for Server { async fn handle_card_query_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: AddressbookQuery, ) -> crate::Result { // Validate URI diff --git a/crates/dav/src/card/update.rs b/crates/dav/src/card/update.rs index e38f09ad..1081193a 100644 --- a/crates/dav/src/card/update.rs +++ b/crates/dav/src/card/update.rs @@ -36,7 +36,7 @@ pub(crate) trait CardUpdateRequestHandler: Sync + Send { fn handle_card_update_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, bytes: Vec, is_patch: bool, ) -> impl Future> + Send; @@ -46,7 +46,7 @@ impl CardUpdateRequestHandler for Server { async fn handle_card_update_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, bytes: Vec, _is_patch: bool, ) -> crate::Result { @@ -115,7 +115,7 @@ impl CardUpdateRequestHandler for Server { match self .validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: Collection::ContactCard, @@ -203,7 +203,7 @@ impl CardUpdateRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index 775384e8..7ecda8c1 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -38,14 +38,14 @@ pub(crate) trait DavAclHandler: Sync + Send { fn handle_acl_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: dav_proto::schema::request::Acl, ) -> impl Future> + Send; fn handle_acl_prop_set( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: AclPrincipalPropSet, ) -> impl Future> + Send; @@ -79,7 +79,7 @@ impl DavAclHandler for Server { async fn handle_acl_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: dav_proto::schema::request::Acl, ) -> crate::Result { // Validate URI @@ -193,7 +193,7 @@ impl DavAclHandler for Server { async fn handle_acl_prop_set( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, mut request: AclPrincipalPropSet, ) -> crate::Result { let uri = self diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index 9e3fd71c..b348fe00 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -77,7 +77,7 @@ pub(crate) trait LockRequestHandler: Sync + Send { fn handle_lock_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, lock_info: LockRequest, ) -> impl Future> + Send; @@ -101,7 +101,7 @@ impl LockRequestHandler for Server { async fn handle_lock_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, lock_info: LockRequest, ) -> crate::Result { let resource = self @@ -150,7 +150,7 @@ impl LockRequestHandler for Server { self.validate_headers( access_token, - &headers, + headers, resources, LockCaches::new_shared(account_id, resource.collection, lock_data), if is_lock_request { @@ -215,7 +215,7 @@ impl LockRequestHandler for Server { } else if is_lock_request { self.validate_headers( access_token, - &headers, + headers, resources, Default::default(), DavMethod::LOCK, diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index 0fa5a335..9759d60d 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -145,7 +145,7 @@ impl<'x> DavQuery<'x> { pub fn propfind( resource: OwnedUri<'x>, propfind: PropFind, - headers: RequestHeaders<'x>, + headers: &RequestHeaders<'x>, ) -> Self { Self { resource: DavQueryResource::Uri(resource), @@ -164,7 +164,7 @@ impl<'x> DavQuery<'x> { pub fn multiget( multiget: MultiGet, collection: Collection, - headers: RequestHeaders<'x>, + headers: &RequestHeaders<'x>, ) -> Self { Self { resource: DavQueryResource::Multiget { @@ -182,7 +182,7 @@ impl<'x> DavQuery<'x> { pub fn addressbook_query( query: AddressbookQuery, items: Vec, - headers: RequestHeaders<'x>, + headers: &RequestHeaders<'x>, ) -> Self { Self { resource: DavQueryResource::Query { @@ -203,7 +203,7 @@ impl<'x> DavQuery<'x> { query: CalendarQuery, max_time_range: Option, items: Vec, - headers: RequestHeaders<'x>, + headers: &RequestHeaders<'x>, ) -> Self { Self { resource: DavQueryResource::Query { @@ -226,7 +226,7 @@ impl<'x> DavQuery<'x> { pub fn changes( resource: OwnedUri<'x>, changes: SyncCollection, - headers: RequestHeaders<'x>, + headers: &RequestHeaders<'x>, ) -> Self { Self { resource: DavQueryResource::Uri(resource), @@ -254,7 +254,7 @@ impl<'x> DavQuery<'x> { pub fn expand( resource: OwnedUri<'x>, expand: ExpandProperty, - headers: RequestHeaders<'x>, + headers: &RequestHeaders<'x>, ) -> Self { let mut props = Vec::with_capacity(expand.properties.len()); for item in expand.properties { diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index e17038e7..a07e0600 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -46,7 +46,7 @@ use dav_proto::{ }, }, }; -use directory::{Type, backend::internal::manage::ManageDirectory}; +use directory::{Permission, Type, backend::internal::manage::ManageDirectory}; use groupware::{ DavCalendarResource, DavResourceName, cache::GroupwareCache, calendar::ArchivedTimezone, }; @@ -70,7 +70,7 @@ pub(crate) trait PropFindRequestHandler: Sync + Send { fn handle_propfind_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: PropFind, ) -> impl Future> + Send; @@ -119,7 +119,7 @@ impl PropFindRequestHandler for Server { async fn handle_propfind_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: PropFind, ) -> crate::Result { // Validate URI @@ -148,6 +148,18 @@ impl PropFindRequestHandler for Server { if let Some(account_id) = resource.account_id { match resource.collection { Collection::FileNode | Collection::Calendar | Collection::AddressBook => { + // Validate permissions + access_token.assert_has_permission(match resource.collection { + Collection::FileNode => Permission::DavFilePropFind, + Collection::Calendar | Collection::CalendarEvent => { + Permission::DavCalPropFind + } + Collection::AddressBook | Collection::ContactCard => { + Permission::DavCardPropFind + } + _ => unreachable!(), + })?; + self.handle_dav_query( access_token, DavQuery::propfind( @@ -165,16 +177,14 @@ impl PropFindRequestHandler for Server { Collection::Principal => { let mut response = MultiStatus::new(Vec::with_capacity(16)); - if let Some(resource) = resource.resource { + if resource.resource.is_some() { response.add_response(Response::new_status( - [format!( - "{}/{}", - headers.base_uri().unwrap_or_default(), - resource - )], + [headers.uri.to_string()], StatusCode::NOT_FOUND, )); - } else { + } else if access_token.has_account_access(account_id) + || access_token.has_permission(Permission::DavPrincipalList) + { self.prepare_principal_propfind_response( access_token, Collection::Principal, @@ -183,6 +193,11 @@ impl PropFindRequestHandler for Server { &mut response, ) .await?; + } else { + response.add_response(Response::new_status( + [headers.uri.to_string()], + StatusCode::FORBIDDEN, + )); } Ok(HttpResponse::new(StatusCode::MULTI_STATUS) @@ -309,10 +324,21 @@ impl PropFindRequestHandler for Server { if return_children { let ids = if !matches!(resource.collection, Collection::Principal) { + // Validate permissions + access_token.assert_has_permission(match resource.collection { + Collection::FileNode => Permission::DavFilePropFind, + Collection::Calendar | Collection::CalendarEvent => { + Permission::DavCalPropFind + } + Collection::AddressBook | Collection::ContactCard => { + Permission::DavCardPropFind + } + _ => unreachable!(), + })?; RoaringBitmap::from_iter( access_token.all_ids_by_collection(resource.collection), ) - } else { + } else if access_token.has_permission(Permission::DavPrincipalList) { // Return all principals let principals = self .store() @@ -328,6 +354,8 @@ impl PropFindRequestHandler for Server { .caused_by(trc::location!())?; RoaringBitmap::from_iter(principals.items.into_iter().map(|p| p.id())) + } else { + RoaringBitmap::from_iter(access_token.all_ids()) }; self.prepare_principal_propfind_response( diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index dad83609..85eea899 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -36,7 +36,7 @@ pub(crate) trait FileCopyMoveRequestHandler: Sync + Send { fn handle_file_copy_move_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_move: bool, ) -> impl Future> + Send; } @@ -45,7 +45,7 @@ impl FileCopyMoveRequestHandler for Server { async fn handle_file_copy_move_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_move: bool, ) -> crate::Result { // Validate source @@ -117,8 +117,8 @@ impl FileCopyMoveRequestHandler for Server { return Ok(HttpResponse::new(StatusCode::BAD_GATEWAY)); } - let mut delete_destination = None; // Check if the resource exists + let mut delete_destination = None; let mut destination = if let Some((destination, new_name)) = to_resources.map_parent(destination_resource_name) { @@ -144,16 +144,6 @@ impl FileCopyMoveRequestHandler for Server { }; destination.account_id = to_account_id; - if delete_destination.is_none() - && from_account_id == destination.account_id - && from_resource.resource.parent_id == destination.document_id - && destination.new_name.is_some() - && is_move - { - // Rename - return rename_item(self, access_token, from_resource, destination).await; - } - // Validate destination ACLs if let Some(document_id) = destination.document_id { if let Some(delete_destination) = &delete_destination { @@ -180,13 +170,13 @@ impl FileCopyMoveRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ ResourceState { account_id: from_account_id, collection: Collection::FileNode, document_id: Some(from_resource.resource.document_id), - path: from_resource_.resource.unwrap(), + path: from_resource_name, ..Default::default() }, ResourceState { @@ -195,8 +185,7 @@ impl FileCopyMoveRequestHandler for Server { document_id: Some( delete_destination .as_ref() - .unwrap_or(&destination) - .document_id + .and_then(|d| d.document_id) .unwrap_or(u32::MAX), ), path: destination_resource_name, @@ -212,6 +201,16 @@ impl FileCopyMoveRequestHandler for Server { ) .await?; + if delete_destination.is_none() + && from_account_id == destination.account_id + && from_resource.resource.parent_id == destination.document_id + && destination.new_name.is_some() + && is_move + { + // Rename + return rename_item(self, access_token, from_resource, destination).await; + } + // Validate quota if !is_move || from_account_id != to_account_id { let space_needed = from_resources diff --git a/crates/dav/src/file/delete.rs b/crates/dav/src/file/delete.rs index 7117306f..5f74212e 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -23,7 +23,7 @@ pub(crate) trait FileDeleteRequestHandler: Sync + Send { fn handle_file_delete_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, ) -> impl Future> + Send; } @@ -31,7 +31,7 @@ impl FileDeleteRequestHandler for Server { async fn handle_file_delete_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, ) -> crate::Result { // Validate URI let resource = self @@ -73,7 +73,7 @@ impl FileDeleteRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, diff --git a/crates/dav/src/file/get.rs b/crates/dav/src/file/get.rs index c5696eb0..7aab00b4 100644 --- a/crates/dav/src/file/get.rs +++ b/crates/dav/src/file/get.rs @@ -9,7 +9,10 @@ use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime}; use groupware::{cache::GroupwareCache, file::FileNode}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::{Collection, SyncCollection}}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use trc::AddContext; use crate::{ @@ -26,7 +29,7 @@ pub(crate) trait FileGetRequestHandler: Sync + Send { fn handle_file_get_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_head: bool, ) -> impl Future> + Send; } @@ -35,7 +38,7 @@ impl FileGetRequestHandler for Server { async fn handle_file_get_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, is_head: bool, ) -> crate::Result { // Validate URI @@ -79,7 +82,7 @@ impl FileGetRequestHandler for Server { let etag = node_.etag(); self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, diff --git a/crates/dav/src/file/mkcol.rs b/crates/dav/src/file/mkcol.rs index 970a8352..1ca052d3 100644 --- a/crates/dav/src/file/mkcol.rs +++ b/crates/dav/src/file/mkcol.rs @@ -34,7 +34,7 @@ pub(crate) trait FileMkColRequestHandler: Sync + Send { fn handle_file_mkcol_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: Option, ) -> impl Future> + Send; } @@ -43,7 +43,7 @@ impl FileMkColRequestHandler for Server { async fn handle_file_mkcol_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: Option, ) -> crate::Result { // Validate URI @@ -69,7 +69,7 @@ impl FileMkColRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index e3b55413..8091131e 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -37,7 +37,7 @@ pub(crate) trait FilePropPatchRequestHandler: Sync + Send { fn handle_file_proppatch_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: PropertyUpdate, ) -> impl Future> + Send; @@ -54,7 +54,7 @@ impl FilePropPatchRequestHandler for Server { async fn handle_file_proppatch_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, mut request: PropertyUpdate, ) -> crate::Result { // Validate URI @@ -98,7 +98,7 @@ impl FilePropPatchRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index f4aaae15..f1521da9 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -36,7 +36,7 @@ pub(crate) trait FileUpdateRequestHandler: Sync + Send { fn handle_file_update_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, bytes: Vec, is_patch: bool, ) -> impl Future> + Send; @@ -46,7 +46,7 @@ impl FileUpdateRequestHandler for Server { async fn handle_file_update_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, bytes: Vec, _is_patch: bool, ) -> crate::Result { @@ -94,7 +94,7 @@ impl FileUpdateRequestHandler for Server { match self .validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, @@ -215,7 +215,7 @@ impl FileUpdateRequestHandler for Server { // Validate headers self.validate_headers( access_token, - &headers, + headers, vec![ResourceState { account_id, collection: resource.collection, diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index 77536c1b..d4570f56 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -42,6 +42,30 @@ pub enum DavMethod { ACL, } +impl From for trc::WebDavEvent { + fn from(value: DavMethod) -> Self { + match value { + DavMethod::GET => trc::WebDavEvent::Get, + DavMethod::PUT => trc::WebDavEvent::Put, + DavMethod::POST => trc::WebDavEvent::Post, + DavMethod::DELETE => trc::WebDavEvent::Delete, + DavMethod::HEAD => trc::WebDavEvent::Head, + DavMethod::PATCH => trc::WebDavEvent::Patch, + DavMethod::PROPFIND => trc::WebDavEvent::Propfind, + DavMethod::PROPPATCH => trc::WebDavEvent::Proppatch, + DavMethod::REPORT => trc::WebDavEvent::Report, + DavMethod::MKCOL => trc::WebDavEvent::Mkcol, + DavMethod::MKCALENDAR => trc::WebDavEvent::Mkcalendar, + DavMethod::COPY => trc::WebDavEvent::Copy, + DavMethod::MOVE => trc::WebDavEvent::Move, + DavMethod::LOCK => trc::WebDavEvent::Lock, + DavMethod::UNLOCK => trc::WebDavEvent::Unlock, + DavMethod::OPTIONS => trc::WebDavEvent::Options, + DavMethod::ACL => trc::WebDavEvent::Acl, + } + } +} + pub(crate) enum DavError { Parse(dav_proto::parser::Error), Internal(trc::Error), diff --git a/crates/dav/src/principal/matching.rs b/crates/dav/src/principal/matching.rs index db97880e..66c34dab 100644 --- a/crates/dav/src/principal/matching.rs +++ b/crates/dav/src/principal/matching.rs @@ -33,7 +33,7 @@ pub(crate) trait PrincipalMatching: Sync + Send { fn handle_principal_match( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, request: PrincipalMatch, ) -> impl Future> + Send; } @@ -42,7 +42,7 @@ impl PrincipalMatching for Server { async fn handle_principal_match( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, + headers: &RequestHeaders<'_>, mut request: PrincipalMatch, ) -> crate::Result { let resource = self.validate_uri(access_token, headers.uri).await?; diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index ac6b6097..013a3146 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -33,6 +33,7 @@ use crate::{ principal::{matching::PrincipalMatching, propsearch::PrincipalPropSearch}, }; use common::{Server, auth::AccessToken}; +use compact_str::{CompactString, ToCompactString}; use dav_proto::{ RequestHeaders, parser::{DavParser, tokenizer::Tokenizer}, @@ -44,13 +45,13 @@ use dav_proto::{ BaseCondition, ErrorResponse, PrincipalSearchProperty, PrincipalSearchPropertySet, }, }, - xml_pretty_print, }; use directory::Permission; use http_proto::{HttpRequest, HttpResponse, HttpSessionData, request::fetch_body}; use hyper::{StatusCode, header}; use jmap_proto::types::collection::Collection; -use std::sync::Arc; +use std::{sync::Arc, time::Instant}; +use trc::{EventType, LimitEvent, StoreEvent, WebDavEvent}; pub trait DavRequestHandler: Sync + Send { fn handle_dav_request( @@ -67,6 +68,7 @@ pub(crate) trait DavRequestDispatcher: Sync + Send { fn dispatch_dav_request( &self, request: &HttpRequest, + headers: &RequestHeaders<'_>, access_token: Arc, resource: DavResourceName, method: DavMethod, @@ -78,29 +80,25 @@ impl DavRequestDispatcher for Server { async fn dispatch_dav_request( &self, request: &HttpRequest, + headers: &RequestHeaders<'_>, access_token: Arc, resource: DavResourceName, method: DavMethod, body: Vec, ) -> crate::Result { - // Parse headers - let mut headers = RequestHeaders::new(request.uri().path()); - for (key, value) in request.headers() { - headers.parse(key.as_str(), value.to_str().unwrap_or_default()); - } - // Dispatch match method { DavMethod::PROPFIND => { - self.handle_propfind_request( - &access_token, - headers, - PropFind::parse(&mut Tokenizer::new(&body))?, - ) - .await + let request = PropFind::parse(&mut Tokenizer::new(&body))?; + + self.handle_propfind_request(&access_token, headers, request) + .await } DavMethod::GET | DavMethod::HEAD => match resource { DavResourceName::Card => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCardGet)?; + self.handle_card_get_request( &access_token, headers, @@ -109,6 +107,9 @@ impl DavRequestDispatcher for Server { .await } DavResourceName::Cal => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCalGet)?; + self.handle_calendar_get_request( &access_token, headers, @@ -117,6 +118,9 @@ impl DavRequestDispatcher for Server { .await } DavResourceName::File => { + // Validate permissions + access_token.assert_has_permission(Permission::DavFileGet)?; + #[cfg(debug_assertions)] { // Deal with Litmus bug @@ -143,6 +147,9 @@ impl DavRequestDispatcher for Server { }, DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? { Report::SyncCollection(sync_collection) => { + // Validate permissions + access_token.assert_has_permission(Permission::DavSyncCollection)?; + let uri = self .validate_uri(&access_token, headers.uri) .await @@ -161,15 +168,24 @@ impl DavRequestDispatcher for Server { } } Report::AclPrincipalPropSet(report) => { + // Validate permissions + access_token.assert_has_permission(Permission::DavPrincipalAcl)?; + self.handle_acl_prop_set(&access_token, headers, report) .await } Report::PrincipalMatch(report) => { + // Validate permissions + access_token.assert_has_permission(Permission::DavPrincipalMatch)?; + self.handle_principal_match(&access_token, headers, report) .await } Report::PrincipalPropertySearch(report) => { if resource == DavResourceName::Principal { + // Validate permissions + access_token.assert_has_permission(Permission::DavPrincipalSearch)?; + self.handle_principal_property_search(&access_token, report) .await } else { @@ -178,6 +194,10 @@ impl DavRequestDispatcher for Server { } Report::PrincipalSearchPropertySet => { if resource == DavResourceName::Principal { + // Validate permissions + access_token + .assert_has_permission(Permission::DavPrincipalSearchPropSet)?; + Ok(HttpResponse::new(StatusCode::OK).with_xml_body( PrincipalSearchPropertySet::new(vec![PrincipalSearchProperty::new( WebDavProperty::DisplayName, @@ -190,10 +210,16 @@ impl DavRequestDispatcher for Server { } } Report::AddressbookQuery(report) => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCardQuery)?; + self.handle_card_query_request(&access_token, headers, report) .await } Report::AddressbookMultiGet(report) => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCardMultiGet)?; + self.handle_dav_query( &access_token, DavQuery::multiget(report, Collection::AddressBook, headers), @@ -201,10 +227,16 @@ impl DavRequestDispatcher for Server { .await } Report::CalendarQuery(report) => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCalQuery)?; + self.handle_calendar_query_request(&access_token, headers, report) .await } Report::CalendarMultiGet(report) => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCalMultiGet)?; + self.handle_dav_query( &access_token, DavQuery::multiget(report, Collection::Calendar, headers), @@ -212,6 +244,9 @@ impl DavRequestDispatcher for Server { .await } Report::FreeBusyQuery(report) => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCalFreeBusyQuery)?; + self.handle_calendar_freebusy_request(&access_token, headers, report) .await } @@ -220,6 +255,10 @@ impl DavRequestDispatcher for Server { .validate_uri(&access_token, headers.uri) .await .and_then(|d| d.into_owned_uri())?; + + // Validate permissions + access_token.assert_has_permission(Permission::DavExpandProperty)?; + match resource { DavResourceName::Card | DavResourceName::Cal | DavResourceName::File => { self.handle_dav_query( @@ -238,14 +277,23 @@ impl DavRequestDispatcher for Server { let request = PropertyUpdate::parse(&mut Tokenizer::new(&body))?; match resource { DavResourceName::Card => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCardPropPatch)?; + self.handle_card_proppatch_request(&access_token, headers, request) .await } DavResourceName::Cal => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCalPropPatch)?; + self.handle_calendar_proppatch_request(&access_token, headers, request) .await } DavResourceName::File => { + // Validate permissions + access_token.assert_has_permission(Permission::DavFilePropPatch)?; + self.handle_file_proppatch_request(&access_token, headers, request) .await } @@ -263,14 +311,23 @@ impl DavRequestDispatcher for Server { match resource { DavResourceName::Card => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCardMkCol)?; + self.handle_card_mkcol_request(&access_token, headers, request) .await } DavResourceName::Cal => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCalMkCol)?; + self.handle_calendar_mkcol_request(&access_token, headers, request) .await } DavResourceName::File => { + // Validate permissions + access_token.assert_has_permission(Permission::DavFileMkCol)?; + self.handle_file_mkcol_request(&access_token, headers, request) .await } @@ -279,33 +336,35 @@ impl DavRequestDispatcher for Server { } } } - DavMethod::DELETE => { - // Include any fragments in the URI - if let Some(p) = request.uri().path_and_query() { - // TODO: Access to the fragment part is pending, see https://github.com/hyperium/http/issues/127 - headers.uri = p.as_str(); - } + DavMethod::DELETE => match resource { + DavResourceName::Card => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCardDelete)?; - match resource { - DavResourceName::Card => { - self.handle_card_delete_request(&access_token, headers) - .await - } - DavResourceName::Cal => { - self.handle_calendar_delete_request(&access_token, headers) - .await - } - DavResourceName::File => { - self.handle_file_delete_request(&access_token, headers) - .await - } - DavResourceName::Principal => { - Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) - } + self.handle_card_delete_request(&access_token, headers) + .await } - } + DavResourceName::Cal => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCalDelete)?; + + self.handle_calendar_delete_request(&access_token, headers) + .await + } + DavResourceName::File => { + // Validate permissions + access_token.assert_has_permission(Permission::DavFileDelete)?; + + self.handle_file_delete_request(&access_token, headers) + .await + } + DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + }, DavMethod::PUT | DavMethod::POST | DavMethod::PATCH => match resource { DavResourceName::Card => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCardPut)?; + self.handle_card_update_request( &access_token, headers, @@ -315,6 +374,9 @@ impl DavRequestDispatcher for Server { .await } DavResourceName::Cal => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCalPut)?; + self.handle_calendar_update_request( &access_token, headers, @@ -324,6 +386,9 @@ impl DavRequestDispatcher for Server { .await } DavResourceName::File => { + // Validate permissions + access_token.assert_has_permission(Permission::DavFilePut)?; + self.handle_file_update_request( &access_token, headers, @@ -334,35 +399,51 @@ impl DavRequestDispatcher for Server { } DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, - DavMethod::COPY | DavMethod::MOVE => match resource { - DavResourceName::Card => { - self.handle_card_copy_move_request( - &access_token, - headers, - matches!(method, DavMethod::MOVE), - ) - .await + DavMethod::COPY | DavMethod::MOVE => { + let is_move = matches!(method, DavMethod::MOVE); + match resource { + DavResourceName::Card => { + // Validate permissions + access_token.assert_has_permission(if is_move { + Permission::DavCardMove + } else { + Permission::DavCardCopy + })?; + + self.handle_card_copy_move_request(&access_token, headers, is_move) + .await + } + DavResourceName::Cal => { + // Validate permissions + access_token.assert_has_permission(if is_move { + Permission::DavCalMove + } else { + Permission::DavCalCopy + })?; + self.handle_calendar_copy_move_request(&access_token, headers, is_move) + .await + } + DavResourceName::File => { + // Validate permissions + access_token.assert_has_permission(if is_move { + Permission::DavFileMove + } else { + Permission::DavFileCopy + })?; + + self.handle_file_copy_move_request(&access_token, headers, is_move) + .await + } + DavResourceName::Principal => { + Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) + } } - DavResourceName::Cal => { - self.handle_calendar_copy_move_request( - &access_token, - headers, - matches!(method, DavMethod::MOVE), - ) - .await - } - DavResourceName::File => { - self.handle_file_copy_move_request( - &access_token, - headers, - matches!(method, DavMethod::MOVE), - ) - .await - } - DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), - }, + } DavMethod::MKCALENDAR => match resource { DavResourceName::Cal => { + // Validate permissions + access_token.assert_has_permission(Permission::DavCalMkCol)?; + self.handle_calendar_mkcol_request( &access_token, headers, @@ -372,36 +453,53 @@ impl DavRequestDispatcher for Server { } _ => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, - DavMethod::LOCK => match resource { - DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), - _ => { - self.handle_lock_request( - &access_token, - headers, - if !body.is_empty() { - LockRequest::Lock(LockInfo::parse(&mut Tokenizer::new(&body))?) - } else { - LockRequest::Refresh - }, - ) - .await - } - }, + DavMethod::LOCK => { + // Validate permissions + access_token.assert_has_permission(match resource { + DavResourceName::File => Permission::DavFileLock, + DavResourceName::Cal => Permission::DavCalLock, + DavResourceName::Card => Permission::DavCardLock, + _ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + })?; + + self.handle_lock_request( + &access_token, + headers, + if !body.is_empty() { + LockRequest::Lock(LockInfo::parse(&mut Tokenizer::new(&body))?) + } else { + LockRequest::Refresh + }, + ) + .await + } DavMethod::UNLOCK => { + // Validate permissions + access_token.assert_has_permission(match resource { + DavResourceName::File => Permission::DavFileLock, + DavResourceName::Cal => Permission::DavCalLock, + DavResourceName::Card => Permission::DavCardLock, + _ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + })?; + self.handle_lock_request(&access_token, headers, LockRequest::Unlock) .await } DavMethod::ACL => { - let request = Acl::parse(&mut Tokenizer::new(&body))?; - match resource { - DavResourceName::Card | DavResourceName::Cal | DavResourceName::File => { - self.handle_acl_request(&access_token, headers, request) - .await - } - DavResourceName::Principal => { - Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) - } - } + // Validate permissions + access_token.assert_has_permission(match resource { + DavResourceName::File => Permission::DavFileAcl, + DavResourceName::Cal => Permission::DavCalAcl, + DavResourceName::Card => Permission::DavCardAcl, + _ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + })?; + + self.handle_acl_request( + &access_token, + headers, + Acl::parse(&mut Tokenizer::new(&body))?, + ) + .await } DavMethod::OPTIONS => unreachable!(), } @@ -454,61 +552,128 @@ impl DavRequestHandler for Server { let std_body = std::str::from_utf8(&body).unwrap_or("[binary]").to_string(); + // Parse headers + let mut headers = RequestHeaders::new(request.uri().path()); + for (key, value) in request.headers() { + headers.parse(key.as_str(), value.to_str().unwrap_or_default()); + } + + let start_time = Instant::now(); let result = match self - .dispatch_dav_request(&request, access_token, resource, method, body) + .dispatch_dav_request(&request, &headers, access_token, resource, method, body) .await { - Ok(response) => response, + Ok(response) => { + let event = WebDavEvent::from(method); + + trc::event!( + WebDav(event), + SpanId = session.session_id, + Url = headers.uri.to_compact_string(), + Type = resource.name(), + Details = &headers, + Result = response.status().as_u16(), + Elapsed = start_time.elapsed(), + ); + + response + } Err(DavError::Internal(err)) => { let err_type = err.event_type(); - trc::error!(err.span_id(session.session_id)); + trc::error!( + err.span_id(session.session_id) + .ctx(trc::Key::Url, headers.uri.to_compact_string()) + .ctx(trc::Key::Type, resource.name()) + .ctx(trc::Key::Elapsed, start_time.elapsed()) + ); match err_type { - trc::EventType::Limit( - trc::LimitEvent::Quota | trc::LimitEvent::TenantQuota, - ) => HttpResponse::new(StatusCode::PRECONDITION_FAILED) - .with_xml_body( - ErrorResponse::new(BaseCondition::QuotaNotExceeded) - .with_namespace(match resource { - DavResourceName::Card => Namespace::CardDav, - DavResourceName::Cal => Namespace::CalDav, - DavResourceName::File | DavResourceName::Principal => { - Namespace::Dav - } - }) - .to_string(), - ) - .with_no_cache(), - trc::EventType::Store(trc::StoreEvent::AssertValueFailed) => { + EventType::Limit(LimitEvent::Quota | LimitEvent::TenantQuota) => { + HttpResponse::new(StatusCode::PRECONDITION_FAILED) + .with_xml_body( + ErrorResponse::new(BaseCondition::QuotaNotExceeded) + .with_namespace(match resource { + DavResourceName::Card => Namespace::CardDav, + DavResourceName::Cal => Namespace::CalDav, + DavResourceName::File | DavResourceName::Principal => { + Namespace::Dav + } + }) + .to_string(), + ) + .with_no_cache() + } + EventType::Store(StoreEvent::AssertValueFailed) => { HttpResponse::new(StatusCode::CONFLICT) } + EventType::Security(_) => HttpResponse::new(StatusCode::FORBIDDEN), _ => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), } } Err(DavError::Parse(err)) => { - if request - .headers() - .get(header::CONTENT_TYPE) - .is_some_and(|h| h.to_str().unwrap_or_default().contains("/xml")) - { - HttpResponse::new(StatusCode::BAD_REQUEST) + let result = if headers.content_type.is_some_and(|h| h.contains("/xml")) { + StatusCode::BAD_REQUEST } else { - HttpResponse::new(StatusCode::UNSUPPORTED_MEDIA_TYPE) - } + StatusCode::UNSUPPORTED_MEDIA_TYPE + }; + + trc::event!( + WebDav(WebDavEvent::Error), + SpanId = session.session_id, + Url = headers.uri.to_compact_string(), + Type = resource.name(), + Details = &headers, + Result = result.as_u16(), + Reason = err.to_compact_string(), + Elapsed = start_time.elapsed(), + ); + + HttpResponse::new(result) + } + Err(DavError::Condition(condition)) => { + let event = WebDavEvent::from(method); + + trc::event!( + WebDav(event), + SpanId = session.session_id, + Url = headers.uri.to_compact_string(), + Type = resource.name(), + Details = &headers, + Result = condition.code.as_u16(), + Reason = CompactString::const_new(condition.condition.display_name()), + Elapsed = start_time.elapsed(), + ); + + HttpResponse::new(condition.code) + .with_xml_body( + ErrorResponse::new(condition.condition) + .with_namespace(match resource { + DavResourceName::Card => Namespace::CardDav, + DavResourceName::Cal => Namespace::CalDav, + DavResourceName::File | DavResourceName::Principal => { + Namespace::Dav + } + }) + .to_string(), + ) + .with_no_cache() + } + Err(DavError::Code(code)) => { + let event = WebDavEvent::from(method); + + trc::event!( + WebDav(event), + SpanId = session.session_id, + Url = headers.uri.to_compact_string(), + Type = resource.name(), + Details = &headers, + Result = code.as_u16(), + Elapsed = start_time.elapsed(), + ); + + HttpResponse::new(code) } - Err(DavError::Condition(condition)) => HttpResponse::new(condition.code) - .with_xml_body( - ErrorResponse::new(condition.condition) - .with_namespace(match resource { - DavResourceName::Card => Namespace::CardDav, - DavResourceName::Cal => Namespace::CalDav, - DavResourceName::File | DavResourceName::Principal => Namespace::Dav, - }) - .to_string(), - ) - .with_no_cache(), - Err(DavError::Code(code)) => HttpResponse::new(code), }; /*let c = println!( @@ -520,7 +685,7 @@ impl DavRequestHandler for Server { std_body, result.headers().unwrap(), match &result.body() { - http_proto::HttpResponseBody::Text(t) => xml_pretty_print(t), + http_proto::HttpResponseBody::Text(t) => dav_proto::xml_pretty_print(t), http_proto::HttpResponseBody::Empty => "[empty]".to_string(), _ => "[binary]".to_string(), } diff --git a/crates/directory/src/core/mod.rs b/crates/directory/src/core/mod.rs index e698d85a..be947439 100644 --- a/crates/directory/src/core/mod.rs +++ b/crates/directory/src/core/mod.rs @@ -200,6 +200,52 @@ impl Permission { Permission::OauthClientDelete => "Remove OAuth clients", Permission::AiModelInteract => "Interact with AI models", Permission::Troubleshoot => "Perform troubleshooting", + Permission::DavSyncCollection => "Synchronize collection changes with client", + Permission::DavPrincipalAcl => "Set principal properties for access control", + Permission::DavPrincipalMatch => "Match principals based on specified criteria", + Permission::DavPrincipalSearch => "Search for principals by property values", + Permission::DavPrincipalSearchPropSet => "Define property sets for principal searches", + Permission::DavExpandProperty => "Expand properties that reference other resources", + Permission::DavPrincipalList => "List available principals in the system", + Permission::DavFilePropFind => "Retrieve properties of file resources", + Permission::DavFilePropPatch => "Modify properties of file resources", + Permission::DavFileGet => "Download file resources", + Permission::DavFileMkCol => "Create new file collections or directories", + Permission::DavFileDelete => "Remove file resources", + Permission::DavFilePut => "Upload or modify file resources", + Permission::DavFileCopy => "Copy file resources to new locations", + Permission::DavFileMove => "Move file resources to new locations", + Permission::DavFileLock => "Lock file resources to prevent concurrent modifications", + Permission::DavFileAcl => "Manage access control lists for file resources", + Permission::DavCardPropFind => "Retrieve properties of address book entries", + Permission::DavCardPropPatch => "Modify properties of address book entries", + Permission::DavCardGet => "Download address book entries", + Permission::DavCardMkCol => "Create new address book collections", + Permission::DavCardDelete => "Remove address book entries or collections", + Permission::DavCardPut => "Upload or modify address book entries", + Permission::DavCardCopy => "Copy address book entries to new locations", + Permission::DavCardMove => "Move address book entries to new locations", + Permission::DavCardLock => { + "Lock address book entries to prevent concurrent modifications" + } + Permission::DavCardAcl => "Manage access control lists for address book entries", + Permission::DavCardQuery => "Search for address book entries matching criteria", + Permission::DavCardMultiGet => { + "Retrieve multiple address book entries in a single request" + } + Permission::DavCalPropFind => "Retrieve properties of calendar entries", + Permission::DavCalPropPatch => "Modify properties of calendar entries", + Permission::DavCalGet => "Download calendar entries", + Permission::DavCalMkCol => "Create new calendar collections", + Permission::DavCalDelete => "Remove calendar entries or collections", + Permission::DavCalPut => "Upload or modify calendar entries", + Permission::DavCalCopy => "Copy calendar entries to new locations", + Permission::DavCalMove => "Move calendar entries to new locations", + Permission::DavCalLock => "Lock calendar entries to prevent concurrent modifications", + Permission::DavCalAcl => "Manage access control lists for calendar entries", + Permission::DavCalQuery => "Search for calendar entries matching criteria", + Permission::DavCalMultiGet => "Retrieve multiple calendar entries in a single request", + Permission::DavCalFreeBusyQuery => "Query free/busy time information for scheduling", } } } diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index cc83e55e..bff4f417 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -1367,6 +1367,46 @@ impl Permission { | Permission::SieveHaveSpace | Permission::SpamFilterClassify | Permission::SpamFilterTrain + | Permission::DavSyncCollection + | Permission::DavExpandProperty + | Permission::DavPrincipalAcl + | Permission::DavPrincipalMatch + | Permission::DavPrincipalSearchPropSet + | Permission::DavFilePropFind + | Permission::DavFilePropPatch + | Permission::DavFileGet + | Permission::DavFileMkCol + | Permission::DavFileDelete + | Permission::DavFilePut + | Permission::DavFileCopy + | Permission::DavFileMove + | Permission::DavFileLock + | Permission::DavFileAcl + | Permission::DavCardPropFind + | Permission::DavCardPropPatch + | Permission::DavCardGet + | Permission::DavCardMkCol + | Permission::DavCardDelete + | Permission::DavCardPut + | Permission::DavCardCopy + | Permission::DavCardMove + | Permission::DavCardLock + | Permission::DavCardAcl + | Permission::DavCardQuery + | Permission::DavCardMultiGet + | Permission::DavCalPropFind + | Permission::DavCalPropPatch + | Permission::DavCalGet + | Permission::DavCalMkCol + | Permission::DavCalDelete + | Permission::DavCalPut + | Permission::DavCalCopy + | Permission::DavCalMove + | Permission::DavCalLock + | Permission::DavCalAcl + | Permission::DavCalQuery + | Permission::DavCalMultiGet + | Permission::DavCalFreeBusyQuery ) } diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index a3c74e49..c4f27d9a 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -330,6 +330,54 @@ pub enum Permission { AiModelInteract, Troubleshoot, SpamFilterClassify, + + // WebDAV permissions + DavSyncCollection, + DavExpandProperty, + + DavPrincipalAcl, + DavPrincipalList, + DavPrincipalMatch, + DavPrincipalSearch, + DavPrincipalSearchPropSet, + + DavFilePropFind, + DavFilePropPatch, + DavFileGet, + DavFileMkCol, + DavFileDelete, + DavFilePut, + DavFileCopy, + DavFileMove, + DavFileLock, + DavFileAcl, + + DavCardPropFind, + DavCardPropPatch, + DavCardGet, + DavCardMkCol, + DavCardDelete, + DavCardPut, + DavCardCopy, + DavCardMove, + DavCardLock, + DavCardAcl, + DavCardQuery, + DavCardMultiGet, + + DavCalPropFind, + DavCalPropPatch, + DavCalGet, + DavCalMkCol, + DavCalDelete, + DavCalPut, + DavCalCopy, + DavCalMove, + DavCalLock, + DavCalAcl, + DavCalQuery, + DavCalMultiGet, + DavCalFreeBusyQuery, // WARNING: add new ids at the end (TODO: use static ids) } diff --git a/crates/groupware/src/calendar/dates.rs b/crates/groupware/src/calendar/dates.rs index f7947cdd..196c0d2c 100644 --- a/crates/groupware/src/calendar/dates.rs +++ b/crates/groupware/src/calendar/dates.rs @@ -18,6 +18,7 @@ use calcard::{ }, }; use chrono::{DateTime, TimeZone}; +use compact_str::ToCompactString; use dav_proto::schema::property::TimeRange; use std::str::FromStr; use store::{ @@ -123,8 +124,17 @@ impl CalendarEventData { }); } - for error in expanded.errors { - let todo = "log me"; + if !expanded.errors.is_empty() { + trc::event!( + Calendar(trc::CalendarEvent::RuleExpansionError), + Reason = expanded + .errors + .into_iter() + .map(|e| e.error.to_compact_string()) + .collect::>(), + Details = ical.to_string(), + Limit = max_expansions, + ); } CalendarEventData { diff --git a/crates/groupware/src/lib.rs b/crates/groupware/src/lib.rs index 36d784f6..e287b32f 100644 --- a/crates/groupware/src/lib.rs +++ b/crates/groupware/src/lib.rs @@ -52,6 +52,15 @@ impl DavResourceName { DavResourceName::Principal => "/dav/pal/", } } + + pub fn name(&self) -> &'static str { + match self { + DavResourceName::Card => "CardDAV", + DavResourceName::Cal => "CalDAV", + DavResourceName::File => "WebDAV", + DavResourceName::Principal => "Principal", + } + } } impl From for Collection { diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index a3d10cda..dbe10f1b 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -213,7 +213,10 @@ impl ParseHttp for Server { (Some(_), Some(DavMethod::OPTIONS)) => HttpResponse::new(StatusCode::OK) .with_header( "DAV", - "1, 2, 3, access-control, extended-mkcol, calendar-access, addressbook", + concat!( + "1, 2, 3, access-control, extended-mkcol, calendar-access, ", + "calendar-no-timezone, addressbook" + ), ) .with_header( "Allow", diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index af4e3d7d..9ea0e6be 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -406,12 +406,12 @@ impl Session { .await } - pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> { + pub fn assert_has_permission(&self, permission: Permission) -> trc::Result { match &self.state { State::Authenticated { data } | State::Selected { data, .. } => { data.access_token.assert_has_permission(permission) } - State::NotAuthenticated { .. } => Ok(()), + State::NotAuthenticated { .. } => Ok(false), } } } diff --git a/crates/managesieve/src/op/mod.rs b/crates/managesieve/src/op/mod.rs index e49dc19e..b06e91cc 100644 --- a/crates/managesieve/src/op/mod.rs +++ b/crates/managesieve/src/op/mod.rs @@ -33,12 +33,12 @@ impl Session { Ok(StatusResponse::ok("Begin TLS negotiation now").into_bytes()) } - pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> { + pub fn assert_has_permission(&self, permission: Permission) -> trc::Result { match &self.state { State::Authenticated { access_token, .. } => { access_token.assert_has_permission(permission) } - State::NotAuthenticated { .. } => Ok(()), + State::NotAuthenticated { .. } => Ok(false), } } } diff --git a/crates/trc/src/event/description.rs b/crates/trc/src/event/description.rs index 33f3e863..27685060 100644 --- a/crates/trc/src/event/description.rs +++ b/crates/trc/src/event/description.rs @@ -52,6 +52,8 @@ impl EventType { EventType::MessageIngest(event) => event.description(), EventType::Security(event) => event.description(), EventType::Ai(event) => event.description(), + EventType::WebDav(event) => event.description(), + EventType::Calendar(event) => event.description(), } } @@ -100,6 +102,8 @@ impl EventType { EventType::MessageIngest(event) => event.explain(), EventType::Security(event) => event.explain(), EventType::Ai(event) => event.explain(), + EventType::WebDav(event) => event.explain(), + EventType::Calendar(event) => event.explain(), } } } @@ -1825,3 +1829,70 @@ impl AiEvent { } } } + +impl WebDavEvent { + pub fn description(&self) -> &'static str { + match self { + WebDavEvent::Propfind => "WebDAV PROPFIND request", + WebDavEvent::Proppatch => "WebDAV PROPPATCH request", + WebDavEvent::Get => "WebDAV GET request", + WebDavEvent::Report => "WebDAV REPORT request", + WebDavEvent::Mkcol => "WebDAV MKCOL request", + WebDavEvent::Delete => "WebDAV DELETE request", + WebDavEvent::Put => "WebDAV PUT request", + WebDavEvent::Post => "WebDAV POST request", + WebDavEvent::Patch => "WebDAV PATCH request", + WebDavEvent::Copy => "WebDAV COPY request", + WebDavEvent::Move => "WebDAV MOVE request", + WebDavEvent::Lock => "WebDAV LOCK request", + WebDavEvent::Unlock => "WebDAV UNLOCK request", + WebDavEvent::Acl => "WebDAV ACL request", + WebDavEvent::Error => "WebDAV error", + WebDavEvent::Head => "WebDAV HEAD request", + WebDavEvent::Mkcalendar => "WebDAV MKCALENDAR request", + WebDavEvent::Options => "WebDAV OPTIONS request", + } + } + + pub fn explain(&self) -> &'static str { + match self { + WebDavEvent::Propfind => "A PROPFIND request has been made to the server", + WebDavEvent::Proppatch => "A PROPPATCH request has been made to the server", + WebDavEvent::Get => "A GET request has been made to the server", + WebDavEvent::Report => "A REPORT request has been made to the server", + WebDavEvent::Mkcol => "A MKCOL request has been made to the server", + WebDavEvent::Delete => "A DELETE request has been made to the server", + WebDavEvent::Put => "A PUT request has been made to the server", + WebDavEvent::Post => "A POST request has been made to the server", + WebDavEvent::Patch => "A PATCH request has been made to the server", + WebDavEvent::Copy => "A COPY request has been made to the server", + WebDavEvent::Move => "A MOVE request has been made to the server", + WebDavEvent::Lock => "A LOCK request has been made to the server", + WebDavEvent::Unlock => "An UNLOCK request has been made to the server", + WebDavEvent::Acl => { + "An ACL request has been made to the + server" + } + WebDavEvent::Error => "An error occurred with the WebDAV request", + WebDavEvent::Head => "A HEAD request has been made to the server", + WebDavEvent::Mkcalendar => "A MKCALENDAR request has been made to the server", + WebDavEvent::Options => "An OPTIONS request has been made to the server", + } + } +} + +impl CalendarEvent { + pub fn description(&self) -> &'static str { + match self { + CalendarEvent::RuleExpansionError => "Calendar rule expansion error", + } + } + + pub fn explain(&self) -> &'static str { + match self { + CalendarEvent::RuleExpansionError => { + "An error occurred while expanding calendar recurrences" + } + } + } +} diff --git a/crates/trc/src/event/level.rs b/crates/trc/src/event/level.rs index 2a15514c..105fe9b4 100644 --- a/crates/trc/src/event/level.rs +++ b/crates/trc/src/event/level.rs @@ -532,6 +532,8 @@ impl EventType { AiEvent::LlmResponse => Level::Trace, AiEvent::ApiError => Level::Warn, }, + EventType::WebDav(_) => Level::Debug, + EventType::Calendar(CalendarEvent::RuleExpansionError) => Level::Debug, } } } diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index 4da98390..773f1b01 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -187,6 +187,8 @@ pub enum EventType { Telemetry(TelemetryEvent), Security(SecurityEvent), Ai(AiEvent), + WebDav(WebDavEvent), + Calendar(CalendarEvent), } #[event_type] @@ -950,6 +952,36 @@ pub enum AiEvent { ApiError, } +#[event_type] +pub enum WebDavEvent { + // Requests + Propfind, + Proppatch, + Get, + Head, + Report, + Mkcol, + Mkcalendar, + Delete, + Put, + Post, + Patch, + Copy, + Move, + Lock, + Unlock, + Acl, + Options, + + // Errors + Error, +} + +#[event_type] +pub enum CalendarEvent { + RuleExpansionError, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MetricType { ServerMemory, diff --git a/crates/trc/src/serializers/binary.rs b/crates/trc/src/serializers/binary.rs index 1cf01cc4..d0e06ec5 100644 --- a/crates/trc/src/serializers/binary.rs +++ b/crates/trc/src/serializers/binary.rs @@ -867,6 +867,25 @@ impl EventType { EventType::Spam(SpamEvent::Pyzor) => 564, EventType::Queue(QueueEvent::BackPressure) => 48, EventType::Imap(ImapEvent::GetQuota) => 57, + EventType::WebDav(WebDavEvent::Propfind) => 147, + EventType::WebDav(WebDavEvent::Proppatch) => 148, + EventType::WebDav(WebDavEvent::Get) => 335, + EventType::WebDav(WebDavEvent::Report) => 336, + EventType::WebDav(WebDavEvent::Mkcol) => 376, + EventType::WebDav(WebDavEvent::Delete) => 458, + EventType::WebDav(WebDavEvent::Put) => 459, + EventType::WebDav(WebDavEvent::Post) => 565, + EventType::WebDav(WebDavEvent::Patch) => 566, + EventType::WebDav(WebDavEvent::Copy) => 567, + EventType::WebDav(WebDavEvent::Move) => 568, + EventType::WebDav(WebDavEvent::Lock) => 569, + EventType::WebDav(WebDavEvent::Unlock) => 570, + EventType::WebDav(WebDavEvent::Acl) => 571, + EventType::WebDav(WebDavEvent::Error) => 572, + EventType::WebDav(WebDavEvent::Options) => 573, + EventType::WebDav(WebDavEvent::Head) => 574, + EventType::WebDav(WebDavEvent::Mkcalendar) => 575, + EventType::Calendar(CalendarEvent::RuleExpansionError) => 576, } } @@ -1470,13 +1489,30 @@ impl EventType { 564 => Some(EventType::Spam(SpamEvent::Pyzor)), 48 => Some(EventType::Queue(QueueEvent::BackPressure)), 57 => Some(EventType::Imap(ImapEvent::GetQuota)), + 147 => Some(EventType::WebDav(WebDavEvent::Propfind)), + 148 => Some(EventType::WebDav(WebDavEvent::Proppatch)), + 335 => Some(EventType::WebDav(WebDavEvent::Get)), + 336 => Some(EventType::WebDav(WebDavEvent::Report)), + 376 => Some(EventType::WebDav(WebDavEvent::Mkcol)), + 458 => Some(EventType::WebDav(WebDavEvent::Delete)), + 459 => Some(EventType::WebDav(WebDavEvent::Put)), + 565 => Some(EventType::WebDav(WebDavEvent::Post)), + 566 => Some(EventType::WebDav(WebDavEvent::Patch)), + 567 => Some(EventType::WebDav(WebDavEvent::Copy)), + 568 => Some(EventType::WebDav(WebDavEvent::Move)), + 569 => Some(EventType::WebDav(WebDavEvent::Lock)), + 570 => Some(EventType::WebDav(WebDavEvent::Unlock)), + 571 => Some(EventType::WebDav(WebDavEvent::Acl)), + 572 => Some(EventType::WebDav(WebDavEvent::Error)), + 573 => Some(EventType::WebDav(WebDavEvent::Options)), + 574 => Some(EventType::WebDav(WebDavEvent::Head)), + 575 => Some(EventType::WebDav(WebDavEvent::Mkcalendar)), + 576 => Some(EventType::Calendar(CalendarEvent::RuleExpansionError)), _ => None, } } } -// 147 148 335 336 376 458 459 - impl Key { fn code(&self) -> u64 { match self { diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs index 1559d5ad..db945db4 100644 --- a/tests/src/directory/internal.rs +++ b/tests/src/directory/internal.rs @@ -6,7 +6,7 @@ use ahash::AHashSet; use directory::{ - QueryBy, Type, + Permission, QueryBy, Type, backend::{ RcptType, internal::{ @@ -774,6 +774,7 @@ pub trait TestInternalDirectory { async fn create_test_group(&self, login: &str, name: &str, emails: &[&str]) -> u32; async fn create_test_list(&self, login: &str, name: &str, emails: &[&str]) -> u32; async fn set_test_quota(&self, login: &str, quota: u32); + async fn add_permissions(&self, login: &str, permissions: impl IntoIterator); async fn add_to_group(&self, login: &str, group: &str) -> ChangedPrincipals; async fn remove_from_group(&self, login: &str, group: &str) -> ChangedPrincipals; async fn remove_test_alias(&self, login: &str, alias: &str); @@ -898,6 +899,28 @@ impl TestInternalDirectory for Store { .unwrap(); } + async fn add_permissions( + &self, + login: &str, + permissions: impl IntoIterator, + ) { + self.update_principal( + UpdatePrincipal::by_name(login).with_updates( + permissions + .into_iter() + .map(|p| { + PrincipalUpdate::add_item( + PrincipalField::EnabledPermissions, + PrincipalValue::String(p.name().to_string()), + ) + }) + .collect(), + ), + ) + .await + .unwrap(); + } + async fn add_to_group(&self, login: &str, group: &str) -> ChangedPrincipals { self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![ PrincipalUpdate::add_item( diff --git a/tests/src/webdav/basic.rs b/tests/src/webdav/basic.rs index 211dd837..dd4a5833 100644 --- a/tests/src/webdav/basic.rs +++ b/tests/src/webdav/basic.rs @@ -16,7 +16,10 @@ pub async fn test(test: &WebDavTest) { .await .with_header( "dav", - "1, 2, 3, access-control, extended-mkcol, calendar-access, addressbook", + concat!( + "1, 2, 3, access-control, extended-mkcol, ", + "calendar-access, calendar-no-timezone, addressbook" + ), ) .with_header( "allow", diff --git a/tests/src/webdav/copy_move.rs b/tests/src/webdav/copy_move.rs index 5fe21fb7..d79fa60f 100644 --- a/tests/src/webdav/copy_move.rs +++ b/tests/src/webdav/copy_move.rs @@ -70,6 +70,28 @@ pub async fn test(test: &WebDavTest) { ); client.validate_values(&hierarchy).await; + // Delete cache an resync + test.clear_cache(); + let response = client + .sync_collection( + &user_base_path, + prev_sync_token, + Depth::Infinity, + None, + ["D:getetag"], + ) + .await; + let sync_token = response.sync_token(); + let changed_hrefs = response.hrefs(); + assert_ne!(sync_token, prev_sync_token); + assert_eq!( + changed_hrefs, + hierarchy.iter().map(|x| x.0.as_str()).collect::>(), + "lengths {} & {}", + changed_hrefs.len(), + hierarchy.len() + ); + // Copying and moving to the same or root containers is invalid for method in ["COPY", "MOVE"] { for destination in [ diff --git a/tests/src/webdav/mod.rs b/tests/src/webdav/mod.rs index 4270fdb3..2c7688b5 100644 --- a/tests/src/webdav/mod.rs +++ b/tests/src/webdav/mod.rs @@ -22,6 +22,7 @@ use common::{ manager::boot::build_ipc, }; use dav_proto::schema::property::{DavProperty, WebDavProperty}; +use directory::Permission; use groupware::{DavResourceName, cache::GroupwareCache}; use http::HttpSessionManager; use hyper::{HeaderMap, Method, StatusCode, header::AUTHORIZATION}; @@ -201,6 +202,12 @@ async fn init_webdav_tests(store_id: &str, delete_if_exists: bool) -> WebDavTest *account, DummyWebDavClient::new(account_id, account, secret, email), ); + store + .add_permissions( + account, + [Permission::DavPrincipalList, Permission::DavPrincipalSearch], + ) + .await; if *account == "mike" { store.set_test_quota(account, 1024).await; } @@ -232,8 +239,7 @@ impl WebDavTest { .unwrap() } - pub async fn assert_is_empty(&self) { - assert_is_empty(self.server.clone()).await; + pub fn clear_cache(&self) { for cache in [ &self.server.inner.cache.events, &self.server.inner.cache.contacts, @@ -242,6 +248,11 @@ impl WebDavTest { cache.clear(); } } + + pub async fn assert_is_empty(&self) { + assert_is_empty(self.server.clone()).await; + self.clear_cache(); + } } #[allow(dead_code)]