Don't let a group's members share its mailboxes on #146

Merged
jcoffey-dev merged 2 commits from fix/group-mailbox-no-onward-share into main 2026-10-05 21:26:53 +00:00
5 changed files with 76 additions and 4 deletions

No files matched your search

+10
View File
@@ -553,6 +553,16 @@ impl AccessToken {
|| self.inner.access_to.iter().any(|a| a.account_id == account_id) || self.inner.access_to.iter().any(|a| a.account_id == account_id)
} }
/// inbuxa: MA-D0: in the account only because it is a group this token
/// belongs to. Such a member has the group's mailbox but may not share it
/// on: who is in a group is an administrator's decision, and a share
/// would let anyone in.
pub fn is_group_member_only(&self, account_id: u32) -> bool {
self.inner.account_id != account_id
&& self.inner.member_of.contains(&account_id)
&& !self.has_permission(Permission::Impersonate)
}
pub fn is_account_id(&self, account_id: u32) -> bool { pub fn is_account_id(&self, account_id: u32) -> bool {
self.inner.account_id == account_id self.inner.account_id == account_id
} }
+18 -4
View File
@@ -212,7 +212,7 @@ impl<T: SessionStream> Session<T> {
} }
rights rights
} else { } else {
vec![ let mut rights = vec![
Rights::Read, Rights::Read,
Rights::Lookup, Rights::Lookup,
Rights::Insert, Rights::Insert,
@@ -223,8 +223,12 @@ impl<T: SessionStream> Session<T> {
Rights::CreateMailbox, Rights::CreateMailbox,
Rights::DeleteMailbox, Rights::DeleteMailbox,
Rights::Post, Rights::Post,
Rights::Administer, ];
] // inbuxa: MA-D0: a group's members don't share its mailboxes on.
if !access_token.is_group_member_only(mailbox_id.account_id) {
rights.push(Rights::Administer);
}
rights
}; };
trc::event!( trc::event!(
@@ -266,10 +270,20 @@ impl<T: SessionStream> Session<T> {
spawn_op!(data, { spawn_op!(data, {
// Validate mailbox // Validate mailbox
let (mailbox_id, current_mailbox, _) = data let (mailbox_id, current_mailbox, access_token) = data
.get_acl_mailbox(&arguments, true) .get_acl_mailbox(&arguments, true)
.await .await
.imap_ctx(&arguments.tag, trc::location!())?; .imap_ctx(&arguments.tag, trc::location!())?;
// inbuxa: MA-D0: a group's members don't share its mailboxes on.
if access_token.is_group_member_only(mailbox_id.account_id) {
return Err(trc::ImapEvent::Error
.into_err()
.details("This mailbox belongs to a group. Only an administrator can change who has it.")
.code(ResponseCode::NoPerm)
.id(arguments.tag.to_string()));
}
let current_mailbox = current_mailbox let current_mailbox = current_mailbox
.into_deserialized::<email::mailbox::Mailbox>() .into_deserialized::<email::mailbox::Mailbox>()
.imap_ctx(&arguments.tag, trc::location!())?; .imap_ctx(&arguments.tag, trc::location!())?;
+8
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
@@ -14,6 +16,7 @@ use jmap_tools::{Map, Value};
use std::future::Future; use std::future::Future;
use store::ahash::AHashSet; use store::ahash::AHashSet;
use types::{acl::Acl, collection::Collection, keyword::Keyword, special_use::SpecialUse}; use types::{acl::Acl, collection::Collection, keyword::Keyword, special_use::SpecialUse};
use utils::map::bitmap::Bitmap;
use crate::{api::acl::JmapRights, changes::state::JmapCacheState}; use crate::{api::acl::JmapRights, changes::state::JmapCacheState};
@@ -138,6 +141,11 @@ impl MailboxGet for Server {
JmapRights::rights::<Mailbox>( JmapRights::rights::<Mailbox>(
cached_mailbox.acls.as_slice().effective_acl(access_token), cached_mailbox.acls.as_slice().effective_acl(access_token),
) )
} else if access_token.is_group_member_only(account_id) {
// inbuxa: MA-D0: everything but sharing it on.
let mut acl = Bitmap::<Acl>::all();
acl.remove(Acl::Share);
JmapRights::rights::<Mailbox>(acl)
} else { } else {
JmapRights::all_rights::<Mailbox>() JmapRights::all_rights::<Mailbox>()
} }
+9
View File
@@ -613,6 +613,15 @@ impl MailboxSet for Server {
// Refresh ACLs // Refresh ACLs
let current = update.map(|(_, current)| current); let current = update.map(|(_, current)| current);
if has_acl_changes { if has_acl_changes {
// inbuxa: MA-D0: a group's members don't share its mailboxes on.
if ctx.access_token.is_group_member_only(ctx.account_id) {
return Ok(Err(SetError::forbidden()
.with_property(MailboxProperty::ShareWith)
.with_description(
"This mailbox belongs to a group. Only an administrator can change who has it.",
)));
}
if !changes.acls.is_empty() if !changes.acls.is_empty()
&& let Err(err) = self.acl_validate(ctx.account_id, &changes.acls).await && let Err(err) = self.acl_validate(ctx.account_id, &changes.acls).await
{ {
+31
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::utils::server::TestServer; use crate::utils::server::TestServer;
@@ -713,6 +715,35 @@ pub async fn test(test: &TestServer) {
.await, .await,
); );
// inbuxa: MA-D0: a member can't share the group's mailbox on, and isn't
// told it may. Who is in a group is an administrator's decision.
assert_forbidden(
john_client
.set_default_account_id(sales.id_string())
.mailbox_update_acl(&inbox_id, bill.id_string(), [ACL::ReadItems])
.await,
);
assert!(
!john_client
.set_default_account_id(sales.id_string())
.mailbox_get(&inbox_id, [mailbox::Property::MyRights].into())
.await
.unwrap()
.unwrap()
.my_rights()
.unwrap()
.acl_list()
.contains(&ACL::Administer)
);
bill_client.refresh_session().await.unwrap();
assert!(bill_client.session().account(sales.id_string()).is_none());
assert_forbidden(
bill_client
.set_default_account_id(sales.id_string())
.email_get(&email_id, [Property::Subject].into())
.await,
);
// Remove John from the sales group // Remove John from the sales group
admin admin
.registry_update_object( .registry_update_object(