feat(postbox): enforce unified inbox separation

This commit is contained in:
2026-08-20 04:22:29 +02:00
parent 174ee97719
commit 41ea8d8e23
16 changed files with 1029 additions and 25 deletions
+207
View File
@@ -546,6 +546,7 @@ class PostboxServiceTests(unittest.TestCase):
encryption_profile: str = "plaintext_v1",
encryption_vault_id: str | None = None,
protection_policy: dict[str, object] | None = None,
grouping_policy: dict[str, object] | None = None,
) -> Postbox:
return self.service.create_exact_postbox(
session,
@@ -560,6 +561,7 @@ class PostboxServiceTests(unittest.TestCase):
encryption_profile=encryption_profile,
encryption_vault_id=encryption_vault_id,
protection_policy=protection_policy,
grouping_policy=grouping_policy,
)
def _routing_policy(
@@ -2091,6 +2093,211 @@ class PostboxServiceTests(unittest.TestCase):
}
self.assertEqual({parent.id, child.id}, visible_ids)
def test_grouping_policy_enforces_function_and_classification_separation(
self,
) -> None:
child_assignment = OrganizationFunctionAssignmentRef(
id="assignment-child",
tenant_id="tenant-1",
identity_id="identity-1",
account_id="account-1",
function_id="function-child",
organization_unit_id="unit-child",
source="delegated",
delegated_from_assignment_id="assignment-1",
)
self.idm.assignments.extend((self.assignment, child_assignment))
privileged_actor = replace(
self.actor,
authorized_classifications=frozenset(
{"public", "internal", "confidential"}
),
)
with Session(self.engine) as session:
parent = self._create_exact(
session,
grouping_policy={
"mode": "same_classification",
"reason": "Confidential responsibilities remain partitioned.",
},
)
child = self.service.create_exact_postbox(
session,
tenant_id="tenant-1",
name="Service Desk / Delegated complaints",
organization_unit_id="unit-child",
function_id="function-child",
address_key=None,
description=None,
classification="confidential",
actor_id="admin-1",
)
with self.assertRaisesRegex(
PostboxError,
"classification_separation_required",
):
self.service.save_grouping(
session,
tenant_id="tenant-1",
actor=privileged_actor,
grouping_id=None,
name="Mixed classification",
is_default=False,
postbox_ids=(parent.id, child.id),
)
grouping = self.service.save_grouping(
session,
tenant_id="tenant-1",
actor=privileged_actor,
grouping_id=None,
name="Same responsibility",
is_default=True,
postbox_ids=(parent.id,),
)
session.commit()
self.service.update_grouping_policy(
session,
tenant_id="tenant-1",
postbox_id=parent.id,
grouping_policy={
"mode": "separate",
"reason": "This function must remain a dedicated inbox.",
},
actor_id="admin-1",
expected_revision=parent.resource_revision,
)
session.commit()
with self.assertRaisesRegex(PostboxError, "source_requires_separation"):
self.service.list_messages(
session,
tenant_id="tenant-1",
postbox_ids=(parent.id, child.id),
actor=privileged_actor,
)
self.assertEqual(
(parent.id,),
tuple(source.postbox_id for source in grouping.sources),
)
def test_grouping_pagination_is_stable_and_delegation_expiry_hides_source(
self,
) -> None:
delegated = OrganizationFunctionAssignmentRef(
id="assignment-child",
tenant_id="tenant-1",
identity_id="identity-1",
account_id="account-1",
function_id="function-child",
organization_unit_id="unit-child",
source="delegated",
delegated_from_assignment_id="assignment-1",
)
self.idm.assignments.extend((self.assignment, delegated))
with Session(self.engine) as session:
parent = self._create_exact(session)
child = self.service.create_exact_postbox(
session,
tenant_id="tenant-1",
name="Service Desk / Delegated intake",
organization_unit_id="unit-child",
function_id="function-child",
address_key=None,
description=None,
classification="internal",
actor_id="admin-1",
)
grouping = self.service.save_grouping(
session,
tenant_id="tenant-1",
actor=self.actor,
grouping_id=None,
name="Delegated work",
is_default=True,
postbox_ids=(parent.id, child.id),
)
focused_grouping = self.service.save_grouping(
session,
tenant_id="tenant-1",
actor=self.actor,
grouping_id=None,
name="Delegated intake only",
is_default=False,
postbox_ids=(child.id,),
)
self.assertEqual(
(child.id,),
tuple(source.postbox_id for source in focused_grouping.sources),
)
for index, postbox in enumerate((parent, child, parent, child), start=1):
self.service.deliver(
session,
PostboxDeliveryRequest(
tenant_id="tenant-1",
target=PostboxTargetRef(postbox_id=postbox.id),
producer_module="tests",
producer_resource_type="stable_page",
producer_resource_id=str(index),
idempotency_key=f"stable-page-{index}",
subject=f"Message {index}",
classification="internal",
),
)
boundary = utc_now()
session.query(PostboxMessage).update(
{PostboxMessage.delivered_at: boundary}
)
session.commit()
first = self.service.list_messages(
session,
tenant_id="tenant-1",
postbox_ids=(parent.id, child.id),
actor=self.actor,
limit=2,
offset=0,
)
second = self.service.list_messages(
session,
tenant_id="tenant-1",
postbox_ids=(parent.id, child.id),
actor=self.actor,
limit=2,
offset=2,
)
repeated = self.service.list_messages(
session,
tenant_id="tenant-1",
postbox_ids=(parent.id, child.id),
actor=self.actor,
limit=2,
offset=0,
)
self.assertEqual(
[item.id for item in first], [item.id for item in repeated]
)
self.assertFalse({item.id for item in first} & {item.id for item in second})
self.idm.assignments = [
self.assignment,
replace(delegated, status="expired"),
]
visible_ids = {
item.id
for item in self.service.list_visible_postboxes(
session,
tenant_id="tenant-1",
actor=self.actor,
)
}
self.assertEqual({parent.id}, visible_ids)
self.assertEqual(
(parent.id, child.id),
tuple(source.postbox_id for source in grouping.sources),
)
if __name__ == "__main__":
unittest.main()