feat(postbox): add governed content protection profiles
This commit is contained in:
+13
-6
@@ -38,6 +38,10 @@ class PostboxMigrationTests(unittest.TestCase):
|
||||
"govoplan_postbox.backend.migrations.versions."
|
||||
"f2a5c8e1b4d7_v015_portal_visibility"
|
||||
)
|
||||
transition_migration = importlib.import_module(
|
||||
"govoplan_postbox.backend.migrations.versions."
|
||||
"a7c1e4f8b2d6_v016_protection_transitions"
|
||||
)
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
@@ -49,6 +53,7 @@ class PostboxMigrationTests(unittest.TestCase):
|
||||
protection_original = protection_migration.op
|
||||
scope_original = scope_migration.op
|
||||
portal_original = portal_migration.op
|
||||
transition_original = transition_migration.op
|
||||
migration.op = operations
|
||||
route_migration.op = operations
|
||||
occ_migration.op = operations
|
||||
@@ -56,6 +61,7 @@ class PostboxMigrationTests(unittest.TestCase):
|
||||
protection_migration.op = operations
|
||||
scope_migration.op = operations
|
||||
portal_migration.op = operations
|
||||
transition_migration.op = operations
|
||||
try:
|
||||
migration.upgrade()
|
||||
route_migration.upgrade()
|
||||
@@ -64,11 +70,14 @@ class PostboxMigrationTests(unittest.TestCase):
|
||||
protection_migration.upgrade()
|
||||
scope_migration.upgrade()
|
||||
portal_migration.upgrade()
|
||||
transition_migration.upgrade()
|
||||
tables = set(inspect(connection).get_table_names())
|
||||
self.assertIn("postboxes", tables)
|
||||
self.assertIn("postbox_messages", tables)
|
||||
self.assertIn("postbox_deliveries", tables)
|
||||
self.assertIn("postbox_access_events", tables)
|
||||
self.assertIn("postbox_protection_transitions", tables)
|
||||
self.assertIn("postbox_protection_transition_items", tables)
|
||||
message_columns = {
|
||||
column["name"]
|
||||
for column in inspect(connection).get_columns(
|
||||
@@ -120,15 +129,12 @@ class PostboxMigrationTests(unittest.TestCase):
|
||||
)
|
||||
route_columns = {
|
||||
column["name"]
|
||||
for column in inspect(connection).get_columns(
|
||||
"postbox_routes"
|
||||
)
|
||||
for column in inspect(connection).get_columns("postbox_routes")
|
||||
}
|
||||
self.assertTrue(
|
||||
{"execute_after", "processed_at"}.issubset(
|
||||
route_columns
|
||||
)
|
||||
{"execute_after", "processed_at"}.issubset(route_columns)
|
||||
)
|
||||
transition_migration.downgrade()
|
||||
portal_migration.downgrade()
|
||||
scope_migration.downgrade()
|
||||
protection_migration.downgrade()
|
||||
@@ -151,6 +157,7 @@ class PostboxMigrationTests(unittest.TestCase):
|
||||
protection_migration.op = protection_original
|
||||
scope_migration.op = scope_original
|
||||
portal_migration.op = portal_original
|
||||
transition_migration.op = transition_original
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
+368
-51
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
from datetime import timedelta
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
@@ -49,6 +51,8 @@ from govoplan_postbox.backend.db.models import (
|
||||
PostboxMessage,
|
||||
PostboxMessageReceipt,
|
||||
PostboxParticipant,
|
||||
PostboxProtectionTransition,
|
||||
PostboxProtectionTransitionItem,
|
||||
PostboxRoute,
|
||||
PostboxTemplate,
|
||||
PostboxTemplateRevision,
|
||||
@@ -64,6 +68,8 @@ POSTBOX_TABLES = (
|
||||
Postbox.__table__,
|
||||
PostboxBinding.__table__,
|
||||
PostboxMessage.__table__,
|
||||
PostboxProtectionTransition.__table__,
|
||||
PostboxProtectionTransitionItem.__table__,
|
||||
PostboxParticipant.__table__,
|
||||
PostboxAttachmentReference.__table__,
|
||||
PostboxDelivery.__table__,
|
||||
@@ -87,7 +93,9 @@ class FakeIdentityDirectory:
|
||||
)
|
||||
|
||||
def identities_for_accounts(self, account_ids):
|
||||
return tuple(self.identity_for_account(account_id) for account_id in account_ids)
|
||||
return tuple(
|
||||
self.identity_for_account(account_id) for account_id in account_ids
|
||||
)
|
||||
|
||||
def accounts_for_identity(self, identity_id: str):
|
||||
return ()
|
||||
@@ -335,8 +343,7 @@ class FakeOrganizationDirectory:
|
||||
if function.function_type_id == function_type_id
|
||||
and (
|
||||
not organization_unit_ids
|
||||
or function.organization_unit_id
|
||||
in organization_unit_ids
|
||||
or function.organization_unit_id in organization_unit_ids
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -355,8 +362,7 @@ class FakeOrganizationDirectory:
|
||||
matches=tuple(
|
||||
unit
|
||||
for unit in self.units.values()
|
||||
if unit.tenant_id == tenant_id
|
||||
and unit.unit_type_id == unit_type_id
|
||||
if unit.tenant_id == tenant_id and unit.unit_type_id == unit_type_id
|
||||
),
|
||||
)
|
||||
|
||||
@@ -457,9 +463,7 @@ class FakeOrganizationDirectory:
|
||||
root=root,
|
||||
matches=matches,
|
||||
cycle_detected=self.cycle_detected,
|
||||
depth_limited=(
|
||||
structure_id == self.structure.id and max_depth < 2
|
||||
),
|
||||
depth_limited=(structure_id == self.structure.id and max_depth < 2),
|
||||
)
|
||||
)
|
||||
return tuple(results)
|
||||
@@ -529,15 +533,20 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
self.actor = PostboxActorRef(
|
||||
account_id="account-1",
|
||||
identity_id="identity-1",
|
||||
authorized_actions=frozenset(
|
||||
{"discover", "read", "send", "acknowledge"}
|
||||
),
|
||||
authorized_actions=frozenset({"discover", "read", "send", "acknowledge"}),
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.engine.dispose()
|
||||
|
||||
def _create_exact(self, session: Session) -> Postbox:
|
||||
def _create_exact(
|
||||
self,
|
||||
session: Session,
|
||||
*,
|
||||
encryption_profile: str = "plaintext_v1",
|
||||
encryption_vault_id: str | None = None,
|
||||
protection_policy: dict[str, object] | None = None,
|
||||
) -> Postbox:
|
||||
return self.service.create_exact_postbox(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
@@ -548,6 +557,9 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
description=None,
|
||||
classification="internal",
|
||||
actor_id="admin-1",
|
||||
encryption_profile=encryption_profile,
|
||||
encryption_vault_id=encryption_vault_id,
|
||||
protection_policy=protection_policy,
|
||||
)
|
||||
|
||||
def _routing_policy(
|
||||
@@ -575,11 +587,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
"max_retention_days": 30,
|
||||
},
|
||||
"attention": {
|
||||
"mode": (
|
||||
"vacancy_escalation"
|
||||
if vacancy_escalation
|
||||
else "none"
|
||||
),
|
||||
"mode": ("vacancy_escalation" if vacancy_escalation else "none"),
|
||||
"delay_minutes": 1 if vacancy_escalation else None,
|
||||
},
|
||||
"shared_visibility": {"mode": "none"},
|
||||
@@ -830,7 +838,9 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
),
|
||||
)
|
||||
|
||||
def test_template_revision_is_immutable_and_materialization_idempotent(self) -> None:
|
||||
def test_template_revision_is_immutable_and_materialization_idempotent(
|
||||
self,
|
||||
) -> None:
|
||||
with Session(self.engine) as session:
|
||||
template = self.service.create_template(
|
||||
session,
|
||||
@@ -957,6 +967,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
routing_policy={},
|
||||
encryption_profile="plaintext_v1",
|
||||
encryption_vault_id=None,
|
||||
protection_policy={},
|
||||
context_key=None,
|
||||
limit=200,
|
||||
)
|
||||
@@ -985,6 +996,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
routing_policy={},
|
||||
encryption_profile="plaintext_v1",
|
||||
encryption_vault_id=None,
|
||||
protection_policy={},
|
||||
context_key=None,
|
||||
limit=200,
|
||||
)
|
||||
@@ -1014,6 +1026,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
routing_policy={},
|
||||
encryption_profile="plaintext_v1",
|
||||
encryption_vault_id=None,
|
||||
protection_policy={},
|
||||
context_key=None,
|
||||
limit=200,
|
||||
)
|
||||
@@ -1067,9 +1080,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
[item.slug for item in catalog.templates],
|
||||
)
|
||||
north = next(
|
||||
unit
|
||||
for unit in catalog.organization_units
|
||||
if unit.id == "unit-1"
|
||||
unit for unit in catalog.organization_units if unit.id == "unit-1"
|
||||
)
|
||||
self.assertEqual(
|
||||
["function-1"],
|
||||
@@ -1309,7 +1320,11 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
self.idm.assignments.append(self.assignment)
|
||||
expires_at = utc_now() + timedelta(days=1)
|
||||
with Session(self.engine) as session:
|
||||
postbox = self._create_exact(session)
|
||||
postbox = self._create_exact(
|
||||
session,
|
||||
encryption_profile="external_e2ee_v1",
|
||||
protection_policy={"external_recipient_assurance": "email_otp"},
|
||||
)
|
||||
delivered = self.service.deliver(
|
||||
session,
|
||||
PostboxDeliveryRequest(
|
||||
@@ -1340,6 +1355,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
assurance_profile="email-otp",
|
||||
),
|
||||
),
|
||||
metadata={"content_digest": "sha256:" + "a" * 64},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1361,9 +1377,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
assert message.external_recipient_tokens[0].expires_at is not None
|
||||
self.assertEqual(
|
||||
expires_at.replace(microsecond=0),
|
||||
message.external_recipient_tokens[0].expires_at.replace(
|
||||
microsecond=0
|
||||
),
|
||||
message.external_recipient_tokens[0].expires_at.replace(microsecond=0),
|
||||
)
|
||||
|
||||
def test_server_envelope_body_is_not_persisted_in_plaintext_and_fails_closed(
|
||||
@@ -1445,14 +1459,330 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
actor=self.actor,
|
||||
)
|
||||
|
||||
def test_future_only_protection_transition_changes_new_message_policy(self) -> None:
|
||||
with Session(self.engine) as session:
|
||||
postbox = self._create_exact(
|
||||
session,
|
||||
protection_policy={
|
||||
"handover_authority": "user_consent",
|
||||
"handover_quorum": 1,
|
||||
},
|
||||
)
|
||||
transition = self.service.create_protection_transition(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
postbox_id=postbox.id,
|
||||
idempotency_key="future-e2ee",
|
||||
expected_revision=postbox.resource_revision,
|
||||
target_profile="external_e2ee_v1",
|
||||
target_vault_id=None,
|
||||
history_mode="future_only",
|
||||
authority_mode="user_consent",
|
||||
required_quorum=1,
|
||||
user_consent_refs=("consent:user-1",),
|
||||
institutional_authorization_refs=(),
|
||||
reason="Use client-held keys for future messages.",
|
||||
actor_id="admin-1",
|
||||
)
|
||||
|
||||
self.assertEqual("completed", transition.state)
|
||||
self.assertEqual(0, transition.message_count)
|
||||
self.assertEqual("external_e2ee_v1", postbox.encryption_profile)
|
||||
self.assertEqual(2, postbox.key_epoch)
|
||||
|
||||
def test_client_transform_completes_plaintext_to_e2ee_history(self) -> None:
|
||||
self.idm.assignments.append(self.assignment)
|
||||
plaintext = "A governed message"
|
||||
digest = "sha256:" + hashlib.sha256(plaintext.encode()).hexdigest()
|
||||
with Session(self.engine) as session:
|
||||
postbox = self._create_exact(
|
||||
session,
|
||||
protection_policy={
|
||||
"handover_authority": "user_consent",
|
||||
"handover_quorum": 1,
|
||||
},
|
||||
)
|
||||
delivered = self.service.deliver(
|
||||
session,
|
||||
PostboxDeliveryRequest(
|
||||
tenant_id="tenant-1",
|
||||
target=PostboxTargetRef(postbox_id=postbox.id),
|
||||
producer_module="campaigns",
|
||||
producer_resource_type="campaign_recipient",
|
||||
producer_resource_id="recipient-1",
|
||||
idempotency_key="plaintext-history",
|
||||
subject="Governed history",
|
||||
body_text=plaintext,
|
||||
),
|
||||
)
|
||||
transition = self.service.create_protection_transition(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
postbox_id=postbox.id,
|
||||
idempotency_key="migrate-e2ee",
|
||||
expected_revision=postbox.resource_revision,
|
||||
target_profile="external_e2ee_v1",
|
||||
target_vault_id=None,
|
||||
history_mode="migrate_history",
|
||||
authority_mode="user_consent",
|
||||
required_quorum=1,
|
||||
user_consent_refs=("consent:user-1",),
|
||||
institutional_authorization_refs=(),
|
||||
reason="Move retained content to client-held keys.",
|
||||
actor_id="admin-1",
|
||||
)
|
||||
|
||||
self.assertEqual("awaiting_client", transition.state)
|
||||
self.assertEqual(1, transition.message_count)
|
||||
completed = self.service.apply_client_protection_transform(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
postbox_id=postbox.id,
|
||||
transition_id=transition.id,
|
||||
message_id=delivered.message_id,
|
||||
expected_revision=transition.resource_revision,
|
||||
plaintext=None,
|
||||
ciphertext_ref="files:ciphertext-transition-1",
|
||||
signed_manifest_ref="files:manifest-transition-1",
|
||||
wrapped_keys=(
|
||||
PostboxWrappedKeyRef(
|
||||
recipient_type="function_postbox",
|
||||
recipient_id=postbox.id,
|
||||
key_epoch=postbox.key_epoch,
|
||||
wrapped_key_ref="trust:wrapped-transition-1",
|
||||
algorithm="HPKE-v1",
|
||||
),
|
||||
),
|
||||
content_digest=digest,
|
||||
transformation_evidence_ref="client:evidence-1",
|
||||
actor_id="admin-1",
|
||||
)
|
||||
|
||||
self.assertEqual("completed", completed.state)
|
||||
self.assertEqual(1, completed.completed_count)
|
||||
stored = session.get(PostboxMessage, delivered.message_id)
|
||||
assert stored is not None
|
||||
self.assertIsNone(stored.body_text)
|
||||
self.assertEqual("external_e2ee_v1", stored.encryption_profile)
|
||||
self.assertEqual("files:ciphertext-transition-1", stored.ciphertext_ref)
|
||||
self.assertEqual(digest, stored.metadata_["content_digest"])
|
||||
|
||||
def test_plaintext_history_migrates_to_managed_envelopes_automatically(
|
||||
self,
|
||||
) -> None:
|
||||
protected = SimpleNamespace(
|
||||
ciphertext=b"managed-history",
|
||||
envelope=SimpleNamespace(
|
||||
envelope_id="transition-envelope-1",
|
||||
ciphertext_ref="postbox-db://messages/history/body",
|
||||
algorithm_suite="AES-256-GCM",
|
||||
wrapped_key_refs=("wrapped-history-1",),
|
||||
vault_id="vault-1",
|
||||
key_version=4,
|
||||
),
|
||||
)
|
||||
with Session(self.engine) as session:
|
||||
postbox = self._create_exact(session)
|
||||
delivered = self.service.deliver(
|
||||
session,
|
||||
PostboxDeliveryRequest(
|
||||
tenant_id="tenant-1",
|
||||
target=PostboxTargetRef(postbox_id=postbox.id),
|
||||
producer_module="campaigns",
|
||||
producer_resource_type="campaign_recipient",
|
||||
producer_resource_id="recipient-managed-history",
|
||||
idempotency_key="managed-history",
|
||||
subject="Managed history",
|
||||
body_text="Move this content",
|
||||
),
|
||||
)
|
||||
with patch(
|
||||
"govoplan_postbox.backend.content_protection.protect_message_body",
|
||||
return_value=protected,
|
||||
):
|
||||
transition = self.service.create_protection_transition(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
postbox_id=postbox.id,
|
||||
idempotency_key="migrate-managed",
|
||||
expected_revision=postbox.resource_revision,
|
||||
target_profile="server_envelope_v1",
|
||||
target_vault_id="vault-1",
|
||||
history_mode="migrate_history",
|
||||
authority_mode="dual_control",
|
||||
required_quorum=2,
|
||||
user_consent_refs=("consent:incumbent-1",),
|
||||
institutional_authorization_refs=("approval:key-holder-1",),
|
||||
reason="Adopt the managed standard.",
|
||||
actor_id="admin-1",
|
||||
)
|
||||
|
||||
self.assertEqual("completed", transition.state)
|
||||
stored = session.get(PostboxMessage, delivered.message_id)
|
||||
assert stored is not None
|
||||
self.assertIsNone(stored.body_text)
|
||||
self.assertEqual(b"managed-history", stored.body_ciphertext)
|
||||
self.assertEqual("transition-envelope-1", stored.encryption_envelope_id)
|
||||
self.assertEqual("server_envelope_v1", stored.encryption_profile)
|
||||
self.assertEqual("vault-1", stored.wrapped_keys[0]["recipient_id"])
|
||||
|
||||
def test_new_incumbent_history_policy_filters_lists_counts_and_direct_reads(
|
||||
self,
|
||||
) -> None:
|
||||
boundary = utc_now() - timedelta(days=1)
|
||||
self.idm.assignments.append(replace(self.assignment, valid_from=boundary))
|
||||
with Session(self.engine) as session:
|
||||
postbox = self._create_exact(
|
||||
session,
|
||||
protection_policy={"new_incumbent_history": "since_assignment"},
|
||||
)
|
||||
old_delivery = self.service.deliver(
|
||||
session,
|
||||
PostboxDeliveryRequest(
|
||||
tenant_id="tenant-1",
|
||||
target=PostboxTargetRef(postbox_id=postbox.id),
|
||||
producer_module="campaigns",
|
||||
producer_resource_type="campaign_recipient",
|
||||
producer_resource_id="recipient-old",
|
||||
idempotency_key="history-old",
|
||||
subject="Before assignment",
|
||||
body_text="Old content",
|
||||
),
|
||||
)
|
||||
recent_delivery = self.service.deliver(
|
||||
session,
|
||||
PostboxDeliveryRequest(
|
||||
tenant_id="tenant-1",
|
||||
target=PostboxTargetRef(postbox_id=postbox.id),
|
||||
producer_module="campaigns",
|
||||
producer_resource_type="campaign_recipient",
|
||||
producer_resource_id="recipient-recent",
|
||||
idempotency_key="history-recent",
|
||||
subject="After assignment",
|
||||
body_text="Recent content",
|
||||
),
|
||||
)
|
||||
old = session.get(PostboxMessage, old_delivery.message_id)
|
||||
recent = session.get(PostboxMessage, recent_delivery.message_id)
|
||||
assert old is not None and recent is not None
|
||||
old.delivered_at = boundary - timedelta(hours=1)
|
||||
recent.delivered_at = boundary + timedelta(hours=1)
|
||||
session.flush()
|
||||
|
||||
messages = self.service.list_messages(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
postbox_ids=(postbox.id,),
|
||||
actor=self.actor,
|
||||
)
|
||||
|
||||
self.assertEqual([recent.id], [message.id for message in messages])
|
||||
self.assertEqual(
|
||||
1,
|
||||
self.service.count_messages(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
postbox_ids=(postbox.id,),
|
||||
actor=self.actor,
|
||||
),
|
||||
)
|
||||
self.assertIsNone(
|
||||
self.service.get_message(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
message_id=old.id,
|
||||
actor=self.actor,
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
self.service.can_read_message(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
message_id=old.id,
|
||||
actor=self.actor,
|
||||
)
|
||||
)
|
||||
|
||||
def test_leaving_e2ee_history_requires_user_consent_authority(self) -> None:
|
||||
with Session(self.engine) as session:
|
||||
postbox = self._create_exact(
|
||||
session,
|
||||
encryption_profile="external_e2ee_v1",
|
||||
)
|
||||
with self.assertRaisesRegex(PostboxError, "user consent"):
|
||||
self.service.create_protection_transition(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
postbox_id=postbox.id,
|
||||
idempotency_key="leave-e2ee-without-user",
|
||||
expected_revision=postbox.resource_revision,
|
||||
target_profile="plaintext_v1",
|
||||
target_vault_id=None,
|
||||
history_mode="future_only",
|
||||
authority_mode="institutional_key_holders",
|
||||
required_quorum=1,
|
||||
user_consent_refs=(),
|
||||
institutional_authorization_refs=("approval:key-holder-1",),
|
||||
reason="Leave E2EE without holder consent.",
|
||||
actor_id="admin-1",
|
||||
)
|
||||
|
||||
def test_protection_policy_update_is_revisioned_and_enforces_assurance(
|
||||
self,
|
||||
) -> None:
|
||||
with Session(self.engine) as session:
|
||||
postbox = self._create_exact(session)
|
||||
original_revision = postbox.resource_revision
|
||||
updated = self.service.update_protection_policy(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
postbox_id=postbox.id,
|
||||
protection_policy={
|
||||
"new_incumbent_history": "all_retained",
|
||||
"external_recipient_assurance": "disabled",
|
||||
},
|
||||
actor_id="admin-1",
|
||||
expected_revision=original_revision,
|
||||
)
|
||||
|
||||
self.assertEqual(original_revision + 1, updated.resource_revision)
|
||||
self.assertEqual(
|
||||
"disabled",
|
||||
updated.settings["protection_policy"]["external_recipient_assurance"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"rewrap",
|
||||
updated.settings["protection_policy"]["ordinary_rotation"],
|
||||
)
|
||||
with self.assertRaisesRegex(PostboxError, "does not permit"):
|
||||
self.service.deliver(
|
||||
session,
|
||||
PostboxDeliveryRequest(
|
||||
tenant_id="tenant-1",
|
||||
target=PostboxTargetRef(postbox_id=postbox.id),
|
||||
producer_module="campaigns",
|
||||
producer_resource_type="campaign_recipient",
|
||||
producer_resource_id="recipient-external-disabled",
|
||||
idempotency_key="external-disabled",
|
||||
subject="External retrieval",
|
||||
body_text="Content",
|
||||
external_recipient_tokens=(
|
||||
PostboxExternalRecipientTokenRef(
|
||||
token_id="grant-disabled",
|
||||
state="available",
|
||||
one_time=True,
|
||||
assurance_profile="strong_identity",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def test_hierarchy_linked_copy_snapshots_path_and_independent_state(
|
||||
self,
|
||||
) -> None:
|
||||
self.idm.assignments.append(self.assignment)
|
||||
with Session(self.engine) as session:
|
||||
service, source, _target_template = (
|
||||
self._create_routing_source(session)
|
||||
)
|
||||
service, source, _target_template = self._create_routing_source(session)
|
||||
result = service.deliver(
|
||||
session,
|
||||
PostboxDeliveryRequest(
|
||||
@@ -1478,10 +1808,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
self.assertNotEqual(result.message_id, route.target_message_id)
|
||||
self.assertEqual(
|
||||
["edge-child-parent"],
|
||||
[
|
||||
edge["edge_id"]
|
||||
for edge in route.policy_snapshot["target"]["path"]
|
||||
],
|
||||
[edge["edge_id"] for edge in route.policy_snapshot["target"]["path"]],
|
||||
)
|
||||
self.assertEqual(
|
||||
route.id,
|
||||
@@ -1496,7 +1823,9 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
)
|
||||
session.commit()
|
||||
receipts = session.query(PostboxMessageReceipt).all()
|
||||
self.assertEqual([route.target_message_id], [item.message_id for item in receipts])
|
||||
self.assertEqual(
|
||||
[route.target_message_id], [item.message_id for item in receipts]
|
||||
)
|
||||
summary = service.delivery_receipt_summaries(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
@@ -1512,8 +1841,8 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
self,
|
||||
) -> None:
|
||||
with Session(self.engine) as session:
|
||||
service, source, _target_template = (
|
||||
self._create_routing_source(session, max_depth=1)
|
||||
service, source, _target_template = self._create_routing_source(
|
||||
session, max_depth=1
|
||||
)
|
||||
blocked = service.preview_hierarchy_routes(
|
||||
session,
|
||||
@@ -1573,9 +1902,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
self.organizations.duplicate_parent_match = True
|
||||
self.organizations.cycle_detected = True
|
||||
with Session(self.engine) as session:
|
||||
service, source, _target_template = (
|
||||
self._create_routing_source(session)
|
||||
)
|
||||
service, source, _target_template = self._create_routing_source(session)
|
||||
preview = service.preview_hierarchy_routes(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
@@ -1588,10 +1915,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
self.assertIn("hierarchy_cycle_bounded", preview["diagnostics"])
|
||||
self.assertEqual(
|
||||
1,
|
||||
sum(
|
||||
route["status"] == "duplicate"
|
||||
for route in preview["routes"]
|
||||
),
|
||||
sum(route["status"] == "duplicate" for route in preview["routes"]),
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
@@ -1639,11 +1963,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
),
|
||||
)
|
||||
session.commit()
|
||||
routes = (
|
||||
session.query(PostboxRoute)
|
||||
.order_by(PostboxRoute.depth)
|
||||
.all()
|
||||
)
|
||||
routes = session.query(PostboxRoute).order_by(PostboxRoute.depth).all()
|
||||
self.assertEqual(
|
||||
["accepted_vacant", "pending_vacancy_escalation"],
|
||||
[route.status for route in routes],
|
||||
@@ -1687,10 +2007,7 @@ class PostboxServiceTests(unittest.TestCase):
|
||||
session.commit()
|
||||
pending = (
|
||||
session.query(PostboxRoute)
|
||||
.filter(
|
||||
PostboxRoute.status
|
||||
== "pending_vacancy_escalation"
|
||||
)
|
||||
.filter(PostboxRoute.status == "pending_vacancy_escalation")
|
||||
.one()
|
||||
)
|
||||
pending.execute_after = utc_now() - timedelta(seconds=1)
|
||||
|
||||
Reference in New Issue
Block a user