From 76f085ab7c58e8dcc51a75fb1ddfe2d047d22e12 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Sat, 29 Mar 2025 19:46:05 +0100 Subject: [PATCH] CardDAV GET + PUT --- Cargo.lock | 1 + crates/common/src/config/dav.rs | 4 + crates/common/src/lib.rs | 12 +- crates/common/src/sharing/document.rs | 7 +- crates/common/src/storage/index.rs | 285 ++++++++++++++-------- crates/dav/Cargo.toml | 1 + crates/dav/src/card/get.rs | 112 +++++++-- crates/dav/src/card/mkcol.rs | 108 ++++++++- crates/dav/src/card/propfind.rs | 24 +- crates/dav/src/card/proppatch.rs | 132 +++++++++- crates/dav/src/card/query.rs | 8 - crates/dav/src/card/update.rs | 333 +++++++++++++++++++++++++- crates/dav/src/common/acl.rs | 53 ++-- crates/dav/src/common/uri.rs | 4 +- crates/dav/src/file/acl.rs | 61 +---- crates/dav/src/file/copy_move.rs | 88 +++---- crates/dav/src/file/delete.rs | 36 +-- crates/dav/src/file/get.rs | 13 +- crates/dav/src/file/mkcol.rs | 4 +- crates/dav/src/file/mod.rs | 6 +- crates/dav/src/file/propfind.rs | 2 +- crates/dav/src/file/proppatch.rs | 23 +- crates/dav/src/file/update.rs | 47 ++-- crates/email/src/mailbox/index.rs | 39 ++- crates/email/src/sieve/activate.rs | 3 +- crates/email/src/sieve/index.rs | 16 +- crates/email/src/sieve/ingest.rs | 2 +- crates/email/src/submission/index.rs | 36 +-- crates/groupware/src/calendar/mod.rs | 5 +- crates/groupware/src/contact/index.rs | 67 ++++-- crates/groupware/src/contact/mod.rs | 19 +- crates/groupware/src/file/index.rs | 12 +- crates/groupware/src/hierarchy.rs | 76 +++--- crates/groupware/src/lib.rs | 61 +++++ crates/store/src/lib.rs | 2 +- crates/utils/src/bimap.rs | 2 +- 36 files changed, 1203 insertions(+), 501 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8aa8a36d..40986223 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1673,6 +1673,7 @@ checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" name = "dav" version = "0.11.7" dependencies = [ + "calcard", "common", "dav-proto", "directory", diff --git a/crates/common/src/config/dav.rs b/crates/common/src/config/dav.rs index 9518fcf3..f69a338c 100644 --- a/crates/common/src/config/dav.rs +++ b/crates/common/src/config/dav.rs @@ -15,6 +15,7 @@ pub struct DavConfig { pub max_locks_per_user: usize, pub max_changes: usize, pub max_match_results: usize, + pub max_vcard_size: usize, } impl DavConfig { @@ -37,6 +38,9 @@ impl DavConfig { max_match_results: config .property("dav.limits.max-match-results") .unwrap_or(1000), + max_vcard_size: config + .property("dav.limits.size.vcard") + .unwrap_or(512 * 1024), } } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 5d1df925..44f4e04b 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -258,7 +258,7 @@ pub struct DavResourceId { #[derive(Debug, Default)] pub struct DavResources { - pub files: IdBimap, + pub paths: IdBimap, pub size: u64, pub modseq: Option, } @@ -508,7 +508,7 @@ pub fn ip_to_bytes_prefix(prefix: u8, ip: &IpAddr) -> Vec { impl DavResources { pub fn subtree(&self, search_path: &str) -> impl Iterator { let prefix = format!("{search_path}/"); - self.files + self.paths .iter() .filter(move |item| item.name.starts_with(&prefix) || item.name == search_path) } @@ -519,7 +519,7 @@ impl DavResources { depth: usize, ) -> impl Iterator { let prefix = format!("{search_path}/"); - self.files.iter().filter(move |item| { + self.paths.iter().filter(move |item| { item.name .strip_prefix(&prefix) .is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth) @@ -528,14 +528,14 @@ impl DavResources { } pub fn tree_with_depth(&self, depth: usize) -> impl Iterator { - self.files.iter().filter(move |item| { + self.paths.iter().filter(move |item| { item.name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth }) } pub fn is_ancestor_of(&self, ancestor: u32, descendant: u32) -> bool { - let ancestor = &self.files.by_id(ancestor).unwrap().name; - let descendant = &self.files.by_id(descendant).unwrap().name; + let ancestor = &self.paths.by_id(ancestor).unwrap().name; + let descendant = &self.paths.by_id(descendant).unwrap().name; let prefix = format!("{ancestor}/"); descendant.starts_with(&prefix) || descendant == ancestor diff --git a/crates/common/src/sharing/document.rs b/crates/common/src/sharing/document.rs index 46495ae4..68f5e5d5 100644 --- a/crates/common/src/sharing/document.rs +++ b/crates/common/src/sharing/document.rs @@ -71,6 +71,7 @@ impl Server { if shared_containers.is_empty() { return Ok(shared_containers); } + let todo = "maybe cache?"; let mut shared_items = RoaringBitmap::new(); for document_id in shared_containers { if let Some(documents_in_folder) = self @@ -147,9 +148,9 @@ impl Server { ) -> trc::Result { let to_collection = to_collection.into(); let check_acls = check_acls.into(); - for &grant_account_id in [access_token.primary_id] - .iter() - .chain(access_token.member_of.clone().iter()) + for grant_account_id in [access_token.primary_id] + .into_iter() + .chain(access_token.member_of.iter().copied()) { match self .core diff --git a/crates/common/src/storage/index.rs b/crates/common/src/storage/index.rs index bea85fb3..58d0923f 100644 --- a/crates/common/src/storage/index.rs +++ b/crates/common/src/storage/index.rs @@ -6,6 +6,11 @@ use ahash::AHashSet; use jmap_proto::types::{property::Property, value::AclGrant}; +use rkyv::{ + option::ArchivedOption, + primitive::{ArchivedU32, ArchivedU64}, + string::ArchivedString, +}; use std::{borrow::Cow, fmt::Debug}; use store::{ Serialize, SerializeInfallible, SerializedVersion, @@ -20,21 +25,13 @@ use crate::auth::AsTenantId; #[derive(Debug, Clone, PartialEq, Eq)] pub enum IndexValue<'x> { - Text { + Index { field: u8, - value: Cow<'x, str>, + value: IndexItem<'x>, }, - U32 { + IndexList { field: u8, - value: Option, - }, - U64 { - field: u8, - value: Option, - }, - U32List { - field: u8, - value: Cow<'x, [u32]>, + value: Vec>, }, Tag { field: u8, @@ -51,6 +48,156 @@ pub enum IndexValue<'x> { }, } +#[derive(Debug, Clone)] +pub enum IndexItem<'x> { + Vec(Vec), + Slice(&'x [u8]), + ShortInt([u8; std::mem::size_of::()]), + LongInt([u8; std::mem::size_of::()]), + None, +} + +impl IndexItem<'_> { + pub fn as_slice(&self) -> &[u8] { + match self { + IndexItem::Vec(v) => v, + IndexItem::Slice(s) => s, + IndexItem::ShortInt(s) => s, + IndexItem::LongInt(s) => s, + IndexItem::None => &[], + } + } + + pub fn into_owned(self) -> Vec { + match self { + IndexItem::Vec(v) => v, + IndexItem::Slice(s) => s.to_vec(), + IndexItem::ShortInt(s) => s.to_vec(), + IndexItem::LongInt(s) => s.to_vec(), + IndexItem::None => vec![], + } + } + + pub fn is_empty(&self) -> bool { + match self { + IndexItem::Vec(v) => v.is_empty(), + IndexItem::Slice(s) => s.is_empty(), + IndexItem::None => true, + _ => false, + } + } +} + +impl PartialEq for IndexItem<'_> { + fn eq(&self, other: &Self) -> bool { + self.as_slice() == other.as_slice() + } +} + +impl Eq for IndexItem<'_> {} + +impl std::hash::Hash for IndexItem<'_> { + fn hash(&self, state: &mut H) { + match self { + IndexItem::Vec(v) => v.as_slice().hash(state), + IndexItem::Slice(s) => s.hash(state), + IndexItem::ShortInt(s) => s.as_slice().hash(state), + IndexItem::LongInt(s) => s.as_slice().hash(state), + IndexItem::None => 0.hash(state), + } + } +} + +impl From for IndexItem<'_> { + fn from(value: u32) -> Self { + IndexItem::ShortInt(value.to_be_bytes()) + } +} + +impl From<&u32> for IndexItem<'_> { + fn from(value: &u32) -> Self { + IndexItem::ShortInt(value.to_be_bytes()) + } +} + +impl From for IndexItem<'_> { + fn from(value: u64) -> Self { + IndexItem::LongInt(value.to_be_bytes()) + } +} + +impl<'x> From<&'x [u8]> for IndexItem<'x> { + fn from(value: &'x [u8]) -> Self { + IndexItem::Slice(value) + } +} + +impl From> for IndexItem<'_> { + fn from(value: Vec) -> Self { + IndexItem::Vec(value) + } +} + +impl<'x> From<&'x str> for IndexItem<'x> { + fn from(value: &'x str) -> Self { + IndexItem::Slice(value.as_bytes()) + } +} + +impl<'x> From<&'x String> for IndexItem<'x> { + fn from(value: &'x String) -> Self { + IndexItem::Slice(value.as_bytes()) + } +} + +impl From for IndexItem<'_> { + fn from(value: String) -> Self { + IndexItem::Vec(value.into_bytes()) + } +} + +impl<'x> From<&'x ArchivedString> for IndexItem<'x> { + fn from(value: &'x ArchivedString) -> Self { + IndexItem::Slice(value.as_bytes()) + } +} + +impl From for IndexItem<'_> { + fn from(value: ArchivedU32) -> Self { + IndexItem::ShortInt(value.to_native().to_be_bytes()) + } +} + +impl From<&ArchivedU32> for IndexItem<'_> { + fn from(value: &ArchivedU32) -> Self { + IndexItem::ShortInt(value.to_native().to_be_bytes()) + } +} + +impl From for IndexItem<'_> { + fn from(value: ArchivedU64) -> Self { + IndexItem::LongInt(value.to_native().to_be_bytes()) + } +} + +impl<'x, T: Into>> From> for IndexItem<'x> { + fn from(value: Option) -> Self { + match value { + Some(v) => v.into(), + None => IndexItem::None, + } + } +} + +impl<'x, T: Into>> From> for IndexItem<'x> { + fn from(value: ArchivedOption) -> Self { + match value { + ArchivedOption::Some(v) => v.into(), + ArchivedOption::None => IndexItem::None, + } + } +} + pub trait IndexableObject: Sync + Send { fn index_values(&self) -> impl Iterator>; } @@ -164,38 +311,20 @@ impl IntoOperations fn build_index(batch: &mut BatchBuilder, item: IndexValue<'_>, tenant_id: Option, set: bool) { match item { - IndexValue::Text { field, value } => { + IndexValue::Index { field, value } => { if !value.is_empty() { batch.ops.push(Operation::Index { field, - key: value.into_owned().into_bytes(), + key: value.into_owned(), set, }); } } - IndexValue::U32 { field, value } => { - if let Some(value) = value { + IndexValue::IndexList { field, value } => { + for key in value { batch.ops.push(Operation::Index { field, - key: value.serialize(), - set, - }); - } - } - IndexValue::U64 { field, value } => { - if let Some(value) = value { - batch.ops.push(Operation::Index { - field, - key: value.serialize(), - set, - }); - } - } - IndexValue::U32List { field, value } => { - for item in value.as_ref() { - batch.ops.push(Operation::Index { - field, - key: (*item).serialize(), + key: key.into_owned(), set, }); } @@ -249,18 +378,18 @@ fn merge_index( ) -> trc::Result<()> { match (current, change) { ( - IndexValue::Text { + IndexValue::Index { field, value: old_value, }, - IndexValue::Text { + IndexValue::Index { value: new_value, .. }, ) => { if !old_value.is_empty() { batch.ops.push(Operation::Index { field, - key: old_value.into_owned().into_bytes(), + key: old_value.into_owned(), set: false, }); } @@ -268,89 +397,39 @@ fn merge_index( if !new_value.is_empty() { batch.ops.push(Operation::Index { field, - key: new_value.into_owned().into_bytes(), + key: new_value.into_owned(), set: true, }); } } ( - IndexValue::U32 { + IndexValue::IndexList { field, value: old_value, }, - IndexValue::U32 { + IndexValue::IndexList { value: new_value, .. }, ) => { - if let Some(value) = old_value { - batch.ops.push(Operation::Index { - field, - key: value.serialize(), - set: false, - }); - } - if let Some(value) = new_value { - batch.ops.push(Operation::Index { - field, - key: value.serialize(), - set: true, - }); - } - } - ( - IndexValue::U64 { - field, - value: old_value, - }, - IndexValue::U64 { - value: new_value, .. - }, - ) => { - if let Some(value) = old_value { - batch.ops.push(Operation::Index { - field, - key: value.serialize(), - set: false, - }); - } - if let Some(value) = new_value { - batch.ops.push(Operation::Index { - field, - key: value.serialize(), - set: true, - }); - } - } - ( - IndexValue::U32List { - field, - value: old_value, - }, - IndexValue::U32List { - value: new_value, .. - }, - ) => { - let mut add_values = AHashSet::new(); - let mut remove_values = AHashSet::new(); + let mut remove_values = AHashSet::from_iter(old_value); - for current_value in old_value.as_ref() { - remove_values.insert(current_value); - } - for value in new_value.as_ref() { + for value in new_value { if !remove_values.remove(&value) { - add_values.insert(value); - } - } - - for (values, set) in [(add_values, true), (remove_values, false)] { - for value in values { batch.ops.push(Operation::Index { field, - key: value.serialize(), - set, + key: value.into_owned(), + set: true, }); } } + + for value in remove_values { + batch.ops.push(Operation::Index { + field, + key: value.into_owned(), + set: false, + }); + } } ( IndexValue::Tag { diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 02c467bd..3e586aa3 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -14,6 +14,7 @@ directory = { path = "../directory" } http_proto = { path = "../http-proto" } jmap_proto = { path = "../jmap-proto" } trc = { path = "../trc" } +calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] } hashify = { version = "0.2" } hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } percent-encoding = "2.3.1" diff --git a/crates/dav/src/card/get.rs b/crates/dav/src/card/get.rs index 6ca0caf5..30d39f57 100644 --- a/crates/dav/src/card/get.rs +++ b/crates/dav/src/card/get.rs @@ -5,10 +5,22 @@ */ use common::{Server, auth::AccessToken}; -use dav_proto::{RequestHeaders, schema::request::MultiGet}; +use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime}; +use groupware::{contact::ContactCard, hierarchy::DavHierarchy}; use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; +use store::write::{AlignedBytes, Archive}; +use trc::AddContext; -use crate::common::uri::DavUriResource; +use crate::{ + DavError, DavMethod, + common::{ + ETag, + lock::{LockRequestHandler, ResourceState}, + uri::DavUriResource, + }, +}; pub(crate) trait CardGetRequestHandler: Sync + Send { fn handle_card_get_request( @@ -17,13 +29,6 @@ pub(crate) trait CardGetRequestHandler: Sync + Send { headers: RequestHeaders<'_>, is_head: bool, ) -> impl Future> + Send; - - fn handle_card_multiget_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: MultiGet, - ) -> impl Future> + Send; } impl CardGetRequestHandler for Server { @@ -38,22 +43,83 @@ impl CardGetRequestHandler for Server { .validate_uri(access_token, headers.uri) .await? .into_owned_uri()?; + let account_id = resource_.account_id; + let resources = self + .fetch_dav_resources(account_id, Collection::AddressBook) + .await + .caused_by(trc::location!())?; + let resource = resources + .paths + .by_name( + resource_ + .resource + .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, + ) + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + if resource.is_container { + return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); + } - todo!() - } + // Validate ACL + if !access_token.is_member(account_id) + && !self + .has_access_to_document( + access_token, + account_id, + Collection::AddressBook, + resource.parent_id.unwrap(), + Acl::ReadItems, + ) + .await + .caused_by(trc::location!())? + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } - async fn handle_card_multiget_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: MultiGet, - ) -> crate::Result { - // Validate URI - let resource_ = self - .validate_uri(access_token, headers.uri) - .await? - .into_owned_uri()?; + // Fetch card + let card_ = self + .get_property::>( + account_id, + Collection::ContactCard, + resource.document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let card = card_ + .unarchive::() + .caused_by(trc::location!())?; - todo!() + // Validate headers + let etag = card_.etag(); + self.validate_headers( + access_token, + &headers, + vec![ResourceState { + account_id, + collection: Collection::ContactCard, + document_id: resource.document_id.into(), + etag: etag.clone().into(), + path: resource_.resource.unwrap(), + ..Default::default() + }], + Default::default(), + DavMethod::GET, + ) + .await?; + + let response = HttpResponse::new(StatusCode::OK) + .with_content_type("text/vcard; charset=utf-8") + .with_etag(etag) + .with_last_modified(Rfc1123DateTime::new(i64::from(card.modified)).to_string()); + + let vcard = card.card.to_string(); + + if !is_head { + Ok(response.with_binary_body(vcard)) + } else { + Ok(response.with_content_length(vcard.len())) + } } } diff --git a/crates/dav/src/card/mkcol.rs b/crates/dav/src/card/mkcol.rs index 3c6a8ee7..04b2b2e5 100644 --- a/crates/dav/src/card/mkcol.rs +++ b/crates/dav/src/card/mkcol.rs @@ -4,12 +4,27 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; -use dav_proto::{RequestHeaders, schema::request::MkCol}; +use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use dav_proto::{ + RequestHeaders, Return, + schema::{Namespace, request::MkCol, response::MkColResponse}, +}; +use groupware::{contact::AddressBook, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; +use jmap_proto::types::{collection::Collection, type_state::DataType}; +use store::write::{BatchBuilder, log::LogInsert, now}; +use trc::AddContext; -use crate::{DavError, common::uri::DavUriResource}; +use crate::{ + DavError, DavMethod, + common::{ + lock::{LockRequestHandler, ResourceState}, + uri::DavUriResource, + }, +}; + +use super::proppatch::CardPropPatchRequestHandler; pub(crate) trait CardMkColRequestHandler: Sync + Send { fn handle_card_mkcol_request( @@ -32,12 +47,91 @@ impl CardMkColRequestHandler for Server { .validate_uri(access_token, headers.uri) .await? .into_owned_uri()?; - if resource.resource.is_none_or(|r| r.contains('/')) - || !access_token.is_member(resource.account_id) - { + let account_id = resource.account_id; + let name = resource + .resource + .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; + if name.contains('/') || !access_token.is_member(account_id) { return Err(DavError::Code(StatusCode::FORBIDDEN)); + } else if self + .fetch_dav_resources(account_id, Collection::AddressBook) + .await + .caused_by(trc::location!())? + .paths + .by_name(name) + .is_some() + { + return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } - todo!() + // Validate headers + self.validate_headers( + access_token, + &headers, + vec![ResourceState { + account_id, + collection: resource.collection, + document_id: Some(u32::MAX), + path: name, + ..Default::default() + }], + Default::default(), + DavMethod::MKCOL, + ) + .await?; + + // Build file container + let change_id = self.generate_snowflake_id().caused_by(trc::location!())?; + let now = now(); + let mut book = AddressBook { + name: name.to_string(), + created: now as i64, + modified: now as i64, + ..Default::default() + }; + + // Apply MKCOL properties + let mut return_prop_stat = None; + if let Some(mkcol) = request { + let mut prop_stat = Vec::new(); + if !self.apply_addressbook_properties(&mut book, false, mkcol.props, &mut prop_stat) { + return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body( + MkColResponse::new(prop_stat) + .with_namespace(Namespace::CardDav) + .to_string(), + )); + } + if headers.ret != Return::Minimal { + return_prop_stat = Some(prop_stat); + } + } + + // Prepare write batch + let mut batch = BatchBuilder::new(); + batch + .with_change_id(change_id) + .with_account_id(account_id) + .with_collection(Collection::AddressBook) + .create_document() + .log(LogInsert()) + .custom(ObjectIndexBuilder::<(), _>::new().with_changes(book)) + .caused_by(trc::location!())?; + self.store() + .write(batch) + .await + .caused_by(trc::location!())?; + + // Broadcast state change + self.broadcast_single_state_change(account_id, change_id, DataType::AddressBook) + .await; + if let Some(prop_stat) = return_prop_stat { + Ok(HttpResponse::new(StatusCode::CREATED).with_xml_body( + MkColResponse::new(prop_stat) + .with_namespace(Namespace::CardDav) + .to_string(), + )) + } else { + Ok(HttpResponse::new(StatusCode::CREATED)) + } } } diff --git a/crates/dav/src/card/propfind.rs b/crates/dav/src/card/propfind.rs index 012e6893..a5e61523 100644 --- a/crates/dav/src/card/propfind.rs +++ b/crates/dav/src/card/propfind.rs @@ -5,7 +5,7 @@ */ use common::{Server, auth::AccessToken}; -use dav_proto::{RequestHeaders, schema::request::PropFind}; +use dav_proto::{RequestHeaders, schema::request::MultiGet}; use http_proto::HttpResponse; use crate::common::{DavQuery, uri::DavUriResource}; @@ -16,6 +16,13 @@ pub(crate) trait CardPropFindRequestHandler: Sync + Send { access_token: &AccessToken, query: DavQuery<'_>, ) -> impl Future> + Send; + + fn handle_card_multiget_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: MultiGet, + ) -> impl Future> + Send; } impl CardPropFindRequestHandler for Server { @@ -28,4 +35,19 @@ impl CardPropFindRequestHandler for Server { todo!() } + + async fn handle_card_multiget_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: MultiGet, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + + todo!() + } } diff --git a/crates/dav/src/card/proppatch.rs b/crates/dav/src/card/proppatch.rs index 60ced889..028bf536 100644 --- a/crates/dav/src/card/proppatch.rs +++ b/crates/dav/src/card/proppatch.rs @@ -5,8 +5,17 @@ */ use common::{Server, auth::AccessToken}; -use dav_proto::{RequestHeaders, schema::request::PropertyUpdate}; +use dav_proto::{ + RequestHeaders, + schema::{ + property::{CardDavProperty, DavProperty, DavValue, ResourceType, WebDavProperty}, + request::{DavPropertyValue, PropertyUpdate}, + response::{BaseCondition, PropStat}, + }, +}; +use groupware::contact::AddressBook; use http_proto::HttpResponse; +use hyper::StatusCode; use crate::common::uri::DavUriResource; @@ -17,6 +26,14 @@ pub(crate) trait CardPropPatchRequestHandler: Sync + Send { headers: RequestHeaders<'_>, request: PropertyUpdate, ) -> impl Future> + Send; + + fn apply_addressbook_properties( + &self, + address_book: &mut AddressBook, + is_update: bool, + properties: Vec, + items: &mut Vec, + ) -> bool; } impl CardPropPatchRequestHandler for Server { @@ -34,4 +51,117 @@ impl CardPropPatchRequestHandler for Server { todo!() } + + fn apply_addressbook_properties( + &self, + address_book: &mut AddressBook, + is_update: bool, + properties: Vec, + items: &mut Vec, + ) -> bool { + let mut has_errors = false; + + for property in properties { + match (property.property, property.value) { + (DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => { + if name.len() <= self.core.dav.live_property_size { + address_book.display_name = Some(name); + items.push( + PropStat::new(DavProperty::WebDav(WebDavProperty::DisplayName)) + .with_status(StatusCode::OK), + ); + } else { + items.push( + PropStat::new(DavProperty::WebDav(WebDavProperty::DisplayName)) + .with_status(StatusCode::INSUFFICIENT_STORAGE) + .with_response_description("Display name too long"), + ); + has_errors = true; + } + } + ( + DavProperty::CardDav(CardDavProperty::AddressbookDescription), + DavValue::String(name), + ) => { + if name.len() <= self.core.dav.live_property_size { + address_book.description = Some(name); + items.push( + PropStat::new(DavProperty::CardDav( + CardDavProperty::AddressbookDescription, + )) + .with_status(StatusCode::OK), + ); + } else { + items.push( + PropStat::new(DavProperty::CardDav( + CardDavProperty::AddressbookDescription, + )) + .with_status(StatusCode::INSUFFICIENT_STORAGE) + .with_response_description("Addressbook description too long"), + ); + has_errors = true; + } + } + (DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => { + address_book.created = dt; + } + ( + DavProperty::WebDav(WebDavProperty::ResourceType), + DavValue::ResourceTypes(types), + ) => { + if types.0.iter().all(|rt| { + matches!(rt, ResourceType::Collection | ResourceType::AddressBook) + }) { + items.push( + PropStat::new(DavProperty::WebDav(WebDavProperty::ResourceType)) + .with_status(StatusCode::FORBIDDEN) + .with_error(BaseCondition::ValidResourceType), + ); + has_errors = true; + } else { + items.push( + PropStat::new(DavProperty::WebDav(WebDavProperty::ResourceType)) + .with_status(StatusCode::OK), + ); + } + } + (DavProperty::DeadProperty(dead), DavValue::DeadProperty(values)) + if self.core.dav.dead_property_size.is_some() => + { + if is_update { + address_book.dead_properties.remove_element(&dead); + } + + if address_book.dead_properties.size() + values.size() + dead.size() + < self.core.dav.dead_property_size.unwrap() + { + address_book + .dead_properties + .add_element(dead.clone(), values.0); + items.push( + PropStat::new(DavProperty::DeadProperty(dead)) + .with_status(StatusCode::OK), + ); + } else { + items.push( + PropStat::new(DavProperty::DeadProperty(dead)) + .with_status(StatusCode::INSUFFICIENT_STORAGE) + .with_response_description("Dead property is too large."), + ); + has_errors = true; + } + } + (property, _) => { + items.push( + PropStat::new(property) + .with_status(StatusCode::CONFLICT) + .with_response_description("Property cannot be modified"), + ); + has_errors = true; + } + } + } + + !has_errors + } } diff --git a/crates/dav/src/card/query.rs b/crates/dav/src/card/query.rs index e24f8cd0..651e4192 100644 --- a/crates/dav/src/card/query.rs +++ b/crates/dav/src/card/query.rs @@ -8,8 +8,6 @@ use common::{Server, auth::AccessToken}; use dav_proto::{RequestHeaders, schema::request::AddressbookQuery}; use http_proto::HttpResponse; -use crate::common::uri::DavUriResource; - pub(crate) trait CardQueryRequestHandler: Sync + Send { fn handle_card_query_request( &self, @@ -26,12 +24,6 @@ impl CardQueryRequestHandler for Server { headers: RequestHeaders<'_>, request: AddressbookQuery, ) -> crate::Result { - // Validate URI - let resource_ = self - .validate_uri(access_token, headers.uri) - .await? - .into_owned_uri()?; - todo!() } } diff --git a/crates/dav/src/card/update.rs b/crates/dav/src/card/update.rs index b8e53ab1..e88eab32 100644 --- a/crates/dav/src/card/update.rs +++ b/crates/dav/src/card/update.rs @@ -4,11 +4,36 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; -use dav_proto::RequestHeaders; +use calcard::{Entry, Parser}; +use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use dav_proto::{ + RequestHeaders, Return, + schema::{property::Rfc1123DateTime, response::CardCondition}, +}; +use groupware::{DavName, IDX_CARD_UID, contact::ContactCard, hierarchy::DavHierarchy}; use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::{ + acl::Acl, collection::Collection, property::Property, state::StateChange, type_state::DataType, +}; +use store::{ + query::Filter, + write::{ + AlignedBytes, Archive, BatchBuilder, + log::{Changes, LogInsert}, + now, + }, +}; +use trc::AddContext; -use crate::common::uri::DavUriResource; +use crate::{ + DavError, DavErrorCondition, DavMethod, + common::{ + ETag, ExtractETag, + lock::{LockRequestHandler, ResourceState}, + uri::DavUriResource, + }, +}; pub(crate) trait CardUpdateRequestHandler: Sync + Send { fn handle_card_update_request( @@ -26,14 +51,310 @@ impl CardUpdateRequestHandler for Server { access_token: &AccessToken, headers: RequestHeaders<'_>, bytes: Vec, - is_patch: bool, + _is_patch: bool, ) -> crate::Result { // Validate URI - let resource_ = self + let resource = self .validate_uri(access_token, headers.uri) .await? .into_owned_uri()?; + let account_id = resource.account_id; + let resources = self + .fetch_dav_resources(account_id, Collection::AddressBook) + .await + .caused_by(trc::location!())?; + let resource_name = resource + .resource + .ok_or(DavError::Code(StatusCode::CONFLICT))?; - todo!() + if bytes.len() > self.core.dav.max_vcard_size { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + CardCondition::MaxResourceSize(self.core.dav.max_vcard_size as u32), + ))); + } + let vcard_raw = std::str::from_utf8(&bytes).map_err(|_| { + DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + CardCondition::SupportedAddressData, + )) + })?; + + let vcard = match Parser::new(vcard_raw).entry() { + Entry::VCard(vcard) => vcard, + _ => { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + CardCondition::SupportedAddressData, + ))); + } + }; + + if let Some(resource) = resources.paths.by_name(resource_name) { + if resource.is_container { + return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); + } + + // Validate ACL + let parent_id = resource.parent_id.unwrap(); + let document_id = resource.document_id; + if !access_token.is_member(account_id) + && !self + .has_access_to_document( + access_token, + account_id, + Collection::AddressBook, + parent_id, + Acl::ModifyItems, + ) + .await + .caused_by(trc::location!())? + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + + // Update + let card_ = self + .get_property::>( + account_id, + Collection::FileNode, + document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let card = card_ + .to_unarchived::() + .caused_by(trc::location!())?; + + // Validate headers + match self + .validate_headers( + access_token, + &headers, + vec![ResourceState { + account_id, + collection: Collection::ContactCard, + document_id: Some(document_id), + etag: card.etag().into(), + path: resource_name, + ..Default::default() + }], + Default::default(), + DavMethod::PUT, + ) + .await + { + Ok(_) => {} + Err(DavError::Code(StatusCode::PRECONDITION_FAILED)) + if headers.ret == Return::Representation => + { + return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED) + .with_content_type("text/vcard; charset=utf-8") + .with_etag(card.etag()) + .with_last_modified( + Rfc1123DateTime::new(i64::from(card.inner.modified)).to_string(), + ) + .with_header("Preference-Applied", "return=representation") + .with_binary_body(card.inner.card.to_string())); + } + Err(e) => return Err(e), + } + + // Validate quota + let extra_bytes = + (bytes.len() as u64).saturating_sub(u32::from(card.inner.size) as u64); + if extra_bytes > 0 { + self.has_available_quota( + &self.get_resource_token(access_token, account_id).await?, + extra_bytes, + ) + .await?; + } + + // Validate UID + match (card.inner.card.uid(), vcard.uid()) { + (Some(old_uid), Some(new_uid)) if old_uid == new_uid => {} + (None, None) | (None, Some(_)) => {} + _ => { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + CardCondition::NoUidConflict( + headers.format_to_base_uri(resource_name).into(), + ), + ))); + } + } + + // Build node + let change_id = self.generate_snowflake_id().caused_by(trc::location!())?; + let mut new_card = card + .deserialize::() + .caused_by(trc::location!())?; + new_card.size = bytes.len() as u32; + new_card.modified = now() as i64; + new_card.card = vcard; + + // Prepare write batch + let mut batch = BatchBuilder::new(); + batch + .with_change_id(change_id) + .with_account_id(account_id) + .with_collection(Collection::AddressBook) + .log(Changes::child_update( + card.inner.names.iter().map(|n| n.parent_id.to_native()), + )) + .with_collection(Collection::ContactCard) + .update_document(document_id) + .log(Changes::update([document_id])) + .custom( + ObjectIndexBuilder::new() + .with_current(card) + .with_changes(new_card) + .with_tenant_id(access_token), + ) + .caused_by(trc::location!())?; + let etag = batch.etag(); + self.store() + .write(batch) + .await + .caused_by(trc::location!())?; + + // Broadcast state change + self.broadcast_state_change( + StateChange::new(account_id) + .with_change(DataType::ContactCard, change_id) + .with_change(DataType::AddressBook, change_id), + ) + .await; + + Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag)) + } else if let Some((parent, name)) = resource_name + .rsplit_once('/') + .and_then(|(parent, name)| resources.paths.by_name(parent).map(|parent| (parent, name))) + { + if !parent.is_container { + return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); + } + + // Validate ACL + if !access_token.is_member(account_id) + && !self + .has_access_to_document( + access_token, + account_id, + Collection::AddressBook, + parent.document_id, + Acl::AddItems, + ) + .await + .caused_by(trc::location!())? + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + + // Validate headers + self.validate_headers( + access_token, + &headers, + vec![ResourceState { + account_id, + collection: resource.collection, + document_id: Some(u32::MAX), + path: resource_name, + ..Default::default() + }], + Default::default(), + DavMethod::PUT, + ) + .await?; + + // Validate quota + if !bytes.is_empty() { + self.has_available_quota( + &self.get_resource_token(access_token, account_id).await?, + bytes.len() as u64, + ) + .await?; + } + + // Validate UID + if let Some(uid) = vcard.uid() { + let hits = self + .store() + .filter( + account_id, + Collection::ContactCard, + vec![Filter::eq(IDX_CARD_UID, uid.as_bytes().to_vec())], + ) + .await + .caused_by(trc::location!())?; + if !hits.results.is_empty() { + for path in resources.paths.iter() { + if !path.is_container + && hits.results.contains(path.document_id) + && path.parent_id.unwrap() == parent.document_id + { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + CardCondition::NoUidConflict( + headers.format_to_base_uri(&path.name).into(), + ), + ))); + } + } + } + } + + // Build node + let change_id = self.generate_snowflake_id().caused_by(trc::location!())?; + let now = now(); + let card = ContactCard { + names: vec![DavName { + name: name.to_string(), + parent_id: parent.document_id, + }], + card: vcard, + created: now as i64, + modified: now as i64, + size: bytes.len() as u32, + ..Default::default() + }; + + // Prepare write batch + let mut batch = BatchBuilder::new(); + batch + .with_change_id(change_id) + .with_account_id(account_id) + .with_collection(Collection::AddressBook) + .log(Changes::child_update([parent.document_id])) + .with_collection(Collection::ContactCard) + .create_document() + .log(LogInsert()) + .custom( + ObjectIndexBuilder::<(), _>::new() + .with_changes(card) + .with_tenant_id(access_token), + ) + .caused_by(trc::location!())?; + let etag = batch.etag(); + self.store() + .write(batch) + .await + .caused_by(trc::location!())?; + + // Broadcast state change + self.broadcast_state_change( + StateChange::new(account_id) + .with_change(DataType::ContactCard, change_id) + .with_change(DataType::AddressBook, change_id), + ) + .await; + + Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag)) + } else { + Err(DavError::Code(StatusCode::CONFLICT))? + } } } diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index 494a7c7e..09f27cc2 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -62,15 +62,13 @@ pub(crate) trait DavAclHandler: Sync + Send { ) -> impl Future> + Send; #[allow(clippy::too_many_arguments)] - fn validate_child_or_parent_acl( + fn validate_acl( &self, access_token: &AccessToken, account_id: u32, collection: Collection, document_id: u32, - parent_id: Option, - child_acl: impl Into> + Send, - parent_acl: impl Into> + Send, + acl: impl Into> + Send, ) -> impl Future> + Send; fn resolve_ace( @@ -103,18 +101,6 @@ impl DavAclHandler for Server { 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, @@ -147,8 +133,16 @@ impl DavAclHandler for Server { } _ => unreachable!(), }; - let account_ids = RoaringBitmap::from_iter(acls.iter().map(|a| u32::from(a.account_id))); + // Validate ACLs + if !access_token.is_member(uri.account_id) + && !acls.effective_acl(access_token).contains(Acl::Read) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + + // Validate + 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() { @@ -338,38 +332,19 @@ impl DavAclHandler for Server { } } - async fn validate_child_or_parent_acl( + async fn validate_acl( &self, access_token: &AccessToken, account_id: u32, collection: Collection, document_id: u32, - parent_id: Option, - child_acl: impl Into> + Send, - parent_acl: impl Into> + Send, + acl: impl Into> + Send, ) -> crate::Result<()> { if access_token.is_member(account_id) || self - .has_access_to_document( - access_token, - account_id, - collection, - document_id, - child_acl, - ) + .has_access_to_document(access_token, account_id, collection, document_id, acl) .await .caused_by(trc::location!())? - || (parent_id.is_some() - && self - .has_access_to_document( - access_token, - account_id, - collection, - parent_id.unwrap(), - parent_acl, - ) - .await - .caused_by(trc::location!())?) { Ok(()) } else { diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index 9ba977eb..d996f94f 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -105,10 +105,10 @@ impl DavUriResource for Server { async fn map_uri_resource(&self, uri: OwnedUri<'_>) -> trc::Result> { if let Some(resource) = uri.resource { if let Some(resource) = self - .fetch_dav_hierarchy(uri.account_id, uri.collection) + .fetch_dav_resources(uri.account_id, uri.collection) .await .caused_by(trc::location!())? - .files + .paths .by_name(resource) { Ok(Some(DocumentUri { diff --git a/crates/dav/src/file/acl.rs b/crates/dav/src/file/acl.rs index cd44f7c8..020b903c 100644 --- a/crates/dav/src/file/acl.rs +++ b/crates/dav/src/file/acl.rs @@ -6,10 +6,7 @@ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::RequestHeaders; -use groupware::{ - file::{ArchivedFileNode, FileNode}, - hierarchy::DavHierarchy, -}; +use groupware::{file::FileNode, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; @@ -29,15 +26,6 @@ pub(crate) trait FileAclRequestHandler: Sync + Send { headers: RequestHeaders<'_>, request: dav_proto::schema::request::Acl, ) -> impl Future> + Send; - - fn validate_file_acl( - &self, - access_token: &AccessToken, - account_id: u32, - node: &ArchivedFileNode, - acl_child: Acl, - acl_parent: Acl, - ) -> impl Future> + Send; } impl FileAclRequestHandler for Server { @@ -54,7 +42,7 @@ impl FileAclRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let files = self - .fetch_dav_hierarchy(account_id, Collection::FileNode) + .fetch_dav_resources(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; @@ -75,14 +63,15 @@ impl FileAclRequestHandler for Server { .caused_by(trc::location!())?; // Validate ACL - self.validate_file_acl( - access_token, - account_id, - node.inner, - Acl::Administer, - Acl::Administer, - ) - .await?; + if !access_token.is_member(account_id) + && !node + .inner + .acls + .effective_acl(access_token) + .contains(Acl::Administer) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } let grants = self .validate_and_map_aces(access_token, request, Collection::FileNode) @@ -113,32 +102,4 @@ impl FileAclRequestHandler for Server { Ok(HttpResponse::new(StatusCode::OK)) } - - async fn validate_file_acl( - &self, - access_token: &AccessToken, - account_id: u32, - node: &ArchivedFileNode, - acl_child: Acl, - acl_parent: Acl, - ) -> crate::Result<()> { - if access_token.is_member(account_id) - || node.acls.effective_acl(access_token).contains(acl_child) - || (u32::from(node.parent_id) > 0 - && self - .has_access_to_document( - access_token, - account_id, - Collection::FileNode, - u32::from(node.parent_id) - 1, - acl_parent, - ) - .await - .caused_by(trc::location!())?) - { - Ok(()) - } else { - Err(DavError::Code(StatusCode::FORBIDDEN)) - } - } } diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index f7590d75..80f0beb6 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -56,44 +56,33 @@ impl FileCopyMoveRequestHandler for Server { .into_owned_uri()?; let from_account_id = from_resource_.account_id; let from_files = self - .fetch_dav_hierarchy(from_account_id, Collection::FileNode) + .fetch_dav_resources(from_account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let from_resource = from_files.map_resource::(&from_resource_)?; // Validate source ACLs - let mut child_acl = Bitmap::new(); - let mut parent_acl = Bitmap::new(); - match (from_resource.resource.is_container, is_move) { - (true, true) => { - child_acl.insert(Acl::Delete); - child_acl.insert(Acl::RemoveItems); - parent_acl.insert(Acl::RemoveItems); - } - (true, false) => { - child_acl.insert(Acl::Read); - child_acl.insert(Acl::ReadItems); - parent_acl.insert(Acl::ReadItems); - } - (false, true) => { - child_acl.insert(Acl::Delete); - parent_acl.insert(Acl::RemoveItems); - } - (false, false) => { - child_acl.insert(Acl::Read); - parent_acl.insert(Acl::ReadItems); + if !access_token.is_member(from_account_id) { + let shared = self + .shared_containers( + access_token, + from_account_id, + Collection::FileNode, + if is_move { + Bitmap::::from_iter([Acl::Read, Acl::Modify]) + } else { + Bitmap::::from_iter([Acl::Read]) + }, + ) + .await + .caused_by(trc::location!())?; + + for resource in from_files.subtree(from_resource_.resource.unwrap()) { + if !shared.contains(resource.document_id) { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } } } - self.validate_child_or_parent_acl( - access_token, - from_account_id, - Collection::FileNode, - from_resource.resource.document_id, - from_resource.resource.parent_id, - child_acl, - parent_acl, - ) - .await?; // Validate destination let destination = self @@ -113,7 +102,7 @@ impl FileCopyMoveRequestHandler for Server { let to_files = if to_account_id == from_account_id { from_files.clone() } else { - self.fetch_dav_hierarchy(to_account_id, Collection::FileNode) + self.fetch_dav_resources(to_account_id, Collection::FileNode) .await .caused_by(trc::location!())? }; @@ -128,7 +117,7 @@ impl FileCopyMoveRequestHandler for Server { to_files.map_parent::(destination_resource_name) { if let Some(mut existing_destination) = to_files - .files + .paths .by_name(destination_resource_name) .map(Destination::from_dav_resource) { @@ -154,53 +143,32 @@ impl FileCopyMoveRequestHandler for Server { return Ok(HttpResponse::new(StatusCode::BAD_GATEWAY)); } else if from_resource.resource.parent_id == destination.parent_id && destination.new_name.is_some() + && is_move { // Rename - self.validate_child_or_parent_acl( - access_token, - from_account_id, - Collection::FileNode, - from_resource.resource.document_id, - from_resource.resource.parent_id, - Acl::Modify, - Acl::ModifyItems, - ) - .await?; return rename_item(self, access_token, from_resource, destination).await; } } // Validate destination ACLs if let Some(document_id) = destination.document_id { - let mut child_acl = Bitmap::new(); - - if destination.is_container { - child_acl.insert(Acl::AddItems); - } else { - child_acl.insert(Acl::Modify); - } - if let Some(delete_destination) = &delete_destination { - self.validate_child_or_parent_acl( + self.validate_acl( access_token, to_account_id, Collection::FileNode, delete_destination.document_id.unwrap(), - delete_destination.parent_id, Acl::Delete, - Acl::RemoveItems, ) .await?; } - self.validate_child_or_parent_acl( + self.validate_acl( access_token, to_account_id, Collection::FileNode, document_id, - destination.parent_id, - child_acl, - Acl::AddItems, + Acl::Modify, ) .await?; } else if !access_token.is_member(to_account_id) { @@ -239,7 +207,7 @@ impl FileCopyMoveRequestHandler for Server { // Validate quota if !is_move || from_account_id != to_account_id { let res = from_files - .files + .paths .by_id(from_resource.resource.document_id) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; let space_needed = from_files @@ -434,7 +402,7 @@ async fn copy_container( // Obtain files to copy let res = from_files - .files + .paths .by_id(from_document_id) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; let mut copy_files = if infinity_copy { diff --git a/crates/dav/src/file/delete.rs b/crates/dav/src/file/delete.rs index 62fded71..c6afb742 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -14,12 +14,10 @@ use jmap_proto::types::{ }; use store::write::{AlignedBytes, Archive, BatchBuilder, log::ChangeLogBuilder}; use trc::AddContext; -use utils::map::bitmap::Bitmap; use crate::{ DavError, DavMethod, common::{ - acl::DavAclHandler, lock::{LockRequestHandler, ResourceState}, uri::DavUriResource, }, @@ -50,7 +48,7 @@ impl FileDeleteRequestHandler for Server { .filter(|r| !r.is_empty()) .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let files = self - .fetch_dav_hierarchy(account_id, Collection::FileNode) + .fetch_dav_resources(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; @@ -62,30 +60,22 @@ impl FileDeleteRequestHandler for Server { // Sort ids descending from the deepest to the root ids.sort_unstable_by(|a, b| b.hierarchy_sequence.cmp(&a.hierarchy_sequence)); - let (document_id, parent_id, is_container) = ids - .last() - .map(|a| (a.document_id, a.parent_id, a.is_container)) - .unwrap(); + let document_id = ids.last().map(|a| a.document_id).unwrap(); let mut sorted_ids = Vec::with_capacity(ids.len()); sorted_ids.extend(ids.into_iter().map(|a| a.document_id)); // Validate ACLs - self.validate_child_or_parent_acl( - access_token, - account_id, - Collection::FileNode, - document_id, - parent_id, - if is_container { - Bitmap::new() - .with_item(Acl::Delete) - .with_item(Acl::RemoveItems) - } else { - Bitmap::new().with_item(Acl::RemoveItems) - }, - Acl::RemoveItems, - ) - .await?; + if !access_token.is_member(account_id) { + let permissions = self + .shared_containers(access_token, account_id, Collection::FileNode, Acl::Delete) + .await + .caused_by(trc::location!())?; + if permissions.len() != sorted_ids.len() as u64 + || sorted_ids.iter().all(|id| permissions.contains(*id)) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + } // Validate headers self.validate_headers( diff --git a/crates/dav/src/file/get.rs b/crates/dav/src/file/get.rs index 1c29dbc3..782a9571 100644 --- a/crates/dav/src/file/get.rs +++ b/crates/dav/src/file/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime}; use groupware::{file::FileNode, hierarchy::DavHierarchy}; use http_proto::HttpResponse; @@ -20,7 +20,7 @@ use crate::{ lock::{LockRequestHandler, ResourceState}, uri::DavUriResource, }, - file::{DavFileResource, acl::FileAclRequestHandler}, + file::DavFileResource, }; pub(crate) trait FileGetRequestHandler: Sync + Send { @@ -46,7 +46,7 @@ impl FileGetRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let files = self - .fetch_dav_hierarchy(account_id, Collection::FileNode) + .fetch_dav_resources(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; @@ -65,8 +65,11 @@ impl FileGetRequestHandler for Server { let node = node_.unarchive::().caused_by(trc::location!())?; // Validate ACL - self.validate_file_acl(access_token, account_id, node, Acl::Read, Acl::ReadItems) - .await?; + if !access_token.is_member(account_id) + && !node.acls.effective_acl(access_token).contains(Acl::Read) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } let (hash, size, content_type) = if let Some(file) = node.file.as_ref() { ( diff --git a/crates/dav/src/file/mkcol.rs b/crates/dav/src/file/mkcol.rs index 917c7188..c0fd49e2 100644 --- a/crates/dav/src/file/mkcol.rs +++ b/crates/dav/src/file/mkcol.rs @@ -51,7 +51,7 @@ impl FileMkColRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let files = self - .fetch_dav_hierarchy(account_id, Collection::FileNode) + .fetch_dav_resources(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_parent_resource(&resource_)?; @@ -63,7 +63,7 @@ impl FileMkColRequestHandler for Server { account_id, Collection::FileNode, resource.resource.0, - Acl::CreateChild, + Acl::AddItems, ) .await?; diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index 605d4a76..059d7520 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -66,7 +66,7 @@ impl DavFileResource for DavResources { ) -> crate::Result> { resource .resource - .and_then(|r| self.files.by_name(r)) + .and_then(|r| self.paths.by_name(r)) .map(|r| UriResource { collection: resource.collection, account_id: resource.account_id, @@ -81,7 +81,7 @@ impl DavFileResource for DavResources { ) -> Option<(Option, &'x str)> { let (parent, child) = if let Some((parent, child)) = resource.rsplit_once('/') { ( - Some(self.files.by_name(parent).map(T::from_dav_resource)?), + Some(self.paths.by_name(parent).map(T::from_dav_resource)?), child, ) } else { @@ -96,7 +96,7 @@ impl DavFileResource for DavResources { resource: &OwnedUri<'x>, ) -> crate::Result, &'x str)>> { if let Some(r) = resource.resource { - if self.files.by_name(r).is_none() { + if self.paths.by_name(r).is_none() { self.map_parent(r) .map(|r| UriResource { collection: resource.collection, diff --git a/crates/dav/src/file/propfind.rs b/crates/dav/src/file/propfind.rs index 299701a9..86c116ce 100644 --- a/crates/dav/src/file/propfind.rs +++ b/crates/dav/src/file/propfind.rs @@ -58,7 +58,7 @@ impl HandleFilePropFindRequest for Server { ) -> crate::Result { let account_id = query.resource.account_id; let files = self - .fetch_dav_hierarchy(account_id, Collection::FileNode) + .fetch_dav_resources(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index bbc68be9..b4c07652 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::{ RequestHeaders, Return, schema::{ @@ -27,7 +27,7 @@ use crate::{ lock::{LockRequestHandler, ResourceState}, uri::DavUriResource, }, - file::{DavFileResource, acl::FileAclRequestHandler}, + file::DavFileResource, }; use super::update_file_node; @@ -64,7 +64,7 @@ impl FilePropPatchRequestHandler for Server { let uri = headers.uri; let account_id = resource_.account_id; let files = self - .fetch_dav_hierarchy(account_id, Collection::FileNode) + .fetch_dav_resources(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; @@ -89,14 +89,15 @@ impl FilePropPatchRequestHandler for Server { .caused_by(trc::location!())?; // Validate ACL - self.validate_file_acl( - access_token, - account_id, - node.inner, - Acl::Modify, - Acl::ModifyItems, - ) - .await?; + if !access_token.is_member(account_id) + && !node + .inner + .acls + .effective_acl(access_token) + .contains(Acl::Modify) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } // Validate headers self.validate_headers( diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index cd2a76e7..7a82e04c 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -4,7 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use common::{ + Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder, +}; use dav_proto::{RequestHeaders, Return, schema::property::Rfc1123DateTime}; use groupware::{ file::{FileNode, FileProperties}, @@ -34,8 +36,6 @@ use crate::{ file::DavFileResource, }; -use super::acl::FileAclRequestHandler; - pub(crate) trait FileUpdateRequestHandler: Sync + Send { fn handle_file_update_request( &self, @@ -61,16 +61,16 @@ impl FileUpdateRequestHandler for Server { .into_owned_uri()?; let account_id = resource.account_id; let files = self - .fetch_dav_hierarchy(account_id, Collection::FileNode) + .fetch_dav_resources(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource_name = resource .resource .ok_or(DavError::Code(StatusCode::CONFLICT))?; - if let Some(document_id) = files.files.by_name(resource_name).map(|r| r.document_id) { + if let Some(document_id) = files.paths.by_name(resource_name).map(|r| r.document_id) { // Update - let node_archive_ = self + let node_ = self .get_property::>( account_id, Collection::FileNode, @@ -80,20 +80,20 @@ impl FileUpdateRequestHandler for Server { .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - let node_archive = node_archive_ + let node = node_ .to_unarchived::() .caused_by(trc::location!())?; - let node = node_archive.inner; // Validate ACL - self.validate_file_acl( - access_token, - account_id, - node, - Acl::Modify, - Acl::ModifyItems, - ) - .await?; + if !access_token.is_member(account_id) + && !node + .inner + .acls + .effective_acl(access_token) + .contains(Acl::Modify) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } // Validate headers match self @@ -104,7 +104,7 @@ impl FileUpdateRequestHandler for Server { account_id, collection: resource.collection, document_id: Some(document_id), - etag: node_archive_.etag().into(), + etag: node.etag().into(), path: resource_name, ..Default::default() }], @@ -117,7 +117,7 @@ impl FileUpdateRequestHandler for Server { Err(DavError::Code(StatusCode::PRECONDITION_FAILED)) if headers.ret == Return::Representation => { - let file = node.file.as_ref().unwrap(); + let file = node.inner.file.as_ref().unwrap(); let contents = self .blob_store() .get_blob(file.blob_hash.0.as_slice(), 0..usize::MAX) @@ -132,9 +132,9 @@ impl FileUpdateRequestHandler for Server { .map(|v| v.as_str()) .unwrap_or("application/octet-stream"), ) - .with_etag(node_archive_.etag()) + .with_etag(node.etag()) .with_last_modified( - Rfc1123DateTime::new(i64::from(node.modified)).to_string(), + Rfc1123DateTime::new(i64::from(node.inner.modified)).to_string(), ) .with_header("Preference-Applied", "return=representation") .with_binary_body(contents)); @@ -143,7 +143,7 @@ impl FileUpdateRequestHandler for Server { } // Verify that the node is a file - if let Some(file) = node.file.as_ref() { + if let Some(file) = node.inner.file.as_ref() { if BlobHash::generate(&bytes).as_slice() == file.blob_hash.0.as_slice() { return Ok(HttpResponse::new(StatusCode::OK)); } @@ -153,7 +153,7 @@ impl FileUpdateRequestHandler for Server { // Validate quota let extra_bytes = (bytes.len() as u64) - .saturating_sub(u32::from(node.file.as_ref().unwrap().size) as u64); + .saturating_sub(u32::from(node.inner.file.as_ref().unwrap().size) as u64); if extra_bytes > 0 { self.has_available_quota( &self.get_resource_token(access_token, account_id).await?, @@ -171,8 +171,7 @@ impl FileUpdateRequestHandler for Server { // Build node let change_id = self.generate_snowflake_id().caused_by(trc::location!())?; - let node = node_archive.to_deserialized().caused_by(trc::location!())?; - let mut new_node = node.inner.clone(); + let mut new_node = node.deserialize::().caused_by(trc::location!())?; let new_file = new_node.file.as_mut().unwrap(); new_file.blob_hash = blob_hash; new_file.media_type = headers.content_type.map(|v| v.to_string()); diff --git a/crates/email/src/mailbox/index.rs b/crates/email/src/mailbox/index.rs index 42c062b0..6acffb9a 100644 --- a/crates/email/src/mailbox/index.rs +++ b/crates/email/src/mailbox/index.rs @@ -18,13 +18,13 @@ use super::{ArchivedMailbox, Mailbox}; impl IndexableObject for Mailbox { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Text { + IndexValue::Index { field: Property::Name.into(), value: self.name.to_lowercase().into(), }, - IndexValue::Text { + IndexValue::Index { field: Property::Role.into(), - value: self.role.as_str().unwrap_or_default().into(), + value: self.role.as_str().into(), }, IndexValue::Tag { field: Property::Role.into(), @@ -34,17 +34,17 @@ impl IndexableObject for Mailbox { vec![] }, }, - IndexValue::U32 { + IndexValue::Index { field: Property::ParentId.into(), value: self.parent_id.into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::SortOrder.into(), - value: self.sort_order, + value: self.sort_order.into(), }, - IndexValue::U32List { + IndexValue::IndexList { field: Property::IsSubscribed.into(), - value: (&self.subscribers).into(), + value: self.subscribers.iter().map(Into::into).collect::>(), }, IndexValue::Acl { value: (&self.acls).into(), @@ -57,13 +57,13 @@ impl IndexableObject for Mailbox { impl IndexableObject for &ArchivedMailbox { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Text { + IndexValue::Index { field: Property::Name.into(), value: self.name.to_lowercase().into(), }, - IndexValue::Text { + IndexValue::Index { field: Property::Role.into(), - value: self.role.as_str().unwrap_or_default().into(), + value: self.role.as_str().into(), }, IndexValue::Tag { field: Property::Role.into(), @@ -73,22 +73,17 @@ impl IndexableObject for &ArchivedMailbox { vec![] }, }, - IndexValue::U32 { + IndexValue::Index { field: Property::ParentId.into(), - value: u32::from(self.parent_id).into(), + value: self.parent_id.into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::SortOrder.into(), - value: self.sort_order.as_ref().map(u32::from), + value: self.sort_order.into(), }, - IndexValue::U32List { + IndexValue::IndexList { field: Property::IsSubscribed.into(), - value: self - .subscribers - .iter() - .map(u32::from) - .collect::>() - .into(), + value: self.subscribers.iter().map(Into::into).collect::>(), }, IndexValue::Acl { value: self diff --git a/crates/email/src/sieve/activate.rs b/crates/email/src/sieve/activate.rs index 47334dbd..19d00184 100644 --- a/crates/email/src/sieve/activate.rs +++ b/crates/email/src/sieve/activate.rs @@ -7,7 +7,6 @@ use common::{Server, storage::index::ObjectIndexBuilder}; use jmap_proto::types::{collection::Collection, property::Property}; use store::{ - SerializeInfallible, query::Filter, write::{AlignedBytes, Archive, BatchBuilder}, }; @@ -36,7 +35,7 @@ impl SieveScriptActivate for Server { .filter( account_id, Collection::SieveScript, - vec![Filter::eq(Property::IsActive, 1u32.serialize())], + vec![Filter::eq(Property::IsActive, vec![1u8])], ) .await? .results; diff --git a/crates/email/src/sieve/index.rs b/crates/email/src/sieve/index.rs index 07951676..1a417162 100644 --- a/crates/email/src/sieve/index.rs +++ b/crates/email/src/sieve/index.rs @@ -12,13 +12,15 @@ use super::{ArchivedSieveScript, SieveScript}; impl IndexableObject for SieveScript { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Text { + IndexValue::Index { field: Property::Name.into(), value: self.name.to_lowercase().into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::IsActive.into(), - value: Some(self.is_active as u32), + value: if self.is_active { &[1u8] } else { &[0u8] } + .as_slice() + .into(), }, IndexValue::Blob { value: self.blob_hash.clone(), @@ -34,13 +36,15 @@ impl IndexableAndSerializableObject for SieveScript {} impl IndexableObject for &ArchivedSieveScript { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Text { + IndexValue::Index { field: Property::Name.into(), value: self.name.to_lowercase().into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::IsActive.into(), - value: Some(self.is_active as u32), + value: if self.is_active { &[1u8] } else { &[0u8] } + .as_slice() + .into(), }, IndexValue::Blob { value: (&self.blob_hash).into(), diff --git a/crates/email/src/sieve/ingest.rs b/crates/email/src/sieve/ingest.rs index 39a72483..21ace559 100644 --- a/crates/email/src/sieve/ingest.rs +++ b/crates/email/src/sieve/ingest.rs @@ -559,7 +559,7 @@ impl SieveScriptIngest for Server { .filter( account_id, Collection::SieveScript, - vec![Filter::eq(Property::IsActive, 1u32.serialize())], + vec![Filter::eq(Property::IsActive, vec![1u8])], ) .await .caused_by(trc::location!())? diff --git a/crates/email/src/submission/index.rs b/crates/email/src/submission/index.rs index fce9a4e2..42b8d667 100644 --- a/crates/email/src/submission/index.rs +++ b/crates/email/src/submission/index.rs @@ -12,25 +12,25 @@ use super::{ArchivedEmailSubmission, EmailSubmission}; impl IndexableObject for EmailSubmission { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Text { + IndexValue::Index { field: Property::UndoStatus.into(), value: self.undo_status.as_index().into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::EmailId.into(), - value: Some(self.email_id), + value: self.email_id.into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::ThreadId.into(), - value: Some(self.thread_id), + value: self.thread_id.into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::IdentityId.into(), - value: Some(self.identity_id), + value: self.identity_id.into(), }, - IndexValue::U64 { + IndexValue::Index { field: Property::SendAt.into(), - value: Some(self.send_at), + value: self.send_at.into(), }, ] .into_iter() @@ -40,25 +40,25 @@ impl IndexableObject for EmailSubmission { impl IndexableObject for &ArchivedEmailSubmission { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Text { + IndexValue::Index { field: Property::UndoStatus.into(), value: self.undo_status.as_index().into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::EmailId.into(), - value: Some(u32::from(self.email_id)), + value: self.email_id.into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::ThreadId.into(), - value: Some(u32::from(self.thread_id)), + value: self.thread_id.into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::IdentityId.into(), - value: Some(u32::from(self.identity_id)), + value: self.identity_id.into(), }, - IndexValue::U64 { + IndexValue::Index { field: Property::SendAt.into(), - value: Some(u64::from(self.send_at)), + value: self.send_at.into(), }, ] .into_iter() diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index 058336b7..5920b801 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -9,6 +9,8 @@ use jmap_proto::types::{acl::Acl, value::AclGrant}; use store::{SERIALIZE_OBJ_14_V1, SerializedVersion}; use utils::map::vec_map::VecMap; +use crate::DavName; + #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] @@ -38,9 +40,8 @@ pub struct CalendarPreferences { rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] pub struct CalendarEvent { - pub name: Option, + pub names: Vec, pub event: ICalendar, - pub calendar_ids: Vec, pub user_properties: VecMap, pub created: u64, pub updated: u64, diff --git a/crates/groupware/src/contact/index.rs b/crates/groupware/src/contact/index.rs index 3ac559c5..02a05eae 100644 --- a/crates/groupware/src/contact/index.rs +++ b/crates/groupware/src/contact/index.rs @@ -4,16 +4,23 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject}; -use jmap_proto::types::{property::Property, value::AclGrant}; +use common::storage::index::{ + IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject, +}; +use jmap_proto::types::value::AclGrant; +use store::SerializeInfallible; -use super::{AddressBook, ArchivedAddressBook, ContactCard}; +use crate::{IDX_CARD_UID, IDX_NAME}; + +use super::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}; impl IndexableObject for AddressBook { fn index_values(&self) -> impl Iterator> { + // Note: When adding a new value with index id above 0u8, tune `build_hierarchy`` to skip + // this value during iteration. [ - IndexValue::Text { - field: Property::Name.into(), + IndexValue::Index { + field: IDX_NAME, value: self.name.as_str().into(), }, IndexValue::Acl { @@ -33,8 +40,8 @@ impl IndexableObject for AddressBook { impl IndexableObject for &ArchivedAddressBook { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Text { - field: Property::Name.into(), + IndexValue::Index { + field: IDX_NAME, value: self.name.as_str().into(), }, IndexValue::Acl { @@ -61,21 +68,53 @@ impl IndexableAndSerializableObject for AddressBook {} impl IndexableObject for ContactCard { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Text { - field: Property::Name.into(), - value: self.name.as_str().into(), + IndexValue::IndexList { + field: IDX_NAME, + value: self + .names + .iter() + .map(|v| IndexItem::Vec(v.serialize())) + .collect::>(), }, - IndexValue::U32List { - field: Property::ParentId.into(), - value: self.addressbook_ids.as_slice().into(), + IndexValue::Index { + field: IDX_CARD_UID, + value: self.card.uid().into(), }, IndexValue::Quota { used: self.dead_properties.size() as u32 + self.display_name.as_ref().map_or(0, |n| n.len() as u32) - + self.name.len() as u32 + + self.names.iter().map(|n| n.name.len() as u32).sum::() + self.size, }, ] .into_iter() } } + +impl IndexableObject for &ArchivedContactCard { + fn index_values(&self) -> impl Iterator> { + [ + IndexValue::IndexList { + field: IDX_NAME, + value: self + .names + .iter() + .map(|v| IndexItem::Vec(v.serialize())) + .collect::>(), + }, + IndexValue::Index { + field: IDX_CARD_UID, + value: self.card.uid().into(), + }, + IndexValue::Quota { + used: self.dead_properties.size() as u32 + + self.display_name.as_ref().map_or(0, |n| n.len() as u32) + + self.names.iter().map(|n| n.name.len() as u32).sum::() + + self.size, + }, + ] + .into_iter() + } +} + +impl IndexableAndSerializableObject for ContactCard {} diff --git a/crates/groupware/src/contact/mod.rs b/crates/groupware/src/contact/mod.rs index 1d8daeaa..28f27cf0 100644 --- a/crates/groupware/src/contact/mod.rs +++ b/crates/groupware/src/contact/mod.rs @@ -9,7 +9,9 @@ pub mod index; use calcard::vcard::VCard; use dav_proto::schema::request::DeadProperty; use jmap_proto::types::{acl::Acl, value::AclGrant}; -use store::{SERIALIZE_OBJ_15_V1, SerializedVersion}; +use store::{SERIALIZE_OBJ_15_V1, SERIALIZE_OBJ_16_V1, SerializedVersion}; + +use crate::DavName; #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, @@ -24,6 +26,8 @@ pub struct AddressBook { pub subscribers: Vec, pub dead_properties: DeadProperty, pub acls: Vec, + pub created: i64, + pub modified: i64, } pub enum AddressBookRight { @@ -38,13 +42,12 @@ pub enum AddressBookRight { )] #[rkyv(derive(Debug))] pub struct ContactCard { - pub name: String, + pub names: Vec, pub display_name: Option, - pub addressbook_ids: Vec, pub card: VCard, pub dead_properties: DeadProperty, - pub created: u64, - pub updated: u64, + pub created: i64, + pub modified: i64, pub size: u32, } @@ -78,3 +81,9 @@ impl SerializedVersion for AddressBook { SERIALIZE_OBJ_15_V1 } } + +impl SerializedVersion for ContactCard { + fn serialize_version() -> u8 { + SERIALIZE_OBJ_16_V1 + } +} diff --git a/crates/groupware/src/file/index.rs b/crates/groupware/src/file/index.rs index d0498cf4..add92217 100644 --- a/crates/groupware/src/file/index.rs +++ b/crates/groupware/src/file/index.rs @@ -21,7 +21,7 @@ impl IndexableObject for FileNode { let mut values = Vec::with_capacity(6); values.extend([ - IndexValue::Text { + IndexValue::Index { field: Property::Name.into(), value: percent_encoding::percent_decode_str(&self.name) .decode_utf8() @@ -29,7 +29,7 @@ impl IndexableObject for FileNode { .to_lowercase() .into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::ParentId.into(), value: self.parent_id.into(), }, @@ -44,7 +44,7 @@ impl IndexableObject for FileNode { IndexValue::Blob { value: file.blob_hash.clone(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::Size.into(), value: size.into(), }, @@ -63,11 +63,11 @@ impl IndexableObject for &ArchivedFileNode { let mut values = Vec::with_capacity(6); values.extend([ - IndexValue::Text { + IndexValue::Index { field: Property::Name.into(), value: self.name.to_lowercase().into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::ParentId.into(), value: u32::from(self.parent_id).into(), }, @@ -87,7 +87,7 @@ impl IndexableObject for &ArchivedFileNode { IndexValue::Blob { value: (&file.blob_hash).into(), }, - IndexValue::U32 { + IndexValue::Index { field: Property::Size.into(), value: size.into(), }, diff --git a/crates/groupware/src/hierarchy.rs b/crates/groupware/src/hierarchy.rs index 57c7ad8a..e2c6ea9c 100644 --- a/crates/groupware/src/hierarchy.rs +++ b/crates/groupware/src/hierarchy.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use common::{DavResource, DavResourceId, DavResources, Server}; -use jmap_proto::types::{collection::Collection, property::Property}; +use jmap_proto::types::collection::Collection; use store::{ Deserialize, IndexKey, IterateParams, SerializeInfallible, U32_LEN, ahash::AHashMap, write::key::DeserializeBigEndian, @@ -15,10 +15,10 @@ use store::{ use trc::AddContext; use utils::bimap::IdBimap; -use crate::file::FileNode; +use crate::{DavName, IDX_NAME, file::FileNode}; pub trait DavHierarchy: Sync + Send { - fn fetch_dav_hierarchy( + fn fetch_dav_resources( &self, account_id: u32, collection: Collection, @@ -26,7 +26,7 @@ pub trait DavHierarchy: Sync + Send { } impl DavHierarchy for Server { - async fn fetch_dav_hierarchy( + async fn fetch_dav_resources( &self, account_id: u32, collection: Collection, @@ -65,20 +65,14 @@ impl DavHierarchy for Server { } } -#[derive(Default)] -struct DavTempResource { - name: String, - parent_id: Vec, -} - async fn build_hierarchy( server: &Server, account_id: u32, collection: Collection, ) -> trc::Result { let collection = u8::from(collection); - let mut containers: AHashMap = AHashMap::with_capacity(16); - let mut resources: AHashMap = AHashMap::with_capacity(16); + let mut containers: AHashMap = AHashMap::with_capacity(16); + let mut resources: AHashMap> = AHashMap::with_capacity(16); server .store() @@ -88,14 +82,14 @@ async fn build_hierarchy( account_id, collection, document_id: 0, - field: 0, + field: IDX_NAME, key: 0u32.serialize(), }, IndexKey { account_id, collection: collection + 1, document_id: u32::MAX, - field: u8::MAX, + field: IDX_NAME, key: u32::MAX.serialize(), }, ) @@ -110,24 +104,17 @@ async fn build_hierarchy( .get(U32_LEN) .copied() .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; - let key_property = key - .get(U32_LEN + 1) - .copied() - .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; - let resource = if key_collection == collection { - containers.entry(document_id).or_default() + if key_collection == collection { + containers.insert( + document_id, + std::str::from_utf8(value) + .map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))? + .to_string(), + ); } else { - resources.entry(document_id).or_default() - }; - - if key_property == u8::from(Property::Value) { - resource.name = std::str::from_utf8(value) - .map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))? - .to_string(); - } else if key_property == u8::from(Property::ParentId) { - resource.parent_id.push( - u32::deserialize(value) + resources.entry(document_id).or_default().push( + DavName::deserialize(value) .map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?, ); } @@ -139,21 +126,21 @@ async fn build_hierarchy( .caused_by(trc::location!())?; let mut files = DavResources { - files: IdBimap::with_capacity(containers.len() + resources.len()), + paths: IdBimap::with_capacity(containers.len() + resources.len()), size: std::mem::size_of::() as u64, modseq: None, }; - for (document_id, resource) in resources { - for parent_id in resource.parent_id { - if let Some(container) = containers.get(&parent_id) { - let name = format!("{}/{}", container.name, resource.name); + for (document_id, dav_names) in resources { + for dav_name in dav_names { + if let Some(container) = containers.get(&dav_name.parent_id) { + let name = format!("{}/{}", container, dav_name.name); files.size += (std::mem::size_of::() + std::mem::size_of::() + name.len()) as u64; - files.files.insert(DavResource { + files.paths.insert(DavResource { document_id, - parent_id: parent_id.into(), + parent_id: dav_name.parent_id.into(), name, size: 0, is_container: false, @@ -163,14 +150,13 @@ async fn build_hierarchy( } } - for (document_id, container) in containers { - files.size += (std::mem::size_of::() - + std::mem::size_of::() - + container.name.len()) as u64; - files.files.insert(DavResource { + for (document_id, name) in containers { + files.size += + (std::mem::size_of::() + std::mem::size_of::() + name.len()) as u64; + files.paths.insert(DavResource { document_id, parent_id: None, - name: container.name, + name, size: 0, is_container: true, hierarchy_sequence: 0, @@ -186,7 +172,7 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result() as u64, modseq: None, }; @@ -195,7 +181,7 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result() + std::mem::size_of::() + expanded.name.len()) as u64; - files.files.insert(DavResource { + files.paths.insert(DavResource { document_id: expanded.document_id, parent_id: expanded.parent_id, name: expanded.name, diff --git a/crates/groupware/src/lib.rs b/crates/groupware/src/lib.rs index fbc592da..e671afde 100644 --- a/crates/groupware/src/lib.rs +++ b/crates/groupware/src/lib.rs @@ -4,7 +4,68 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use store::{Deserialize, SerializeInfallible, write::key::KeySerializer}; +use utils::codec::leb128::Leb128Reader; + pub mod calendar; pub mod contact; pub mod file; pub mod hierarchy; + +pub const IDX_NAME: u8 = 0; +pub const IDX_CARD_UID: u8 = 1; + +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +#[rkyv(derive(Debug))] +pub struct DavName { + pub name: String, + pub parent_id: u32, +} + +impl SerializeInfallible for DavName { + fn serialize(&self) -> Vec { + KeySerializer::new(self.name.len() + std::mem::size_of::()) + .write_leb128(self.parent_id) + .write(self.name.as_bytes()) + .finalize() + } +} + +impl SerializeInfallible for ArchivedDavName { + fn serialize(&self) -> Vec { + KeySerializer::new(self.name.len() + std::mem::size_of::()) + .write_leb128(self.parent_id.to_native()) + .write(self.name.as_bytes()) + .finalize() + } +} + +impl DavName { + pub fn new(name: String, parent_id: u32) -> Self { + Self { name, parent_id } + } +} + +impl Deserialize for DavName { + fn deserialize(bytes: &[u8]) -> trc::Result { + let (parent_id, bytes_read) = bytes.read_leb128::().ok_or_else(|| { + trc::StoreEvent::DataCorruption + .caused_by(trc::location!()) + .ctx(trc::Key::Value, bytes) + })?; + + let name = bytes + .get(bytes_read..) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .ok_or_else(|| { + trc::StoreEvent::DataCorruption + .caused_by(trc::location!()) + .ctx(trc::Key::Value, bytes) + })? + .to_string(); + + Ok(DavName { name, parent_id }) + } +} diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 24f0ba7c..09932aca 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; diff --git a/crates/utils/src/bimap.rs b/crates/utils/src/bimap.rs index 8d3b2a55..f58a017d 100644 --- a/crates/utils/src/bimap.rs +++ b/crates/utils/src/bimap.rs @@ -45,7 +45,7 @@ impl IdBimap { } pub fn iter(&self) -> impl Iterator { - self.id_to_name.values().map(|v| v.as_ref()) + self.name_to_id.values().map(|v| v.as_ref()) } }