ACL method and reports

This commit is contained in:
mdecimus
2025-05-16 16:20:04 +02:00
parent 3f825aaafd
commit a5e6f77b26
24 changed files with 742 additions and 125 deletions
+4
View File
@@ -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),
}
}
}
@@ -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, "<D:sync-collection/>"),
ReportSet::ExpandProperty => write!(f, "<D:expand-property/>"),
ReportSet::AddressbookQuery => write!(f, "<C:addressbook-query/>"),
ReportSet::AddressbookMultiGet => write!(f, "<C:addressbook-multiget/>"),
ReportSet::CalendarQuery => write!(f, "<C:calendar-query/>"),
ReportSet::CalendarMultiGet => write!(f, "<C:calendar-multiget/>"),
ReportSet::FreeBusyQuery => write!(f, "<C:free-busy-query/>"),
ReportSet::AclPrincipalPropSet => write!(f, "<D:acl-principal-prop-set/>"),
ReportSet::PrincipalMatch => write!(f, "<D:principal-match/>"),
ReportSet::PrincipalPropertySearch => write!(f, "<D:principal-property-search/>"),
ReportSet::PrincipalSearchPropertySet => {
write!(f, "<D:principal-search-property-set/>")
}
}
}
}
+10
View File
@@ -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)]
+6
View File
@@ -338,3 +338,9 @@ impl ArchivedDeadProperty {
}
}
}
impl PropertyUpdate {
pub fn has_changes(&self) -> bool {
!self.set.is_empty() || !self.remove.is_empty()
}
}
+1 -1
View File
@@ -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<T: Display>(pub Vec<T>);
+260 -7
View File
@@ -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<Output = crate::Result<HttpResponse>> + Send;
fn validate_and_map_aces(
&self,
access_token: &AccessToken,
acl: dav_proto::schema::request::Acl,
collection: Collection,
) -> impl Future<Output = crate::Result<Vec<AclGrant>>> + 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<HttpResponse> {
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::<Archive<AlignedBytes>>(
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::<FileNode>()
.caused_by(trc::location!())?
.acls
}
Collection::AddressBook => {
&archive
.unarchive::<AddressBook>()
.caused_by(trc::location!())?
.acls
}
Collection::Calendar => {
&archive
.unarchive::<Calendar>()
.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<Vec<AclGrant>> {
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::<Acl>::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,
+12 -20
View File
@@ -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) =
+22 -4
View File
@@ -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(
+42 -1
View File
@@ -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<u32>, Option<&'x str>>;
pub(crate) type OwnedUri<'x> = UriResource<u32, Option<&'x str>>;
//pub(crate) type DocumentUri<'x> = UriResource<u32, u32>;
pub(crate) type DocumentUri = UriResource<u32, u32>;
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<Output = crate::Result<UnresolvedUri<'x>>> + Send;
fn map_uri_resource(
&self,
uri: OwnedUri<'_>,
) -> impl Future<Output = trc::Result<Option<DocumentUri>>> + 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<Option<DocumentUri>> {
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> {
+36 -5
View File
@@ -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::<FileNode>().caused_by(trc::location!())?;
let node = node_
.to_unarchived::<FileNode>()
.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(
+34 -26
View File
@@ -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::<Archive<AlignedBytes>>(
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::<FileNode>()
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.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::<Archive<AlignedBytes>>(
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::<FileNode>()
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let dest_node = dest_node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
// source_node is the file to be copied
let source_node_ = server
let source_node__ = server
.get_property::<Archive<AlignedBytes>>(
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::<FileNode>()
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let source_node_ = source_node__
.to_unarchived::<FileNode>()
.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::<Archive<AlignedBytes>>(
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::<FileNode>()
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let dest_node = dest_node_
.to_unarchived::<FileNode>()
.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::<Archive<AlignedBytes>>(
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::<FileNode>()
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.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::<Archive<AlignedBytes>>(
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::<FileNode>()
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.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;
}
-2
View File
@@ -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))
+3 -3
View File
@@ -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<FileNode>,
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<FileNode>,
node: Archive<&ArchivedFileNode>,
account_id: u32,
document_id: u32,
) -> trc::Result<()> {
+16 -4
View File
@@ -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 => {
+6 -3
View File
@@ -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::<Archive<AlignedBytes>>(
@@ -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,
+92
View File
@@ -0,0 +1,92 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* 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<Output = crate::Result<HttpResponse>> + Send;
}
impl PrincipalMatching for Server {
async fn handle_principal_match(
&self,
access_token: &AccessToken,
headers: RequestHeaders<'_>,
mut request: PrincipalMatch,
) -> crate::Result<HttpResponse> {
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)),
}
}
}
+2
View File
@@ -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;
+15 -5
View File
@@ -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(
+92
View File
@@ -0,0 +1,92 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* 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<Output = crate::Result<HttpResponse>> + Send;
}
impl PrincipalPropSearch for Server {
async fn handle_principal_property_search(
&self,
access_token: &AccessToken,
mut request: PrincipalPropertySearch,
) -> crate::Result<HttpResponse> {
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()))
}
}
+50 -20
View File
@@ -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!(),
}
+15 -2
View File
@@ -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<u32, CalendarPreferences>,
pub acls: Vec<AclGrant>,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct CalendarPreferences {
pub name: String,
pub description: Option<String>,
@@ -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<String, ICalendar>,
pub default_alerts_without_time: VecMap<String, ICalendar>,
pub time_zone: Timezone,
pub time_zone: Timezone,*/
}
pub struct CalendarEvent {
@@ -95,3 +102,9 @@ impl From<CalendarRight> for Acl {
}
}
}
impl SerializedVersion for Calendar {
fn serialize_version() -> u8 {
SERIALIZE_OBJ_14_V1
}
}
+11
View File
@@ -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<String>,
@@ -57,3 +62,9 @@ impl From<AddressBookRight> for Acl {
}
}
}
impl SerializedVersion for AddressBook {
fn serialize_version() -> u8 {
SERIALIZE_OBJ_15_V1
}
}
-21
View File
@@ -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::<Vec<_>>()
);
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::<Vec<_>>()
);
Ok(files)
}
}
@@ -68,9 +50,6 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result<F
.fetch_folders::<FileNode>(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::<Files>() as u64,
+1 -1
View File
@@ -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;