From 6c6fe91d0cb41446914451a299b86f431fc472c0 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Mon, 21 Sep 2026 14:56:04 -0700 Subject: [PATCH] A deleted tenant's legacy protocols switch goes with it Deleting a tenant now also removes its stored inbuxa:TenantProtocolPolicy, in the same place the registry's other per-type clean-ups run. Without it the row outlived the tenant, and a tenant that later came to have the same id would have started with legacy protocols off. The e2e deletes a tenant whose switch a server administrator had turned off, and would check that a new tenant with the same id starts with them on. On this build the registry hands out a fresh id instead ("d" after "c"), so the reuse -- and with it the removal -- isn't observable over JMAP; the test says so rather than passing silently. The risk it guards was therefore smaller than feared, and the change is mostly about not leaving an orphaned row behind. All 72 checks pass. --- .../src/security/tenant_protocol_policy.rs | 11 +++++++++++ crates/jmap/src/registry/set.rs | 8 ++++++++ tests/e2e/legacy_protocols.py | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/crates/features/src/security/tenant_protocol_policy.rs b/crates/features/src/security/tenant_protocol_policy.rs index 4bdd93d..81e72d9 100644 --- a/crates/features/src/security/tenant_protocol_policy.rs +++ b/crates/features/src/security/tenant_protocol_policy.rs @@ -93,6 +93,17 @@ pub async fn set(data: &Store, tenant_id: u32, policy: &TenantProtocolPolicy) -> .map(|_| ()) } +/// Forgets a tenant's switch, when the tenant is deleted. Otherwise a tenant +/// that came to have the same id would start with the old one's switch. +pub async fn remove(data: &Store, tenant_id: u32) -> trc::Result<()> { + let mut batch = BatchBuilder::new(); + batch.clear(key(tenant_id)); + data.write(batch.build_all()) + .await + .caused_by(trc::location!()) + .map(|_| ()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/jmap/src/registry/set.rs b/crates/jmap/src/registry/set.rs index a7f59ab..d7a1634 100644 --- a/crates/jmap/src/registry/set.rs +++ b/crates/jmap/src/registry/set.rs @@ -851,6 +851,14 @@ impl RegistrySet for Server { if let ObjectInner::MaskedEmail(mask) = &object.inner { crate::inbuxa::masked_email::destroyed(self, id, mask).await?; } + // inbuxa: legacy-protocols, a tenant's switch goes with it + if matches!(object.inner, ObjectInner::Tenant(_)) { + inbuxa_features::security::tenant_protocol_policy::remove( + &self.core.storage.data, + id.document_id(), + ) + .await?; + } cache_invalidator.process_delete(id, &object); set.response.destroyed.push(id); } diff --git a/tests/e2e/legacy_protocols.py b/tests/e2e/legacy_protocols.py index 27fd509..a27706f 100755 --- a/tests/e2e/legacy_protocols.py +++ b/tests/e2e/legacy_protocols.py @@ -380,6 +380,24 @@ def tenant_checks(admin, admin_pw, account): "and its user signs in over IMAP again") check(session_flag(tu, user_pw) == "enabled", "and its session says enabled again (test 13)") + # A deleted tenant's switch goes with it, so a tenant that later gets the + # same id doesn't start with legacy protocols off. + sget = lambda ids: one(admin, admin_pw, "inbuxa:TenantProtocolPolicy/get", + {"accountId": account, "ids": ids}) + one(admin, admin_pw, "inbuxa:TenantProtocolPolicy/set", + {"accountId": account, "update": {t2: {"legacyProtocols": "disabled"}}}) + check(sget([t2])[1]["list"][0]["legacyProtocols"] == "disabled", + "a server admin turns another tenant's switch off") + res = one(admin, admin_pw, "x:Tenant/set", {"destroy": [t2]}) + check(t2 in (res[1].get("destroyed") or []), "that tenant can be deleted") + t3 = created(one(admin, admin_pw, "x:Tenant/set", {"create": {"t": {"name": "legacy-t3"}}}), + "t", "third tenant") + if t3 == t2: + check(sget([t3])[1]["list"][0]["legacyProtocols"] == "enabled", + "a new tenant with the deleted one's id starts with legacy protocols on") + else: + print(f" (the registry gave the new tenant a fresh id, {t3} not {t2}: reuse not observable)") + def session_flag(user, password): """legacyProtocols from the account's urn:inbuxa:jmap capability."""