Files
inbuxa-server/crates/dav/src/common/acl.rs
T
jcoffey-dev 6a53d47106 Mark the files this fork changed (AGPL section 5(a))
The AGPL asks a modified version to carry prominent notices saying it was
modified, and giving a date. Publishing the source is the conveyance that
asks for it, so it wants doing before the repository is public rather than
at the release.

Every upstream file the fork changed now says so in its header, beneath the
notice it came with: 164 files, found by diffing against the upstream
snapshot branch rather than by guessing, so the list is what actually
differs. Files the fork wrote itself already carry their own copyright and
need nothing. Upstream's notices are untouched, which its licence requires
and which was already true.

The README says the same thing in prose, since the obligation is on the
work as a whole and not only its Rust files.

Builds unchanged: the server and the test binary both compile.
2026-09-19 23:48:35 -07:00

605 lines
21 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* 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<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?;
// 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::<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()
}