#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.
321 lines
10 KiB
Rust
321 lines
10 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 common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
|
use jmap_proto::{
|
|
error::set::{SetError, SetErrorType},
|
|
object::{JmapRight, JmapSharedObject},
|
|
};
|
|
use jmap_tools::{JsonPointerIter, Key, Map, Property, Value};
|
|
use registry::{schema::prelude::ObjectType, types::id::ObjectId};
|
|
use store::{registry::RegistryQuery, roaring::RoaringBitmap};
|
|
use types::{
|
|
acl::{Acl, AclGrant},
|
|
id::Id,
|
|
};
|
|
use utils::map::bitmap::Bitmap;
|
|
|
|
pub struct JmapRights;
|
|
|
|
impl JmapRights {
|
|
pub fn acl_set<T: JmapSharedObject>(
|
|
value: Value<'_, T::Property, T::Element>,
|
|
) -> Result<Vec<AclGrant>, SetError<T::Property>>
|
|
where
|
|
Id: TryFrom<T::Property>,
|
|
T::Right: TryFrom<T::Property>,
|
|
{
|
|
let mut grants = Vec::new();
|
|
|
|
for (key, value) in value.into_expanded_object() {
|
|
let account_id = key
|
|
.try_into_property()
|
|
.and_then(|p| Id::try_from(p).ok())
|
|
.ok_or_else(|| {
|
|
SetError::invalid_properties()
|
|
.with_property(T::SHARE_WITH_PROPERTY)
|
|
.with_description("Invalid account id.")
|
|
})?
|
|
.document_id();
|
|
|
|
if !grants
|
|
.iter()
|
|
.any(|item: &AclGrant| item.account_id == account_id)
|
|
{
|
|
let acls = Self::map_acls::<T>(value)?;
|
|
if !acls.is_empty() {
|
|
grants.push(AclGrant {
|
|
account_id,
|
|
grants: acls,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(grants)
|
|
}
|
|
|
|
pub fn acl_patch<T: JmapSharedObject>(
|
|
mut grants: Vec<AclGrant>,
|
|
mut path: JsonPointerIter<'_, T::Property>,
|
|
value: Value<'_, T::Property, T::Element>,
|
|
) -> Result<Vec<AclGrant>, SetError<T::Property>>
|
|
where
|
|
Id: TryFrom<T::Property>,
|
|
T::Right: TryFrom<T::Property>,
|
|
{
|
|
let account_id = path
|
|
.next()
|
|
.and_then(|item| item.as_property_key())
|
|
.cloned()
|
|
.and_then(|p| Id::try_from(p).ok())
|
|
.ok_or_else(|| {
|
|
SetError::invalid_properties()
|
|
.with_property(T::SHARE_WITH_PROPERTY)
|
|
.with_description("Invalid account id.")
|
|
})?
|
|
.document_id();
|
|
|
|
if let Some(right) = path.next() {
|
|
if path.next().is_some() {
|
|
return Err(SetError::invalid_properties()
|
|
.with_property(T::SHARE_WITH_PROPERTY)
|
|
.with_description("Invalid path for ACL patch."));
|
|
}
|
|
|
|
let is_set = match value {
|
|
Value::Bool(is_set) => is_set,
|
|
Value::Null => false,
|
|
_ => {
|
|
return Err(SetError::invalid_properties()
|
|
.with_property(T::SHARE_WITH_PROPERTY)
|
|
.with_description("Invalid ACL value."));
|
|
}
|
|
};
|
|
|
|
let acl = right
|
|
.as_property_key()
|
|
.cloned()
|
|
.and_then(|p| T::Right::try_from(p).ok())
|
|
.ok_or_else(|| {
|
|
SetError::invalid_properties()
|
|
.with_property(T::SHARE_WITH_PROPERTY)
|
|
.with_description(format!(
|
|
"Invalid permission {:?}.",
|
|
right.to_cow().unwrap_or_default()
|
|
))
|
|
})?
|
|
.to_acl()
|
|
.iter()
|
|
.copied();
|
|
|
|
if let Some(acl_item) = grants.iter_mut().find(|item| item.account_id == account_id) {
|
|
if is_set {
|
|
acl_item.grants.insert_many(acl);
|
|
} else {
|
|
acl_item.grants.remove_many(acl);
|
|
if acl_item.grants.is_empty() {
|
|
grants.retain(|item| item.account_id != account_id);
|
|
}
|
|
}
|
|
} else if is_set {
|
|
grants.push(AclGrant {
|
|
account_id,
|
|
grants: Bitmap::from_iter(acl),
|
|
});
|
|
}
|
|
} else {
|
|
let acls = Self::map_acls::<T>(value)?;
|
|
if !acls.is_empty() {
|
|
if let Some(acl_item) = grants.iter_mut().find(|item| item.account_id == account_id)
|
|
{
|
|
acl_item.grants = acls;
|
|
} else {
|
|
grants.push(AclGrant {
|
|
account_id,
|
|
grants: acls,
|
|
});
|
|
}
|
|
} else {
|
|
grants.retain(|item| item.account_id != account_id);
|
|
}
|
|
}
|
|
|
|
Ok(grants)
|
|
}
|
|
|
|
fn map_acls<T: JmapSharedObject>(
|
|
value: Value<'_, T::Property, T::Element>,
|
|
) -> Result<Bitmap<Acl>, SetError<T::Property>>
|
|
where
|
|
Id: TryFrom<T::Property>,
|
|
T::Right: TryFrom<T::Property>,
|
|
{
|
|
let mut acls = Bitmap::new();
|
|
|
|
for key in value.into_expanded_boolean_set() {
|
|
acls.insert_many(
|
|
key.as_property()
|
|
.and_then(|p| T::Right::try_from(p.clone()).ok())
|
|
.ok_or_else(|| {
|
|
SetError::invalid_properties()
|
|
.with_property(T::SHARE_WITH_PROPERTY)
|
|
.with_description(format!("Invalid permission {:?}.", key.to_string()))
|
|
})?
|
|
.to_acl()
|
|
.iter()
|
|
.copied(),
|
|
);
|
|
}
|
|
|
|
Ok(acls)
|
|
}
|
|
|
|
pub fn all_rights<T: JmapSharedObject>() -> Value<'static, T::Property, T::Element> {
|
|
let rights = T::Right::all_rights();
|
|
let mut obj = Map::with_capacity(rights.len());
|
|
|
|
for right in rights {
|
|
obj.insert_unchecked(Key::Property((*right).into()), Value::Bool(true));
|
|
}
|
|
|
|
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> {
|
|
let mut obj = Map::with_capacity(3);
|
|
|
|
for right in T::Right::all_rights() {
|
|
obj.insert_unchecked(
|
|
Key::Property((*right).into()),
|
|
Value::Bool(right.to_acl().iter().all(|acl| acls.contains(*acl))),
|
|
);
|
|
}
|
|
|
|
Value::Object(obj)
|
|
}
|
|
|
|
pub fn share_with<T: JmapSharedObject>(
|
|
account_id: u32,
|
|
access_token: &AccessToken,
|
|
grants: &[AclGrant],
|
|
) -> Value<'static, T::Property, T::Element>
|
|
where
|
|
T::Property: From<Id>,
|
|
{
|
|
if access_token.is_member(account_id)
|
|
|| grants.effective_acl(access_token).contains(Acl::Share)
|
|
{
|
|
let mut share_with = Map::with_capacity(grants.len());
|
|
for grant in grants {
|
|
share_with.insert_unchecked(
|
|
Key::Property(Id::from(grant.account_id).into()),
|
|
Self::rights::<T>(grant.grants),
|
|
);
|
|
}
|
|
|
|
Value::Object(share_with)
|
|
} else {
|
|
Value::Null
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait JmapAcl {
|
|
fn acl_validate(
|
|
&self,
|
|
account_id: u32,
|
|
grants: &[AclGrant],
|
|
) -> impl Future<Output = Result<(), ShareValidationError>> + Send;
|
|
}
|
|
|
|
pub enum ShareValidationError {
|
|
MaxSharesExceeded(usize),
|
|
InvalidAccountId(Id),
|
|
}
|
|
|
|
impl JmapAcl for Server {
|
|
async fn acl_validate(
|
|
&self,
|
|
account_id: u32,
|
|
grants: &[AclGrant],
|
|
) -> Result<(), ShareValidationError> {
|
|
if grants.len() > self.core.groupware.max_shares_per_item {
|
|
return Err(ShareValidationError::MaxSharesExceeded(
|
|
self.core.groupware.max_shares_per_item,
|
|
));
|
|
}
|
|
|
|
let principal_ids = self
|
|
.registry()
|
|
.query::<RoaringBitmap>(RegistryQuery::new(ObjectType::Account))
|
|
.await
|
|
.unwrap_or_default();
|
|
|
|
// inbuxa: MT-3: grants stay within the owner's tenant
|
|
let tenant_id = self
|
|
.try_account(account_id)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|owner| owner.id_tenant);
|
|
|
|
for grant in grants {
|
|
if !principal_ids.contains(grant.account_id)
|
|
|| self
|
|
.try_account(grant.account_id)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.is_none_or(|grantee| grantee.id_tenant != tenant_id)
|
|
{
|
|
return Err(ShareValidationError::InvalidAccountId(Id::from(
|
|
grant.account_id,
|
|
)));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<T: Property> From<ShareValidationError> for SetError<T> {
|
|
fn from(err: ShareValidationError) -> Self {
|
|
match err {
|
|
ShareValidationError::MaxSharesExceeded(max) => SetError::invalid_properties()
|
|
.with_description(format!(
|
|
"Maximum number of shares per item exceeded (max: {max})"
|
|
)),
|
|
// inbuxa: MT-3: the same error whether the account is missing or
|
|
// in another tenant, so it never confirms the account exists
|
|
ShareValidationError::InvalidAccountId(id) => {
|
|
SetError::new(SetErrorType::InvalidForeignKey)
|
|
.with_object_id(ObjectId::new(ObjectType::Account, id))
|
|
.with_description(format!("Account id {id} is invalid."))
|
|
}
|
|
}
|
|
}
|
|
}
|