Multi-tenancy: registry reach, links, membership and count limits over JMAP (MT-2, MT-3, MT-6, MT-7, MT-8, MT-11, MT-12, MT-17, MT-18)
Inside a tenant, server-level object types are forbidden and x:Tenant reads return only the caller's own tenant, which it can't change. Registry writes refuse links across tenant boundaries in both directions, give a new principal its domain's tenant, move a domain's principals and DKIM keys with it into a tenant, refuse moves out of a tenant while its people remain, and refuse creates past a tenant's count limits with overQuota and limit.tenant-quota.
This commit is contained in:
@@ -19,6 +19,7 @@ spam-filter = { path = "../spam-filter" }
|
|||||||
email = { path = "../email" }
|
email = { path = "../email" }
|
||||||
groupware = { path = "../groupware" }
|
groupware = { path = "../groupware" }
|
||||||
registry = { path = "../registry" }
|
registry = { path = "../registry" }
|
||||||
|
inbuxa-features = { path = "../features" }
|
||||||
calcard = { version = "0.3" }
|
calcard = { version = "0.3" }
|
||||||
smtp-proto = { version = "0.2" }
|
smtp-proto = { version = "0.2" }
|
||||||
mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] }
|
mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] }
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ impl JmapAuthorization for AccessToken {
|
|||||||
let MethodObject::Registry(object_type) = object else {
|
let MethodObject::Registry(object_type) = object else {
|
||||||
unreachable!()
|
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()
|
object_type.get_permission()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -203,6 +208,11 @@ impl JmapAuthorization for AccessToken {
|
|||||||
let MethodObject::Registry(object_type) = object else {
|
let MethodObject::Registry(object_type) = object else {
|
||||||
unreachable!()
|
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();
|
let set_permissions = object_type.set_permission();
|
||||||
validate_set(
|
validate_set(
|
||||||
s,
|
s,
|
||||||
@@ -295,6 +305,11 @@ impl JmapAuthorization for AccessToken {
|
|||||||
let MethodObject::Registry(object_type) = object else {
|
let MethodObject::Registry(object_type) = object else {
|
||||||
unreachable!()
|
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()
|
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<T: JmapObject>(
|
fn validate_set<T: JmapObject>(
|
||||||
set: &SetRequest<'_, T>,
|
set: &SetRequest<'_, T>,
|
||||||
access_token: &AccessToken,
|
access_token: &AccessToken,
|
||||||
|
|||||||
@@ -198,6 +198,11 @@ impl RegistryGet for Server {
|
|||||||
|
|
||||||
let ids = if let Some(ids) = get.ids.take() {
|
let ids = if let Some(ids) = get.ids.take() {
|
||||||
ids
|
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 {
|
} else {
|
||||||
self.registry()
|
self.registry()
|
||||||
.query::<Vec<Id>>(
|
.query::<Vec<Id>>(
|
||||||
@@ -223,6 +228,11 @@ impl RegistryGet for Server {
|
|||||||
!= object.inner.member_tenant_id())
|
!= object.inner.member_tenant_id())
|
||||||
|| (is_account_filtered
|
|| (is_account_filtered
|
||||||
&& object.inner.account_id() != Some(Id::from(get.account_id)))
|
&& 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);
|
get.not_found(id);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -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
|
// Build response
|
||||||
let mut response = QueryResponseBuilder::new(
|
let mut response = QueryResponseBuilder::new(
|
||||||
results.len(),
|
results.len(),
|
||||||
|
|||||||
@@ -381,6 +381,15 @@ impl RegistrySet for Server {
|
|||||||
new_object.inner.set_member_tenant_id(tenant_id.into());
|
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
|
// Add accountId
|
||||||
if has_account_id {
|
if has_account_id {
|
||||||
new_object.inner.set_account_id(set.account_id.into());
|
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
|
// Validate expressions
|
||||||
if let Some(expressions) = new_object.inner.expression_ctxs() {
|
if let Some(expressions) = new_object.inner.expression_ctxs() {
|
||||||
let mut bp = Bootstrap::new_uninitialized(self.registry().clone());
|
let mut bp = Bootstrap::new_uninitialized(self.registry().clone());
|
||||||
@@ -590,6 +619,16 @@ impl RegistrySet for Server {
|
|||||||
let object_id = match (modification, result) {
|
let object_id = match (modification, result) {
|
||||||
(Modification::Update { id, object }, RegistryWriteResult::Success(_)) => {
|
(Modification::Update { id, object }, RegistryWriteResult::Success(_)) => {
|
||||||
cache_invalidator.process_update(id, &object, &new_object);
|
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 (
|
if let (
|
||||||
ObjectInner::Application(previous),
|
ObjectInner::Application(previous),
|
||||||
ObjectInner::Application(updated),
|
ObjectInner::Application(updated),
|
||||||
|
|||||||
Reference in New Issue
Block a user