WebDAV permissions and logging (closes #1362)

This commit is contained in:
mdecimus 2025-05-09 15:47:09 +02:00
parent fe7d646966
commit 095c501a66
50 changed files with 1031 additions and 273 deletions

2
Cargo.lock generated
View file

@ -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]]

View file

@ -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<bool> {
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,

View file

@ -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"] }

View file

@ -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<T: AsRef<str>> {
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:

View file

@ -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(())
}
}
}
}

View file

@ -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;

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// Validate URI

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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,

View file

@ -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<MkCol>,
) -> impl Future<Output = crate::Result<HttpResponse>> + 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<MkCol>,
) -> crate::Result<HttpResponse> {
// 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,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// Validate URI
@ -120,7 +120,7 @@ impl CalendarPropPatchRequestHandler for Server {
// Validate headers
self.validate_headers(
access_token,
&headers,
headers,
vec![ResourceState {
account_id,
collection,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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![]
})
})

View file

@ -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<u8>,
is_patch: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + 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<u8>,
_is_patch: bool,
) -> crate::Result<HttpResponse> {
@ -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,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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,

View file

@ -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<MkCol>,
) -> impl Future<Output = crate::Result<HttpResponse>> + 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<MkCol>,
) -> crate::Result<HttpResponse> {
// 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,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// Validate URI
@ -117,7 +117,7 @@ impl CardPropPatchRequestHandler for Server {
// Validate headers
self.validate_headers(
access_token,
&headers,
headers,
vec![ResourceState {
account_id,
collection,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// Validate URI

View file

@ -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<u8>,
is_patch: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + 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<u8>,
_is_patch: bool,
) -> crate::Result<HttpResponse> {
@ -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,

View file

@ -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<Output = crate::Result<HttpResponse>> + Send;
fn handle_acl_prop_set(
&self,
access_token: &AccessToken,
headers: RequestHeaders<'_>,
headers: &RequestHeaders<'_>,
request: AclPrincipalPropSet,
) -> impl Future<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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<HttpResponse> {
let uri = self

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
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,

View file

@ -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<PropFindItem>,
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<TimeRange>,
items: Vec<PropFindItem>,
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 {

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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(

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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,

View file

@ -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<MkCol>,
) -> impl Future<Output = crate::Result<HttpResponse>> + 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<MkCol>,
) -> crate::Result<HttpResponse> {
// 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,

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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,

View file

@ -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<u8>,
is_patch: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + 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<u8>,
_is_patch: bool,
) -> crate::Result<HttpResponse> {
@ -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,

View file

@ -42,6 +42,30 @@ pub enum DavMethod {
ACL,
}
impl From<DavMethod> 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),

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
let resource = self.validate_uri(access_token, headers.uri).await?;

View file

@ -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<AccessToken>,
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<AccessToken>,
resource: DavResourceName,
method: DavMethod,
body: Vec<u8>,
) -> crate::Result<HttpResponse> {
// 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(),
}

View file

@ -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",
}
}
}

View file

@ -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
)
}

View file

@ -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)
}

View file

@ -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::<Vec<_>>(),
Details = ical.to_string(),
Limit = max_expansions,
);
}
CalendarEventData {

View file

@ -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<DavResourceName> for Collection {

View file

@ -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",

View file

@ -406,12 +406,12 @@ impl<T: SessionStream> Session<T> {
.await
}
pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> {
pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<bool> {
match &self.state {
State::Authenticated { data } | State::Selected { data, .. } => {
data.access_token.assert_has_permission(permission)
}
State::NotAuthenticated { .. } => Ok(()),
State::NotAuthenticated { .. } => Ok(false),
}
}
}

View file

@ -33,12 +33,12 @@ impl<T: SessionStream> Session<T> {
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<bool> {
match &self.state {
State::Authenticated { access_token, .. } => {
access_token.assert_has_permission(permission)
}
State::NotAuthenticated { .. } => Ok(()),
State::NotAuthenticated { .. } => Ok(false),
}
}
}

View file

@ -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"
}
}
}
}

View file

@ -532,6 +532,8 @@ impl EventType {
AiEvent::LlmResponse => Level::Trace,
AiEvent::ApiError => Level::Warn,
},
EventType::WebDav(_) => Level::Debug,
EventType::Calendar(CalendarEvent::RuleExpansionError) => Level::Debug,
}
}
}

View file

@ -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,

View file

@ -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 {

View file

@ -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<Item = Permission>);
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<Item = Permission>,
) {
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(

View file

@ -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",

View file

@ -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::<Vec<_>>(),
"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 [

View file

@ -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)]