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.
This commit is contained in:
2026-09-21 14:56:04 -07:00
parent 840215d109
commit 6c6fe91d0c
3 changed files with 37 additions and 0 deletions
@@ -93,6 +93,17 @@ pub async fn set(data: &Store, tenant_id: u32, policy: &TenantProtocolPolicy) ->
.map(|_| ()) .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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+8
View File
@@ -851,6 +851,14 @@ impl RegistrySet for Server {
if let ObjectInner::MaskedEmail(mask) = &object.inner { if let ObjectInner::MaskedEmail(mask) = &object.inner {
crate::inbuxa::masked_email::destroyed(self, id, mask).await?; 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); cache_invalidator.process_delete(id, &object);
set.response.destroyed.push(id); set.response.destroyed.push(id);
} }
+18
View File
@@ -380,6 +380,24 @@ def tenant_checks(admin, admin_pw, account):
"and its user signs in over IMAP again") "and its user signs in over IMAP again")
check(session_flag(tu, user_pw) == "enabled", "and its session says enabled again (test 13)") 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): def session_flag(user, password):
"""legacyProtocols from the account's urn:inbuxa:jmap capability.""" """legacyProtocols from the account's urn:inbuxa:jmap capability."""