feat: define governed tenant erasure contracts
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-24 16:00:06 +02:00
parent 9cb2080938
commit 9a3008002d
12 changed files with 725 additions and 20 deletions
+51 -9
View File
@@ -344,6 +344,18 @@ class ModuleSystemTests(unittest.TestCase):
self.assertTrue(scopes_grant_compatible(["access:membership:read"], "admin:users:read"))
self.assertTrue(scopes_grant_compatible(["admin:users:read"], "access:membership:read"))
self.assertTrue(scopes_grant_compatible(["access:tenant:read"], "system:tenants:read"))
self.assertTrue(
scopes_grant_compatible(
["access:tenant:erase"],
"system:tenants:erase",
)
)
self.assertFalse(
scopes_grant_compatible(
["system:tenants:write"],
"system:tenants:erase",
)
)
self.assertTrue(scopes_grant_compatible(["system:*"], "access:tenant:read"))
self.assertTrue(
scopes_grant_compatible(
@@ -1015,8 +1027,10 @@ finally:
json={"mode": "destroy", "reason": "not supported"},
)
self.assertEqual(409, destructive.status_code, destructive.text)
issue_codes = {item["code"] for item in destructive.json()["detail"]["plan"]["issues"]}
self.assertIn("tenant_data_present", issue_codes)
self.assertIn(
"Direct destructive deletion is disabled",
destructive.json()["detail"]["message"],
)
with database.session() as session:
empty_tenant = Tenant(
@@ -1029,15 +1043,43 @@ finally:
session.commit()
empty_tenant_id = empty_tenant.id
destroyed = client.request(
"DELETE",
f"/api/v1/admin/tenants/{empty_tenant_id}",
erasure_policy = client.patch(
"/api/v1/admin/tenant-erasure-policy",
headers=headers,
json={"mode": "destroy", "reason": "empty tenant cleanup"},
json={
"production_profile": False,
"required_approvals": 1,
"preview_ttl_seconds": 900,
"recent_authentication_seconds": 900,
},
)
self.assertEqual(200, destroyed.status_code, destroyed.text)
self.assertEqual("destroy", destroyed.json()["plan"]["action"])
self.assertTrue(destroyed.json()["plan"]["destructive_supported"])
self.assertEqual(200, erasure_policy.status_code, erasure_policy.text)
erasure_preview = client.post(
f"/api/v1/admin/tenants/{empty_tenant_id}/erasure-operations",
headers=headers,
json={
"idempotency_key": f"empty-destroy-{name}",
"reason": "empty tenant cleanup",
},
)
self.assertEqual(201, erasure_preview.status_code, erasure_preview.text)
self.assertTrue(erasure_preview.json()["preview"]["allowed"])
operation_id = erasure_preview.json()["id"]
approved_erasure = client.post(
f"/api/v1/admin/tenants/{empty_tenant_id}/erasure-operations/{operation_id}/approve",
headers=headers,
json={"confirmation": f"empty-destroy-{name}"},
)
self.assertEqual(200, approved_erasure.status_code, approved_erasure.text)
self.assertEqual("ready", approved_erasure.json()["state"])
executed_erasure = client.post(
f"/api/v1/admin/tenants/{empty_tenant_id}/erasure-operations/{operation_id}/execute",
headers=headers,
json={"confirmation": f"empty-destroy-{name}"},
)
self.assertEqual(200, executed_erasure.status_code, executed_erasure.text)
self.assertEqual("completed", executed_erasure.json()["state"])
self.assertIsNone(executed_erasure.json()["reason"])
retired = client.request(
"DELETE",