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" }
|
||||
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"] }
|
||||
|
||||
@@ -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<T: JmapObject>(
|
||||
set: &SetRequest<'_, T>,
|
||||
access_token: &AccessToken,
|
||||
|
||||
@@ -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::<Vec<Id>>(
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user