diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 8f40ccb..a55745e 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -19,6 +19,7 @@ spam-filter = { path = "../spam-filter" } email = { path = "../email" } groupware = { path = "../groupware" } registry = { path = "../registry" } +inbuxa-features = { path = "../features" } calcard = { version = "0.3" } smtp-proto = { version = "0.2" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } diff --git a/crates/jmap/src/api/auth.rs b/crates/jmap/src/api/auth.rs index 043411c..110e2d2 100644 --- a/crates/jmap/src/api/auth.rs +++ b/crates/jmap/src/api/auth.rs @@ -89,6 +89,11 @@ impl JmapAuthorization for AccessToken { let MethodObject::Registry(object_type) = object else { unreachable!() }; + // inbuxa: MT-2: server-level objects are out of a tenant's reach + assert_tenant_reach( + self, + inbuxa_features::tenancy::reach::can_read(object_type), + )?; object_type.get_permission() } }, @@ -203,6 +208,11 @@ impl JmapAuthorization for AccessToken { let MethodObject::Registry(object_type) = object else { unreachable!() }; + // inbuxa: MT-2, MT-12: server-level objects are out of a tenant's reach + assert_tenant_reach( + self, + inbuxa_features::tenancy::reach::can_write(object_type), + )?; let set_permissions = object_type.set_permission(); validate_set( s, @@ -295,6 +305,11 @@ impl JmapAuthorization for AccessToken { let MethodObject::Registry(object_type) = object else { unreachable!() }; + // inbuxa: MT-2: server-level objects are out of a tenant's reach + assert_tenant_reach( + self, + inbuxa_features::tenancy::reach::can_read(object_type), + )?; object_type.query_permission() } }, @@ -316,6 +331,17 @@ impl JmapAuthorization for AccessToken { } } +// inbuxa: MT-2 +fn assert_tenant_reach(access_token: &AccessToken, reachable: bool) -> trc::Result<()> { + if reachable || access_token.tenant_id().is_none() { + Ok(()) + } else { + Err(trc::JmapEvent::Forbidden + .into_err() + .details("You are not authorized to perform this action")) + } +} + fn validate_set( set: &SetRequest<'_, T>, access_token: &AccessToken, diff --git a/crates/jmap/src/registry/get.rs b/crates/jmap/src/registry/get.rs index 074eefa..172d512 100644 --- a/crates/jmap/src/registry/get.rs +++ b/crates/jmap/src/registry/get.rs @@ -198,6 +198,11 @@ impl RegistryGet for Server { let ids = if let Some(ids) = get.ids.take() { ids + } else if object_type == ObjectType::Tenant + && let Some(tenant_id) = access_token.tenant_id() + { + // inbuxa: MT-12: inside a tenant, only its own tenant object + vec![Id::from(tenant_id)] } else { self.registry() .query::>( @@ -223,6 +228,11 @@ impl RegistryGet for Server { != object.inner.member_tenant_id()) || (is_account_filtered && object.inner.account_id() != Some(Id::from(get.account_id))) + // inbuxa: MT-12: inside a tenant, only its own tenant object + || (object_type == ObjectType::Tenant + && access_token + .tenant_id() + .is_some_and(|tenant_id| tenant_id != id.document_id())) { get.not_found(id); continue; diff --git a/crates/jmap/src/registry/query.rs b/crates/jmap/src/registry/query.rs index 22ce60f..6e773aa 100644 --- a/crates/jmap/src/registry/query.rs +++ b/crates/jmap/src/registry/query.rs @@ -265,6 +265,14 @@ impl RegistryQuery for Server { } }; + // inbuxa: MT-12: inside a tenant, only its own tenant object + let mut results = results; + if object_type == ObjectType::Tenant + && let Some(tenant_id) = access_token.tenant_id() + { + results.retain(|id| id.document_id() == tenant_id); + } + // Build response let mut response = QueryResponseBuilder::new( results.len(), diff --git a/crates/jmap/src/registry/set.rs b/crates/jmap/src/registry/set.rs index cc90454..dc83811 100644 --- a/crates/jmap/src/registry/set.rs +++ b/crates/jmap/src/registry/set.rs @@ -381,6 +381,15 @@ impl RegistrySet for Server { new_object.inner.set_member_tenant_id(tenant_id.into()); } + // inbuxa: MT-7: a principal takes its domain's tenant + if can_set_tenant { + inbuxa_features::tenancy::writes::default_tenant( + self.registry(), + &mut new_object, + ) + .await?; + } + // Add accountId if has_account_id { new_object.inner.set_account_id(set.account_id.into()); @@ -526,6 +535,26 @@ impl RegistrySet for Server { } }; + // inbuxa: MT-3, MT-8, MT-17 + let (stored_id, stored) = match &modification { + Modification::Update { id, object } => (Some(*id), Some(object)), + Modification::Create { .. } => (None, None), + }; + let after_save = match inbuxa_features::tenancy::writes::check( + self.registry(), + stored_id, + stored, + &new_object, + ) + .await? + { + Ok(after_save) => after_save, + Err(err) => { + set.failed(modification, err); + continue 'outer; + } + }; + // Validate expressions if let Some(expressions) = new_object.inner.expression_ctxs() { let mut bp = Bootstrap::new_uninitialized(self.registry().clone()); @@ -590,6 +619,16 @@ impl RegistrySet for Server { let object_id = match (modification, result) { (Modification::Update { id, object }, RegistryWriteResult::Success(_)) => { cache_invalidator.process_update(id, &object, &new_object); + // inbuxa: MT-8: what moves with a domain follows it + for (id, old, new) in inbuxa_features::tenancy::writes::after_save( + &self.core.storage.data, + self.registry(), + after_save, + ) + .await? + { + cache_invalidator.process_update(id, &old, &new); + } if let ( ObjectInner::Application(previous), ObjectInner::Application(updated),