Don't let a group's members share its calendars, address books or files
github/ci (branch) GitHub Actions
ci / github (pull_request) Skipped
ci / fork-checks (pull_request) Successful in 14s
ci / build (pull_request) Canceled after 25m26s

#146 stopped a group's members sharing its mailboxes on. The same
shortcut lets them through everywhere else a group owns things: a
member counts as the account's owner, so Calendar/set, AddressBook/set
and FileNode/set skip the share check, and so does the WebDAV ACL
method. Who has what a group owns is decided by who is in the group.

For a member through a group only (is_group_member_only):

- Calendar/set, AddressBook/set and FileNode/set refuse a shareWith
  change as forbidden, on create and update; for files at the top of
  the account too, not only inside a folder;
- the DAV ACL method answers 403 on the group's calendars, address
  books and files;
- myRights reports mayShare false (JmapRights::owner_rights), and the
  DAV current-user-privilege-set leaves out all and write-acl.

Reading who something is shared with is unchanged, as in JMAP.

Tests: a new jmap::group_share module has a member create with a
share, create without one (and check myRights), share afterwards, and
an outsider reach each kind; the WebDAV ACL test has a member try the
ACL method on the group's folders; the IMAP ACL test now checks #146's
SETACL refusal, which had no test of its own. jmap_tests, webdav_tests
and imap_tests pass (RocksDB). specs/multi-account.md MA-D0.
This commit is contained in:
jcoffey-dev committed 2026-10-05 15:03:15 -07:00
1 parent 5f6548bfdd
commit 58d2804278
12 files changed
+267 -4

No files matched your search

+11 -1
View File
@@ -133,6 +133,10 @@ impl DavAclHandler for Server {
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// inbuxa: MA-D0: a group's members don't share what it owns on.
if access_token.is_group_member_only(account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate ACEs
let grants = self
@@ -565,7 +569,13 @@ impl Privileges for AccessToken {
grants: &ArchivedVec<ArchivedAclGrant>,
is_calendar: bool,
) -> Vec<Privilege> {
if self.is_member(account_id) {
if self.is_group_member_only(account_id) {
// inbuxa: MA-D0: everything but sharing it on.
Privilege::all(is_calendar)
.into_iter()
.filter(|privilege| !matches!(privilege, Privilege::All | Privilege::WriteAcl))
.collect()
} else if self.is_member(account_id) {
Privilege::all(is_calendar)
} else {
current_user_privilege_set(grants.effective_acl(self))
+3 -1
View File
@@ -2,6 +2,8 @@
* 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 crate::{api::acl::JmapRights, changes::state::JmapCacheState};
@@ -180,7 +182,7 @@ impl AddressBookGet for Server {
address_book.acls.effective_acl(access_token),
)
} else {
JmapRights::all_rights::<addressbook::AddressBook>()
JmapRights::owner_rights::<addressbook::AddressBook>(access_token, account_id)
},
);
}
+16
View File
@@ -101,6 +101,14 @@ impl AddressBookSet for Server {
continue 'create;
}
// inbuxa: MA-D0: a group's members don't share what it owns on.
if !address_book.acls.is_empty() && access_token.is_group_member_only(account_id) {
response.not_created.append(
id,
SetError::forbidden().with_description("This belongs to a group. Only an administrator can change who has it."),
);
continue 'create;
}
// Validate ACLs
if !address_book.acls.is_empty() {
if let Err(err) = self.acl_validate(account_id, &address_book.acls).await {
@@ -203,6 +211,14 @@ impl AddressBookSet for Server {
continue 'update;
}
}
// inbuxa: MA-D0: a group's members don't share what it owns on.
if has_acl_changes && access_token.is_group_member_only(account_id) {
response.not_updated.append(
id,
SetError::forbidden().with_description("This belongs to a group. Only an administrator can change who has it."),
);
continue 'update;
}
if has_acl_changes {
if let Err(err) = self.acl_validate(account_id, &new_address_book.acls).await {
response.not_updated.append(id, err.into());
+15
View File
@@ -187,6 +187,21 @@ impl JmapRights {
Value::Object(obj)
}
/// inbuxa: MA-D0: an owner's rights, which for a group's member are
/// everything but sharing it on.
pub fn owner_rights<T: JmapSharedObject>(
access_token: &AccessToken,
account_id: u32,
) -> Value<'static, T::Property, T::Element> {
if access_token.is_group_member_only(account_id) {
let mut acl = Bitmap::<Acl>::all();
acl.remove(Acl::Share);
Self::rights::<T>(acl)
} else {
Self::all_rights::<T>()
}
}
pub fn rights<T: JmapSharedObject>(
acls: Bitmap<Acl>,
) -> Value<'static, T::Property, T::Element> {
+3 -1
View File
@@ -2,6 +2,8 @@
* 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 crate::{api::acl::JmapRights, calendar::Availability, changes::state::JmapCacheState};
@@ -253,7 +255,7 @@ impl CalendarGet for Server {
calendar.acls.effective_acl(access_token),
)
} else {
JmapRights::all_rights::<calendar::Calendar>()
JmapRights::owner_rights::<calendar::Calendar>(access_token, account_id)
},
);
}
+16
View File
@@ -105,6 +105,14 @@ impl CalendarSet for Server {
continue 'create;
}
// inbuxa: MA-D0: a group's members don't share what it owns on.
if !calendar.acls.is_empty() && access_token.is_group_member_only(account_id) {
response.not_created.append(
id,
SetError::forbidden().with_description("This belongs to a group. Only an administrator can change who has it."),
);
continue 'create;
}
// Validate ACLs
if !calendar.acls.is_empty() {
if let Err(err) = self.acl_validate(account_id, &calendar.acls).await {
@@ -207,6 +215,14 @@ impl CalendarSet for Server {
continue 'update;
}
}
// inbuxa: MA-D0: a group's members don't share what it owns on.
if has_acl_changes && access_token.is_group_member_only(account_id) {
response.not_updated.append(
id,
SetError::forbidden().with_description("This belongs to a group. Only an administrator can change who has it."),
);
continue 'update;
}
if has_acl_changes {
if let Err(err) = self.acl_validate(account_id, &new_calendar.acls).await {
response.not_updated.append(id, err.into());
+3 -1
View File
@@ -2,6 +2,8 @@
* 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 crate::{api::acl::JmapRights, changes::state::JmapCacheState};
@@ -172,7 +174,7 @@ impl FileNodeGet for Server {
file_node.acls.effective_acl(access_token),
)
} else {
JmapRights::all_rights::<file_node::FileNode>()
JmapRights::owner_rights::<file_node::FileNode>(access_token, account_id)
},
);
}
+18
View File
@@ -250,6 +250,16 @@ impl FileNodeSet for Server {
},
};
// inbuxa: MA-D0: a group's members don't share what it owns on,
// at the top of its files as anywhere else
if has_acl_changes && access_token.is_group_member_only(account_id) {
response.not_created.append(
id,
SetError::forbidden().with_description("This belongs to a group. Only an administrator can change who has it."),
);
continue 'create;
}
// Inherit ACLs from parent
if file_node.parent_id > 0 {
let parent_id = file_node.parent_id - 1;
@@ -509,6 +519,14 @@ impl FileNodeSet for Server {
continue 'update;
}
}
// inbuxa: MA-D0: a group's members don't share what it owns on.
if has_acl_changes && access_token.is_group_member_only(account_id) {
response.not_updated.append(
id,
SetError::forbidden().with_description("This belongs to a group. Only an administrator can change who has it."),
);
continue 'update;
}
if has_acl_changes {
if let Err(err) = self.acl_validate(account_id, &new_file_node.acls).await {
response.not_updated.append(id, err.into());