/* * 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 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( value: Value<'_, T::Property, T::Element>, ) -> Result, SetError> where Id: TryFrom, T::Right: TryFrom, { 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::(value)?; if !acls.is_empty() { grants.push(AclGrant { account_id, grants: acls, }); } } } Ok(grants) } pub fn acl_patch( mut grants: Vec, mut path: JsonPointerIter<'_, T::Property>, value: Value<'_, T::Property, T::Element>, ) -> Result, SetError> where Id: TryFrom, T::Right: TryFrom, { 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::(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( value: Value<'_, T::Property, T::Element>, ) -> Result, SetError> where Id: TryFrom, T::Right: TryFrom, { 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() -> 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) } pub fn rights( acls: Bitmap, ) -> 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( account_id: u32, access_token: &AccessToken, grants: &[AclGrant], ) -> Value<'static, T::Property, T::Element> where T::Property: From, { 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::(grant.grants), ); } Value::Object(share_with) } else { Value::Null } } } pub trait JmapAcl { fn acl_validate( &self, account_id: u32, grants: &[AclGrant], ) -> impl Future> + 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::(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 From for SetError { 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.")) } } } }