ACL method and reports
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) =
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user