Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ArchivedResource;
|
||||
use crate::{
|
||||
DavError, DavErrorCondition, DavResourceName, common::uri::DavUriResource,
|
||||
principal::propfind::PrincipalPropFind,
|
||||
};
|
||||
use common::{DavResources, Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::{DavProperty, Privilege, WebDavProperty},
|
||||
request::{AclPrincipalPropSet, PropFind},
|
||||
response::{Ace, BaseCondition, GrantDeny, Href, MultiStatus, Principal},
|
||||
},
|
||||
};
|
||||
use groupware::RFC_3986;
|
||||
use groupware::{cache::GroupwareCache, calendar::Calendar, contact::AddressBook, file::FileNode};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use rkyv::vec::ArchivedVec;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use store::{ahash::AHashSet, roaring::RoaringBitmap, write::BatchBuilder};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::{Acl, AclGrant, ArchivedAclGrant},
|
||||
collection::Collection,
|
||||
};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
pub(crate) trait DavAclHandler: Sync + Send {
|
||||
fn handle_acl_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: dav_proto::schema::request::Acl,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + 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 resolve_ace(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
grants: &ArchivedVec<ArchivedAclGrant>,
|
||||
expand: Option<&PropFind>,
|
||||
) -> impl Future<Output = crate::Result<Vec<Ace>>> + Send;
|
||||
}
|
||||
|
||||
pub(crate) trait ResourceAcl {
|
||||
fn validate_and_map_parent_acl(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
is_member: bool,
|
||||
parent_id: Option<u32>,
|
||||
check_acls: impl Into<Bitmap<Acl>> + Send,
|
||||
) -> crate::Result<u32>;
|
||||
}
|
||||
|
||||
impl DavAclHandler for Server {
|
||||
async fn handle_acl_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: dav_proto::schema::request::Acl,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let collection = resource_.collection;
|
||||
|
||||
if !matches!(
|
||||
collection,
|
||||
Collection::AddressBook | Collection::Calendar | Collection::FileNode
|
||||
) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
let resources = self
|
||||
.fetch_dav_resources(access_token.account_id(), account_id, collection.into())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resource_
|
||||
.resource
|
||||
.and_then(|r| resources.by_path(r))
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if !resource.resource.is_container() && !matches!(collection, Collection::FileNode) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Fetch node
|
||||
let archive = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
collection,
|
||||
resource.document_id(),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let container =
|
||||
ArchivedResource::from_archive(&archive, collection).caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACL
|
||||
let acls = container.acls().unwrap();
|
||||
if !access_token.is_member(account_id)
|
||||
&& !acls.effective_acl(access_token).contains(Acl::Share)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate ACEs
|
||||
let grants = self
|
||||
.validate_and_map_aces(access_token, request, collection)
|
||||
.await?;
|
||||
|
||||
if grants.len() != acls.len() || acls.iter().zip(grants.iter()).any(|(a, b)| a != b) {
|
||||
// Refresh ACLs
|
||||
self.refresh_archived_acls(&grants, acls)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
match container {
|
||||
ArchivedResource::Calendar(calendar) => {
|
||||
let mut new_calendar = calendar
|
||||
.deserialize::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
new_calendar.acls = grants;
|
||||
new_calendar
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
calendar,
|
||||
account_id,
|
||||
resource.document_id(),
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
ArchivedResource::AddressBook(book) => {
|
||||
let mut new_book = book
|
||||
.deserialize::<AddressBook>()
|
||||
.caused_by(trc::location!())?;
|
||||
new_book.acls = grants;
|
||||
new_book
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
book,
|
||||
account_id,
|
||||
resource.document_id(),
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
ArchivedResource::FileNode(node) => {
|
||||
let mut new_node =
|
||||
node.deserialize::<FileNode>().caused_by(trc::location!())?;
|
||||
new_node.acls = grants;
|
||||
new_node
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
node,
|
||||
account_id,
|
||||
resource.document_id(),
|
||||
true,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
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(access_token, 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));
|
||||
}
|
||||
|
||||
let archive = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
uri.account_id,
|
||||
uri.collection,
|
||||
uri.resource,
|
||||
))
|
||||
.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!(),
|
||||
};
|
||||
|
||||
// Validate ACLs
|
||||
if !access_token.is_member(uri.account_id)
|
||||
&& !acls.effective_acl(access_token).contains(Acl::Share)
|
||||
{
|
||||
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() {
|
||||
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::AddItems);
|
||||
acls.insert(Acl::ModifyItems);
|
||||
acls.insert(Acl::RemoveItems);
|
||||
}
|
||||
Privilege::WriteContent => {
|
||||
acls.insert(Acl::AddItems);
|
||||
acls.insert(Acl::Modify);
|
||||
acls.insert(Acl::ModifyItems);
|
||||
}
|
||||
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::Share);
|
||||
}
|
||||
Privilege::ReadFreeBusy
|
||||
| Privilege::ScheduleQueryFreeBusy
|
||||
| Privilege::ScheduleSendFreeBusy => {
|
||||
if collection == Collection::Calendar {
|
||||
acls.insert(Acl::SchedulingReadFreeBusy);
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NotSupportedPrivilege,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Privilege::ScheduleDeliver | Privilege::ScheduleSend => {
|
||||
if collection == Collection::Calendar {
|
||||
acls.insert(Acl::SchedulingReadFreeBusy);
|
||||
acls.insert(Acl::SchedulingInvite);
|
||||
acls.insert(Acl::SchedulingReply);
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NotSupportedPrivilege,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Privilege::ScheduleDeliverInvite | Privilege::ScheduleSendInvite => {
|
||||
if collection == Collection::Calendar {
|
||||
acls.insert(Acl::SchedulingInvite);
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NotSupportedPrivilege,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Privilege::ScheduleDeliverReply | Privilege::ScheduleSendReply => {
|
||||
if collection == Collection::Calendar {
|
||||
acls.insert(Acl::SchedulingReply);
|
||||
} 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(QueryParams::id(principal_id).with_return_member_of(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 resolve_ace(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
grants: &ArchivedVec<ArchivedAclGrant>,
|
||||
expand: Option<&PropFind>,
|
||||
) -> crate::Result<Vec<Ace>> {
|
||||
let mut aces = Vec::with_capacity(grants.len());
|
||||
if access_token.is_member(account_id)
|
||||
|| grants.effective_acl(access_token).contains(Acl::Share)
|
||||
{
|
||||
for grant in grants.iter() {
|
||||
let grant_account_id = u32::from(grant.account_id);
|
||||
let principal = if let Some(expand) = expand {
|
||||
self.expand_principal(access_token, grant_account_id, expand)
|
||||
.await?
|
||||
.map(Principal::Response)
|
||||
.unwrap_or_else(|| {
|
||||
Principal::Href(Href(format!(
|
||||
"{}/_{grant_account_id}/",
|
||||
DavResourceName::Principal.base_path(),
|
||||
)))
|
||||
})
|
||||
} else {
|
||||
let grant_account = self
|
||||
.account(grant_account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Principal::Href(Href(format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Principal.base_path(),
|
||||
percent_encoding::utf8_percent_encode(grant_account.name(), RFC_3986),
|
||||
)))
|
||||
};
|
||||
|
||||
aces.push(Ace::new(
|
||||
principal,
|
||||
GrantDeny::grant(current_user_privilege_set(Bitmap::<Acl>::from(
|
||||
&grant.grants,
|
||||
))),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(aces)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceAcl for DavResources {
|
||||
fn validate_and_map_parent_acl(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
is_member: bool,
|
||||
parent_id: Option<u32>,
|
||||
check_acls: impl Into<Bitmap<Acl>> + Send,
|
||||
) -> crate::Result<u32> {
|
||||
match parent_id {
|
||||
Some(parent_id) => {
|
||||
if is_member || self.has_access_to_container(access_token, parent_id, check_acls) {
|
||||
Ok(parent_id + 1)
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::FORBIDDEN))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if is_member {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::FORBIDDEN))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Privileges {
|
||||
fn current_privilege_set(
|
||||
&self,
|
||||
account_id: u32,
|
||||
grants: &ArchivedVec<ArchivedAclGrant>,
|
||||
is_calendar: bool,
|
||||
) -> Vec<Privilege>;
|
||||
}
|
||||
|
||||
impl Privileges for AccessToken {
|
||||
fn current_privilege_set(
|
||||
&self,
|
||||
account_id: u32,
|
||||
grants: &ArchivedVec<ArchivedAclGrant>,
|
||||
is_calendar: bool,
|
||||
) -> Vec<Privilege> {
|
||||
if self.is_member(account_id) {
|
||||
Privilege::all(is_calendar)
|
||||
} else {
|
||||
current_user_privilege_set(grants.effective_acl(self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_user_privilege_set(acl_bitmap: Bitmap<Acl>) -> Vec<Privilege> {
|
||||
let mut acls = AHashSet::with_capacity(16);
|
||||
for grant in acl_bitmap {
|
||||
match grant {
|
||||
Acl::Read | Acl::ReadItems => {
|
||||
acls.insert(Privilege::Read);
|
||||
acls.insert(Privilege::ReadCurrentUserPrivilegeSet);
|
||||
}
|
||||
Acl::Modify => {
|
||||
acls.insert(Privilege::WriteProperties);
|
||||
}
|
||||
Acl::ModifyItems => {
|
||||
acls.insert(Privilege::WriteContent);
|
||||
}
|
||||
Acl::Delete | Acl::RemoveItems => {
|
||||
acls.insert(Privilege::Write);
|
||||
}
|
||||
Acl::Share => {
|
||||
acls.insert(Privilege::ReadAcl);
|
||||
acls.insert(Privilege::WriteAcl);
|
||||
}
|
||||
Acl::SchedulingReadFreeBusy => {
|
||||
acls.insert(Privilege::ReadFreeBusy);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
acls.into_iter().collect()
|
||||
}
|
||||
Reference in New Issue
Block a user