/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * * Modified by Coffey Labs in 2026 for INBUXA. */ 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> + Send; fn handle_acl_prop_set( &self, access_token: &AccessToken, headers: &RequestHeaders<'_>, request: AclPrincipalPropSet, ) -> impl Future> + Send; fn validate_and_map_aces( &self, access_token: &AccessToken, acl: dav_proto::schema::request::Acl, collection: Collection, ) -> impl Future>> + Send; fn resolve_ace( &self, access_token: &AccessToken, account_id: u32, grants: &ArchivedVec, expand: Option<&PropFind>, ) -> impl Future>> + Send; } pub(crate) trait ResourceAcl { fn validate_and_map_parent_acl( &self, access_token: &AccessToken, is_member: bool, parent_id: Option, check_acls: impl Into> + Send, ) -> crate::Result; } impl DavAclHandler for Server { async fn handle_acl_request( &self, access_token: &AccessToken, headers: &RequestHeaders<'_>, request: dav_proto::schema::request::Acl, ) -> crate::Result { // 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::>(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?; // inbuxa: MT-3: grants stay within the owner's tenant let tenant_id = self .try_account(account_id) .await .caused_by(trc::location!())? .and_then(|owner| owner.id_tenant); for grant in &grants { if self .try_account(grant.account_id) .await .caused_by(trc::location!())? .is_none_or(|grantee| grantee.id_tenant != tenant_id) { return Err(DavError::Condition(DavErrorCondition::new( StatusCode::FORBIDDEN, BaseCondition::AllowedPrincipal, ))); } } 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::() .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::() .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::().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 { 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::>(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::() .caused_by(trc::location!())? .acls } Collection::AddressBook => { &archive .unarchive::() .caused_by(trc::location!())? .acls } Collection::Calendar => { &archive .unarchive::() .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> { 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::::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, expand: Option<&PropFind>, ) -> crate::Result> { 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::::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, check_acls: impl Into> + Send, ) -> crate::Result { 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, is_calendar: bool, ) -> Vec; } impl Privileges for AccessToken { fn current_privilege_set( &self, account_id: u32, grants: &ArchivedVec, is_calendar: bool, ) -> Vec { 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) -> Vec { 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() }