mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-03-17 14:34:03 +00:00
569 lines
20 KiB
Rust
569 lines
20 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* 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 directory::{QueryParams, Type, backend::internal::manage::ManageDirectory};
|
|
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::{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, 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
|
|
.get_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::Administer)
|
|
{
|
|
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;
|
|
|
|
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,
|
|
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,
|
|
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,
|
|
node,
|
|
account_id,
|
|
resource.document_id(),
|
|
&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
|
|
.get_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::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() {
|
|
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::Administer);
|
|
}
|
|
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::Administer)
|
|
{
|
|
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_name = self
|
|
.store()
|
|
.get_principal_name(grant_account_id)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.unwrap_or_else(|| format!("_{grant_account_id}"));
|
|
|
|
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::Administer => {
|
|
acls.insert(Privilege::ReadAcl);
|
|
acls.insert(Privilege::WriteAcl);
|
|
}
|
|
Acl::SchedulingReadFreeBusy => {
|
|
acls.insert(Privilege::ReadFreeBusy);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
acls.into_iter().collect()
|
|
}
|