diff --git a/crates/common/src/config/dav.rs b/crates/common/src/config/dav.rs index bd655371..9518fcf3 100644 --- a/crates/common/src/config/dav.rs +++ b/crates/common/src/config/dav.rs @@ -14,6 +14,7 @@ pub struct DavConfig { pub max_lock_timeout: u64, pub max_locks_per_user: usize, pub max_changes: usize, + pub max_match_results: usize, } impl DavConfig { @@ -33,6 +34,9 @@ impl DavConfig { .property("dav.limits.max-locks-per-user") .unwrap_or(10), max_changes: config.property("dav.limits.max-changes").unwrap_or(1000), + max_match_results: config + .property("dav.limits.max-match-results") + .unwrap_or(1000), } } } diff --git a/crates/dav-proto/src/responses/property.rs b/crates/dav-proto/src/responses/property.rs index c097d3ba..e529c469 100644 --- a/crates/dav-proto/src/responses/property.rs +++ b/crates/dav-proto/src/responses/property.rs @@ -170,6 +170,18 @@ impl Display for ReportSet { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ReportSet::SyncCollection => write!(f, ""), + ReportSet::ExpandProperty => write!(f, ""), + ReportSet::AddressbookQuery => write!(f, ""), + ReportSet::AddressbookMultiGet => write!(f, ""), + ReportSet::CalendarQuery => write!(f, ""), + ReportSet::CalendarMultiGet => write!(f, ""), + ReportSet::FreeBusyQuery => write!(f, ""), + ReportSet::AclPrincipalPropSet => write!(f, ""), + ReportSet::PrincipalMatch => write!(f, ""), + ReportSet::PrincipalPropertySearch => write!(f, ""), + ReportSet::PrincipalSearchPropertySet => { + write!(f, "") + } } } } diff --git a/crates/dav-proto/src/schema/property.rs b/crates/dav-proto/src/schema/property.rs index e3531369..ada74ee5 100644 --- a/crates/dav-proto/src/schema/property.rs +++ b/crates/dav-proto/src/schema/property.rs @@ -160,6 +160,16 @@ pub enum DavValue { #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum ReportSet { SyncCollection, + ExpandProperty, + AddressbookQuery, + AddressbookMultiGet, + CalendarQuery, + CalendarMultiGet, + FreeBusyQuery, + AclPrincipalPropSet, + PrincipalMatch, + PrincipalPropertySearch, + PrincipalSearchPropertySet, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/dav-proto/src/schema/request.rs b/crates/dav-proto/src/schema/request.rs index ed12bf8c..7bb63b7a 100644 --- a/crates/dav-proto/src/schema/request.rs +++ b/crates/dav-proto/src/schema/request.rs @@ -338,3 +338,9 @@ impl ArchivedDeadProperty { } } } + +impl PropertyUpdate { + pub fn has_changes(&self) -> bool { + !self.set.is_empty() || !self.remove.is_empty() + } +} diff --git a/crates/dav-proto/src/schema/response.rs b/crates/dav-proto/src/schema/response.rs index c8f4af5b..3ebb9ea6 100644 --- a/crates/dav-proto/src/schema/response.rs +++ b/crates/dav-proto/src/schema/response.rs @@ -55,7 +55,7 @@ pub struct SyncToken(pub String); #[repr(transparent)] pub struct Href(pub String); -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] #[repr(transparent)] pub struct List(pub Vec); diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index d7fba69d..494a7c7e 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -5,21 +5,53 @@ */ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; -use dav_proto::schema::{ - property::Privilege, - response::{Ace, GrantDeny, Href, Principal}, +use dav_proto::{ + RequestHeaders, + schema::{ + property::{DavProperty, Privilege, WebDavProperty}, + request::{AclPrincipalPropSet, PropFind}, + response::{Ace, BaseCondition, GrantDeny, Href, MultiStatus, Principal}, + }, }; -use directory::{QueryBy, backend::internal::PrincipalField}; +use directory::{QueryBy, Type, backend::internal::PrincipalField}; +use groupware::{calendar::Calendar, contact::AddressBook, file::FileNode}; +use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection, value::ArchivedAclGrant}; +use jmap_proto::types::{ + acl::Acl, + collection::Collection, + property::Property, + value::{AclGrant, ArchivedAclGrant}, +}; use rkyv::vec::ArchivedVec; -use store::ahash::AHashSet; +use store::{ + ahash::AHashSet, + roaring::RoaringBitmap, + write::{AlignedBytes, Archive}, +}; use trc::AddContext; use utils::map::bitmap::Bitmap; -use crate::{DavError, DavResource}; +use crate::{ + DavError, DavErrorCondition, DavResource, common::uri::DavUriResource, + principal::propfind::PrincipalPropFind, +}; pub(crate) trait DavAclHandler: Sync + Send { + fn handle_acl_prop_set( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: AclPrincipalPropSet, + ) -> impl Future> + Send; + + fn validate_and_map_aces( + &self, + access_token: &AccessToken, + acl: dav_proto::schema::request::Acl, + collection: Collection, + ) -> impl Future>> + Send; + fn validate_and_map_parent_acl( &self, access_token: &AccessToken, @@ -48,6 +80,227 @@ pub(crate) trait DavAclHandler: Sync + Send { } impl DavAclHandler for Server { + async fn handle_acl_prop_set( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + mut request: AclPrincipalPropSet, + ) -> crate::Result { + let uri = self + .validate_uri(access_token, headers.uri) + .await + .and_then(|uri| uri.into_owned_uri())?; + let uri = self + .map_uri_resource(uri) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + + if !matches!( + uri.collection, + Collection::Calendar | Collection::AddressBook | Collection::FileNode + ) { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + + // Validate ACLs + self.validate_child_or_parent_acl( + access_token, + uri.account_id, + uri.collection, + uri.resource, + None, + Acl::Read, + Acl::Read, + ) + .await?; + + let archive = self + .get_property::>( + uri.account_id, + uri.collection, + uri.resource, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + + let acls = match uri.collection { + Collection::FileNode => { + &archive + .unarchive::() + .caused_by(trc::location!())? + .acls + } + Collection::AddressBook => { + &archive + .unarchive::() + .caused_by(trc::location!())? + .acls + } + Collection::Calendar => { + &archive + .unarchive::() + .caused_by(trc::location!())? + .acls + } + _ => unreachable!(), + }; + let account_ids = RoaringBitmap::from_iter(acls.iter().map(|a| u32::from(a.account_id))); + + let mut response = MultiStatus::new(Vec::with_capacity(16)); + + if !account_ids.is_empty() { + if request.properties.is_empty() { + request + .properties + .push(DavProperty::WebDav(WebDavProperty::DisplayName)); + } + let request = PropFind::Prop(request.properties); + self.prepare_principal_propfind_response( + access_token, + Collection::Principal, + account_ids.into_iter(), + &request, + &mut response, + ) + .await?; + } + + Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())) + } + + async fn validate_and_map_aces( + &self, + access_token: &AccessToken, + acl: dav_proto::schema::request::Acl, + collection: Collection, + ) -> crate::Result> { + let mut grants = Vec::with_capacity(acl.aces.len()); + for ace in acl.aces { + if ace.invert { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::NoInvert, + ))); + } + let privileges = match ace.grant_deny { + GrantDeny::Grant(list) => list.0, + GrantDeny::Deny(_) => { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::GrantOnly, + ))); + } + }; + let principal_uri = match ace.principal { + Principal::Href(href) => href.0, + _ => { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::AllowedPrincipal, + ))); + } + }; + + let mut acls = Bitmap::::default(); + for privilege in privileges { + match privilege { + Privilege::Read => { + acls.insert(Acl::Read); + acls.insert(Acl::ReadItems); + } + Privilege::Write => { + acls.insert(Acl::Modify); + acls.insert(Acl::Delete); + acls.insert(Acl::ModifyItems); + acls.insert(Acl::RemoveItems); + } + Privilege::WriteContent => { + acls.insert(Acl::Modify); + acls.insert(Acl::ModifyItems); + acls.insert(Acl::RemoveItems); + } + Privilege::WriteProperties => { + acls.insert(Acl::Modify); + } + Privilege::ReadCurrentUserPrivilegeSet + | Privilege::Unlock + | Privilege::Bind + | Privilege::Unbind => {} + Privilege::All => { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::NoAbstract, + ))); + } + Privilege::ReadAcl => {} + Privilege::WriteAcl => { + acls.insert(Acl::Administer); + } + Privilege::ReadFreeBusy => { + if collection == Collection::Calendar { + acls.insert(Acl::ReadFreeBusy); + } else { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::NotSupportedPrivilege, + ))); + } + } + } + } + + if acls.is_empty() { + continue; + } + + let principal_id = self + .validate_uri(access_token, &principal_uri) + .await + .map_err(|_| { + DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::AllowedPrincipal, + )) + })? + .account_id + .ok_or_else(|| { + DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::AllowedPrincipal, + )) + })?; + + // Verify that the principal is a valid principal + let principal = self + .directory() + .query(QueryBy::Id(principal_id), false) + .await + .caused_by(trc::location!())? + .ok_or_else(|| { + DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::AllowedPrincipal, + )) + })?; + if !matches!(principal.typ(), Type::Individual | Type::Group) { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::AllowedPrincipal, + ))); + } + + grants.push(AclGrant { + account_id: principal_id, + grants: acls, + }); + } + + Ok(grants) + } + async fn validate_and_map_parent_acl( &self, access_token: &AccessToken, diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index e9c83a65..15e4bcae 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -11,7 +11,6 @@ use dav_proto::schema::request::{DavPropertyValue, DeadProperty}; use dav_proto::schema::response::{BaseCondition, List, PropResponse}; use dav_proto::{Condition, Depth, Timeout}; use dav_proto::{RequestHeaders, schema::request::LockInfo}; -use groupware::file::hierarchy::FileHierarchy; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::collection::Collection; @@ -24,7 +23,7 @@ use store::{SERIALIZE_OBJ_02_V1, Serialize, SerializedVersion, U32_LEN}; use trc::AddContext; use super::ETag; -use super::uri::{DavUriResource, OwnedUri, Urn}; +use super::uri::{DavUriResource, OwnedUri, UriResource, Urn}; use crate::{DavError, DavErrorCondition, DavMethod}; #[derive(Debug, Default, Clone)] @@ -476,24 +475,17 @@ impl LockRequestHandler for Server { // Fetch eTag if needs_etag && resource_state.etag.is_none() { if resource_state.document_id.is_none() { - let todo = "map cal, card"; - - resource_state.document_id = match resource_state.collection { - Collection::FileNode => self - .fetch_file_hierarchy(resource_state.account_id) - .await - .caused_by(trc::location!())? - .files - .by_name(resource_state.path) - .map(|f| f.document_id), - Collection::Calendar => todo!(), - Collection::CalendarEvent => todo!(), - Collection::AddressBook => todo!(), - Collection::ContactCard => todo!(), - _ => None, - } - .unwrap_or(u32::MAX) - .into(); + resource_state.document_id = self + .map_uri_resource(UriResource { + collection: resource_state.collection, + account_id: resource_state.account_id, + resource: resource_state.path.into(), + }) + .await + .caused_by(trc::location!())? + .map(|uri| uri.resource) + .unwrap_or(u32::MAX) + .into(); } if let Some(document_id) = diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index fc1afb15..87b29638 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use common::{ + Server, + auth::{AccessToken, AsTenantId}, +}; use dav_proto::{ Depth, RequestHeaders, schema::{ @@ -13,6 +16,10 @@ use dav_proto::{ response::{BaseCondition, MultiStatus, PropStat, Response}, }, }; +use directory::{ + Type, + backend::internal::{PrincipalField, manage::ManageDirectory}, +}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::collection::Collection; @@ -161,9 +168,20 @@ impl PropFindRequestHandler for Server { RoaringBitmap::from_iter(access_token.all_ids()) } else { // Return all principals - self.get_document_ids(u32::MAX, Collection::Principal) - .await? - .unwrap_or_default() + let principals = self + .store() + .list_principals( + None, + access_token.tenant_id(), + &[Type::Individual, Type::Group], + &[PrincipalField::Name], + 0, + 0, + ) + .await + .caused_by(trc::location!())?; + + RoaringBitmap::from_iter(principals.items.into_iter().map(|p| p.id())) }; self.prepare_principal_propfind_response( diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index d2dc79be..b1f0f08f 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -9,6 +9,7 @@ use std::fmt::Display; use common::{Server, auth::AccessToken}; use directory::backend::internal::manage::ManageDirectory; +use groupware::file::hierarchy::FileHierarchy; use http_proto::request::decode_path_element; use hyper::StatusCode; use jmap_proto::types::collection::Collection; @@ -29,7 +30,7 @@ pub(crate) enum Urn { pub(crate) type UnresolvedUri<'x> = UriResource, Option<&'x str>>; pub(crate) type OwnedUri<'x> = UriResource>; -//pub(crate) type DocumentUri<'x> = UriResource; +pub(crate) type DocumentUri = UriResource; pub(crate) trait DavUriResource: Sync + Send { fn validate_uri<'x>( @@ -37,6 +38,11 @@ pub(crate) trait DavUriResource: Sync + Send { access_token: &AccessToken, uri: &'x str, ) -> impl Future>> + Send; + + fn map_uri_resource( + &self, + uri: OwnedUri<'_>, + ) -> impl Future>> + Send; } impl DavUriResource for Server { @@ -95,6 +101,41 @@ impl DavUriResource for Server { Ok(resource) } + + async fn map_uri_resource(&self, uri: OwnedUri<'_>) -> trc::Result> { + let todo = "map cal, card"; + + let resource = if let Some(resource) = uri.resource { + resource + } else { + return Ok(None); + }; + + let document_id = match uri.collection { + Collection::FileNode => self + .fetch_file_hierarchy(uri.account_id) + .await + .caused_by(trc::location!())? + .files + .by_name(resource) + .map(|f| f.document_id), + Collection::Calendar => todo!(), + Collection::CalendarEvent => todo!(), + Collection::AddressBook => todo!(), + Collection::ContactCard => todo!(), + _ => None, + }; + + if let Some(document_id) = document_id { + Ok(Some(DocumentUri { + collection: uri.collection, + account_id: uri.account_id, + resource: document_id, + })) + } else { + Ok(None) + } + } } impl<'x> UnresolvedUri<'x> { diff --git a/crates/dav/src/file/acl.rs b/crates/dav/src/file/acl.rs index 5885f323..39200899 100644 --- a/crates/dav/src/file/acl.rs +++ b/crates/dav/src/file/acl.rs @@ -13,7 +13,11 @@ use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; use store::write::{AlignedBytes, Archive}; use trc::AddContext; -use crate::{DavError, common::uri::DavUriResource, file::DavFileResource}; +use crate::{ + DavError, + common::{acl::DavAclHandler, uri::DavUriResource}, + file::{DavFileResource, update_file_node}, +}; pub(crate) trait FileAclRequestHandler: Sync + Send { fn handle_file_acl_request( @@ -63,21 +67,48 @@ impl FileAclRequestHandler for Server { .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - let node = node_.unarchive::().caused_by(trc::location!())?; + let node = node_ + .to_unarchived::() + .caused_by(trc::location!())?; // Validate ACL self.validate_file_acl( access_token, account_id, - node, + node.inner, Acl::Administer, Acl::Administer, ) .await?; - for ace in request.aces {} + let grants = self + .validate_and_map_aces(access_token, request, Collection::FileNode) + .await?; - todo!() + if grants.len() != node.inner.acls.len() + || node + .inner + .acls + .iter() + .zip(grants.iter()) + .any(|(a, b)| a != b) + { + let mut new_node = node.deserialize().caused_by(trc::location!())?; + new_node.acls = grants; + update_file_node( + self, + access_token, + node, + new_node, + account_id, + resource.resource, + false, + ) + .await + .caused_by(trc::location!())?; + } + + Ok(HttpResponse::new(StatusCode::OK)) } async fn validate_file_acl( diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index 9b09989b..e2f4f45c 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -365,7 +365,7 @@ async fn move_container( if parent_id != 0 && to_files.is_ancestor_of(from_document_id, parent_id - 1) { return Err(DavError::Code(StatusCode::BAD_GATEWAY)); } - let node = server + let node_ = server .get_property::>( from_account_id, Collection::FileNode, @@ -374,10 +374,11 @@ async fn move_container( ) .await .caused_by(trc::location!())? - .ok_or(DavError::Code(StatusCode::NOT_FOUND))? - .into_deserialized::() + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let node = node_ + .to_unarchived::() .caused_by(trc::location!())?; - let mut new_node = node.inner.clone(); + let mut new_node = node.deserialize().caused_by(trc::location!())?; new_node.parent_id = parent_id; if let Some(new_name) = destination.new_name { new_node.name = new_name; @@ -579,7 +580,7 @@ async fn overwrite_and_delete_item( let to_document_id = destination.document_id.unwrap(); // dest_node is the current file at the destination - let dest_node = server + let dest_node_ = server .get_property::>( to_account_id, Collection::FileNode, @@ -588,12 +589,14 @@ async fn overwrite_and_delete_item( ) .await .caused_by(trc::location!())? - .ok_or(DavError::Code(StatusCode::NOT_FOUND))? - .into_deserialized::() + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + + let dest_node = dest_node_ + .to_unarchived::() .caused_by(trc::location!())?; // source_node is the file to be copied - let source_node_ = server + let source_node__ = server .get_property::>( from_account_id, Collection::FileNode, @@ -602,16 +605,17 @@ async fn overwrite_and_delete_item( ) .await .caused_by(trc::location!())? - .ok_or(DavError::Code(StatusCode::NOT_FOUND))? - .into_deserialized::() + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let source_node_ = source_node__ + .to_unarchived::() .caused_by(trc::location!())?; - let mut source_node = source_node_.inner.clone(); + let mut source_node = source_node_.deserialize().caused_by(trc::location!())?; source_node.name = if let Some(new_name) = destination.new_name { new_name } else { - dest_node.inner.name.clone() + dest_node.inner.name.to_string() }; - source_node.parent_id = dest_node.inner.parent_id; + source_node.parent_id = dest_node.inner.parent_id.into(); let etag = update_file_node( server, @@ -651,7 +655,7 @@ async fn overwrite_item( let to_document_id = destination.document_id.unwrap(); // dest_node is the current file at the destination - let dest_node = server + let dest_node_ = server .get_property::>( to_account_id, Collection::FileNode, @@ -660,8 +664,10 @@ async fn overwrite_item( ) .await .caused_by(trc::location!())? - .ok_or(DavError::Code(StatusCode::NOT_FOUND))? - .into_deserialized::() + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + + let dest_node = dest_node_ + .to_unarchived::() .caused_by(trc::location!())?; // source_node is the file to be copied @@ -680,9 +686,9 @@ async fn overwrite_item( source_node.name = if let Some(new_name) = destination.new_name { new_name } else { - dest_node.inner.name.clone() + dest_node.inner.name.to_string() }; - source_node.parent_id = dest_node.inner.parent_id; + source_node.parent_id = dest_node.inner.parent_id.into(); let etag = update_file_node( server, @@ -711,7 +717,7 @@ async fn move_item( let from_document_id = from_resource.resource.document_id; let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0); - let node = server + let node_ = server .get_property::>( from_account_id, Collection::FileNode, @@ -720,10 +726,11 @@ async fn move_item( ) .await .caused_by(trc::location!())? - .ok_or(DavError::Code(StatusCode::NOT_FOUND))? - .into_deserialized::() + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let node = node_ + .to_unarchived::() .caused_by(trc::location!())?; - let mut new_node = node.inner.clone(); + let mut new_node = node.deserialize().caused_by(trc::location!())?; new_node.parent_id = parent_id; if let Some(new_name) = destination.new_name { new_node.name = new_name; @@ -807,7 +814,7 @@ async fn rename_item( let from_account_id = from_resource.account_id; let from_document_id = from_resource.resource.document_id; - let node = server + let node_ = server .get_property::>( from_account_id, Collection::FileNode, @@ -816,10 +823,11 @@ async fn rename_item( ) .await .caused_by(trc::location!())? - .ok_or(DavError::Code(StatusCode::NOT_FOUND))? - .into_deserialized::() + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let node = node_ + .to_unarchived::() .caused_by(trc::location!())?; - let mut new_node = node.inner.clone(); + let mut new_node = node.deserialize().caused_by(trc::location!())?; if let Some(new_name) = destination.new_name { new_node.name = new_name; } diff --git a/crates/dav/src/file/delete.rs b/crates/dav/src/file/delete.rs index 7f4c5d5d..4631dfb5 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -103,8 +103,6 @@ impl FileDeleteRequestHandler for Server { ) .await?; - let c = println!("DELETE files: {:?}", sorted_ids); - delete_files(self, access_token, account_id, sorted_ids).await?; Ok(HttpResponse::new(StatusCode::NO_CONTENT)) diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index 18d2dbf3..bc8e58ab 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -5,7 +5,7 @@ */ use common::{FileItem, Files, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; -use groupware::file::FileNode; +use groupware::file::{ArchivedFileNode, FileNode}; use hyper::StatusCode; use jmap_proto::types::{collection::Collection, type_state::DataType}; use store::write::{ @@ -126,7 +126,7 @@ impl FromFileItem for FileItemId { pub(crate) async fn update_file_node( server: &Server, access_token: &AccessToken, - node: Archive, + node: Archive<&ArchivedFileNode>, mut new_node: FileNode, account_id: u32, document_id: u32, @@ -202,7 +202,7 @@ pub(crate) async fn insert_file_node( pub(crate) async fn delete_file_node( server: &Server, access_token: &AccessToken, - node: Archive, + node: Archive<&ArchivedFileNode>, account_id: u32, document_id: u32, ) -> trc::Result<()> { diff --git a/crates/dav/src/file/propfind.rs b/crates/dav/src/file/propfind.rs index 5c4b92bb..2f169356 100644 --- a/crates/dav/src/file/propfind.rs +++ b/crates/dav/src/file/propfind.rs @@ -12,7 +12,8 @@ use dav_proto::schema::{ }, request::{DavPropertyValue, PropFind}, response::{ - AclRestrictions, Href, MultiStatus, PropStat, Response, ResponseType, SupportedPrivilege, + AclRestrictions, BaseCondition, Href, MultiStatus, PropStat, Response, ResponseType, + SupportedPrivilege, }, }; use groupware::file::{FileNode, hierarchy::FileHierarchy}; @@ -30,7 +31,7 @@ use trc::AddContext; use utils::map::bitmap::Bitmap; use crate::{ - DavResource, + DavError, DavErrorCondition, DavResource, common::{ DavQuery, ETag, acl::{DavAclHandler, Privileges}, @@ -146,6 +147,11 @@ impl HandleFilePropFindRequest for Server { return Ok( HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()) ); + } else if query.depth == usize::MAX && paths.len() > self.core.dav.max_match_results { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + BaseCondition::NumberOfMatchesWithinLimit, + ))); } // Prepare response @@ -379,7 +385,11 @@ impl HandleFilePropFindRequest for Server { if node.file.is_none() { fields.push(DavPropertyValue::new( property.clone(), - vec![ReportSet::SyncCollection], + vec![ + ReportSet::SyncCollection, + ReportSet::AclPrincipalPropSet, + ReportSet::PrincipalMatch, + ], )); } else if !is_all_prop { fields_not_found @@ -492,7 +502,9 @@ impl HandleFilePropFindRequest for Server { WebDavProperty::AclRestrictions => { fields.push(DavPropertyValue::new( property.clone(), - AclRestrictions::default().with_no_invert(), + AclRestrictions::default() + .with_no_invert() + .with_grant_only(), )); } WebDavProperty::InheritedAclSet => { diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index 8c01e607..ec7c483e 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -69,6 +69,10 @@ impl FilePropPatchRequestHandler for Server { .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; + if !request.has_changes() { + return Ok(HttpResponse::new(StatusCode::NO_CONTENT)); + } + // Fetch node let node_ = self .get_property::>( @@ -112,8 +116,7 @@ impl FilePropPatchRequestHandler for Server { .await?; // Deserialize - let node = node.to_deserialized().caused_by(trc::location!())?; - let mut new_node = node.inner.clone(); + let mut new_node = node.deserialize().caused_by(trc::location!())?; // Remove properties let mut items = Vec::with_capacity(request.remove.len() + request.set.len()); @@ -133,7 +136,7 @@ impl FilePropPatchRequestHandler for Server { remove_file_properties(&mut new_node, request.remove, &mut items); } - let etag = if new_node != node.inner { + let etag = if is_success { update_file_node( self, access_token, diff --git a/crates/dav/src/principal/matching.rs b/crates/dav/src/principal/matching.rs new file mode 100644 index 00000000..fdbc80c2 --- /dev/null +++ b/crates/dav/src/principal/matching.rs @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::{ + RequestHeaders, Return, + schema::{ + property::{DavProperty, WebDavProperty}, + request::{PrincipalMatch, PropFind}, + response::MultiStatus, + }, +}; +use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::collection::Collection; +use store::roaring::RoaringBitmap; + +use crate::{ + DavError, + common::{DavQuery, uri::DavUriResource}, + file::propfind::HandleFilePropFindRequest, +}; + +use super::propfind::PrincipalPropFind; + +pub(crate) trait PrincipalMatching: Sync + Send { + fn handle_principal_match( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: PrincipalMatch, + ) -> impl Future> + Send; +} + +impl PrincipalMatching for Server { + async fn handle_principal_match( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + mut request: PrincipalMatch, + ) -> crate::Result { + let resource = self + .validate_uri(access_token, headers.uri) + .await + .and_then(|uri| uri.into_owned_uri())?; + + let todo = "implement cal, card"; + + match resource.collection { + Collection::Calendar => todo!(), + Collection::AddressBook => todo!(), + Collection::FileNode => { + self.handle_file_propfind_request( + access_token, + DavQuery { + resource, + base_uri: headers.uri, + propfind: PropFind::Prop(request.properties), + from_change_id: None, + depth: usize::MAX, + limit: None, + ret: headers.ret, + depth_no_root: headers.depth_no_root, + }, + ) + .await + } + Collection::Principal => { + let mut response = MultiStatus::new(Vec::with_capacity(16)); + if request.properties.is_empty() { + request + .properties + .push(DavProperty::WebDav(WebDavProperty::DisplayName)); + } + let request = PropFind::Prop(request.properties); + self.prepare_principal_propfind_response( + access_token, + Collection::Principal, + RoaringBitmap::from_iter(access_token.all_ids()).into_iter(), + &request, + &mut response, + ) + .await?; + Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())) + } + _ => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + } + } +} diff --git a/crates/dav/src/principal/mod.rs b/crates/dav/src/principal/mod.rs index d961d6f2..fdd659cb 100644 --- a/crates/dav/src/principal/mod.rs +++ b/crates/dav/src/principal/mod.rs @@ -10,7 +10,9 @@ use percent_encoding::NON_ALPHANUMERIC; use crate::DavResource; +pub mod matching; pub mod propfind; +pub mod propsearch; pub trait CurrentUserPrincipal { fn current_user_principal(&self) -> Href; diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index f7d49621..40c18583 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -141,11 +141,21 @@ impl PrincipalPropFind for Server { fields.push(DavPropertyValue::empty(property.clone())); } } - WebDavProperty::SupportedReportSet if !is_principal => { - fields.push(DavPropertyValue::new( - property.clone(), - vec![ReportSet::SyncCollection], - )); + WebDavProperty::SupportedReportSet => { + let reports = if !is_principal { + vec![ + ReportSet::SyncCollection, + ReportSet::AclPrincipalPropSet, + ReportSet::PrincipalMatch, + ] + } else { + vec![ + ReportSet::PrincipalPropertySearch, + ReportSet::PrincipalSearchPropertySet, + ReportSet::PrincipalMatch, + ] + }; + fields.push(DavPropertyValue::new(property.clone(), reports)); } WebDavProperty::CurrentUserPrincipal => { fields.push(DavPropertyValue::new( diff --git a/crates/dav/src/principal/propsearch.rs b/crates/dav/src/principal/propsearch.rs new file mode 100644 index 00000000..f4d91987 --- /dev/null +++ b/crates/dav/src/principal/propsearch.rs @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{ + Server, + auth::{AccessToken, AsTenantId}, +}; +use dav_proto::schema::{ + property::{DavProperty, WebDavProperty}, + request::{PrincipalPropertySearch, PropFind}, + response::MultiStatus, +}; +use directory::{ + Type, + backend::internal::{PrincipalField, manage::ManageDirectory}, +}; +use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::collection::Collection; +use store::roaring::RoaringBitmap; +use trc::AddContext; + +use super::propfind::PrincipalPropFind; + +pub(crate) trait PrincipalPropSearch: Sync + Send { + fn handle_principal_property_search( + &self, + access_token: &AccessToken, + request: PrincipalPropertySearch, + ) -> impl Future> + Send; +} + +impl PrincipalPropSearch for Server { + async fn handle_principal_property_search( + &self, + access_token: &AccessToken, + mut request: PrincipalPropertySearch, + ) -> crate::Result { + let mut search_for = None; + + for prop_search in request.property_search { + if matches!( + prop_search.property, + DavProperty::WebDav(WebDavProperty::DisplayName) + ) && !prop_search.match_.is_empty() + { + search_for = Some(prop_search.match_); + } + } + + let mut response = MultiStatus::new(Vec::with_capacity(16)); + if let Some(search_for) = search_for { + // Return all principals + let principals = self + .store() + .list_principals( + search_for.as_str().into(), + access_token.tenant_id(), + &[Type::Individual, Type::Group], + &[PrincipalField::Name], + 0, + 0, + ) + .await + .caused_by(trc::location!())?; + + let ids = RoaringBitmap::from_iter(principals.items.into_iter().map(|p| p.id())); + + if !ids.is_empty() { + if request.properties.is_empty() { + request + .properties + .push(DavProperty::WebDav(WebDavProperty::DisplayName)); + } + let request = PropFind::Prop(request.properties); + self.prepare_principal_propfind_response( + access_token, + Collection::Principal, + ids.into_iter(), + &request, + &mut response, + ) + .await?; + } + } + + Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())) + } +} diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index c9423db5..96d3e833 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -12,8 +12,11 @@ use dav_proto::{ parser::{DavParser, tokenizer::Tokenizer}, schema::{ Namespace, + property::WebDavProperty, request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report}, - response::{BaseCondition, ErrorResponse}, + response::{ + BaseCondition, ErrorResponse, PrincipalSearchProperty, PrincipalSearchPropertySet, + }, }, }; use directory::Permission; @@ -24,6 +27,7 @@ use crate::{ DavError, DavMethod, DavResource, common::{ DavQuery, + acl::DavAclHandler, lock::{LockRequest, LockRequestHandler}, propfind::PropFindRequestHandler, uri::DavUriResource, @@ -34,6 +38,7 @@ use crate::{ mkcol::FileMkColRequestHandler, propfind::HandleFilePropFindRequest, proppatch::FilePropPatchRequestHandler, update::FileUpdateRequestHandler, }, + principal::{matching::PrincipalMatching, propsearch::PrincipalPropSearch}, }; pub trait DavRequestHandler: Sync + Send { @@ -86,7 +91,6 @@ impl DavRequestDispatcher for Server { DavMethod::PROPPATCH => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { self.handle_file_proppatch_request( &access_token, @@ -95,11 +99,11 @@ impl DavRequestDispatcher for Server { ) .await } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::MKCOL => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { self.handle_file_mkcol_request( &access_token, @@ -112,20 +116,20 @@ impl DavRequestDispatcher for Server { ) .await } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::GET => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { self.handle_file_get_request(&access_token, headers, false) .await } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::HEAD => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { #[cfg(debug_assertions)] { @@ -144,6 +148,7 @@ impl DavRequestDispatcher for Server { .await } } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::DELETE => { // Include any fragments in the URI @@ -155,53 +160,52 @@ impl DavRequestDispatcher for Server { match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { self.handle_file_delete_request(&access_token, headers) .await } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), } } DavMethod::PUT | DavMethod::POST => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { self.handle_file_update_request(&access_token, headers, body, false) .await } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::PATCH => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { self.handle_file_update_request(&access_token, headers, body, true) .await } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::COPY => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { self.handle_file_copy_move_request(&access_token, headers, false) .await } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::MOVE => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { self.handle_file_copy_move_request(&access_token, headers, true) .await } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::LOCK => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), - DavResource::Principal => todo!(), DavResource::File => { self.handle_lock_request( &access_token, @@ -214,6 +218,7 @@ impl DavRequestDispatcher for Server { ) .await } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::UNLOCK => { self.handle_lock_request(&access_token, headers, LockRequest::Unlock) @@ -253,15 +258,40 @@ impl DavRequestDispatcher for Server { } } } - Report::Addressbook(addressbook_query) => todo!(), - Report::AddressbookMultiGet(multi_get) => todo!(), - Report::CalendarQuery(calendar_query) => todo!(), - Report::CalendarMultiGet(multi_get) => todo!(), - Report::FreeBusyQuery(free_busy_query) => todo!(), - Report::AclPrincipalPropSet(acl_principal_prop_set) => todo!(), - Report::PrincipalMatch(principal_match) => todo!(), - Report::PrincipalPropertySearch(principal_property_search) => todo!(), - Report::PrincipalSearchPropertySet => todo!(), + Report::AclPrincipalPropSet(report) => { + self.handle_acl_prop_set(&access_token, headers, report) + .await + } + Report::PrincipalMatch(report) => { + self.handle_principal_match(&access_token, headers, report) + .await + } + Report::PrincipalPropertySearch(report) => { + if resource == DavResource::Principal { + self.handle_principal_property_search(&access_token, report) + .await + } else { + Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) + } + } + Report::PrincipalSearchPropertySet => { + if resource == DavResource::Principal { + Ok(HttpResponse::new(StatusCode::OK).with_xml_body( + PrincipalSearchPropertySet::new(vec![PrincipalSearchProperty::new( + WebDavProperty::DisplayName, + "Account or Group name", + )]) + .to_string(), + )) + } else { + Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) + } + } + Report::Addressbook(report) => todo!(), + Report::AddressbookMultiGet(report) => todo!(), + Report::CalendarQuery(report) => todo!(), + Report::CalendarMultiGet(report) => todo!(), + Report::FreeBusyQuery(report) => todo!(), }, DavMethod::OPTIONS => unreachable!(), } diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index 1a66def0..f2e2419b 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -6,13 +6,20 @@ use calcard::icalendar::ICalendar; use jmap_proto::types::{acl::Acl, value::AclGrant}; +use store::{SERIALIZE_OBJ_14_V1, SerializedVersion}; use utils::map::vec_map::VecMap; +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] pub struct Calendar { pub preferences: VecMap, pub acls: Vec, } +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] pub struct CalendarPreferences { pub name: String, pub description: Option, @@ -21,10 +28,10 @@ pub struct CalendarPreferences { pub is_subscribed: bool, pub is_default: bool, pub is_visible: bool, - pub include_in_availability: IncludeInAvailability, + /*pub include_in_availability: IncludeInAvailability, pub default_alerts_with_time: VecMap, pub default_alerts_without_time: VecMap, - pub time_zone: Timezone, + pub time_zone: Timezone,*/ } pub struct CalendarEvent { @@ -95,3 +102,9 @@ impl From for Acl { } } } + +impl SerializedVersion for Calendar { + fn serialize_version() -> u8 { + SERIALIZE_OBJ_14_V1 + } +} diff --git a/crates/groupware/src/contact/mod.rs b/crates/groupware/src/contact/mod.rs index 9302753a..a4f4bee3 100644 --- a/crates/groupware/src/contact/mod.rs +++ b/crates/groupware/src/contact/mod.rs @@ -6,7 +6,12 @@ use calcard::vcard::VCard; use jmap_proto::types::{acl::Acl, value::AclGrant}; +use store::{SERIALIZE_OBJ_15_V1, SerializedVersion}; +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +#[rkyv(derive(Debug))] pub struct AddressBook { pub name: String, pub display_name: Option, @@ -57,3 +62,9 @@ impl From for Acl { } } } + +impl SerializedVersion for AddressBook { + fn serialize_version() -> u8 { + SERIALIZE_OBJ_15_V1 + } +} diff --git a/crates/groupware/src/file/hierarchy.rs b/crates/groupware/src/file/hierarchy.rs index f344c7f6..37506e5e 100644 --- a/crates/groupware/src/file/hierarchy.rs +++ b/crates/groupware/src/file/hierarchy.rs @@ -34,30 +34,12 @@ impl FileHierarchy for Server { .get(&account_id) .filter(|x| x.modseq == change_id) { - let c = println!( - "Hierarchy: {:?}", - files - .files - .iter() - .map(|f| f.name.clone()) - .collect::>() - ); Ok(files) } else { let mut files = build_file_hierarchy(self, account_id).await?; files.modseq = change_id; let files = Arc::new(files); self.inner.cache.files.insert(account_id, files.clone()); - - let c = println!( - "Hierarchy: {:?}", - files - .files - .iter() - .map(|f| f.name.clone()) - .collect::>() - ); - Ok(files) } } @@ -68,9 +50,6 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; - /*.format(|f| { - f.name = percent_encoding::utf8_percent_encode(&f.name, NON_ALPHANUMERIC).to_string(); - });*/ let mut files = Files { files: IdBimap::with_capacity(list.len()), size: std::mem::size_of::() as u64, diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 09932aca..24f0ba7c 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -84,7 +84,7 @@ pub const SERIALIZE_OBJ_12_V1: u8 = 11; pub const SERIALIZE_OBJ_13_V1: u8 = 12; pub const SERIALIZE_OBJ_14_V1: u8 = 13; pub const SERIALIZE_OBJ_15_V1: u8 = 14; -pub const SERIALIZE_OBJ_16_V1: u8 = 15; +//pub const SERIALIZE_OBJ_16_V1: u8 = 15; pub trait SerializedVersion { fn serialize_version() -> u8;