ACL method and reports

This commit is contained in:
mdecimus
2025-03-27 19:40:12 +01:00
parent 3f825aaafd
commit a5e6f77b26
24 changed files with 742 additions and 125 deletions

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,

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) =

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(

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