Files
stalwart/crates/dav/src/common/acl.rs
2025-09-22 10:01:31 +02:00

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()
}