Add encrypted envelope recipient state

This commit is contained in:
2026-07-31 22:48:07 +02:00
parent dc6f81dd33
commit e3daf400f3
10 changed files with 272 additions and 2 deletions
+10
View File
@@ -22,6 +22,10 @@ class PostboxMigrationTests(unittest.TestCase):
"govoplan_postbox.backend.migrations.versions."
"f5c8d0e3b7a2_v012_authoring_and_occ"
)
envelope_migration = importlib.import_module(
"govoplan_postbox.backend.migrations.versions."
"a6d9e1f4c8b3_v013_external_recipient_tokens"
)
engine = create_engine("sqlite:///:memory:")
try:
with engine.begin() as connection:
@@ -29,13 +33,16 @@ class PostboxMigrationTests(unittest.TestCase):
original = migration.op
route_original = route_migration.op
occ_original = occ_migration.op
envelope_original = envelope_migration.op
migration.op = operations
route_migration.op = operations
occ_migration.op = operations
envelope_migration.op = operations
try:
migration.upgrade()
route_migration.upgrade()
occ_migration.upgrade()
envelope_migration.upgrade()
tables = set(inspect(connection).get_table_names())
self.assertIn("postboxes", tables)
self.assertIn("postbox_messages", tables)
@@ -52,6 +59,7 @@ class PostboxMigrationTests(unittest.TestCase):
"ciphertext_ref",
"signed_manifest_ref",
"wrapped_keys",
"external_recipient_tokens",
"key_epoch",
"expires_at",
"withdrawn_at",
@@ -83,6 +91,7 @@ class PostboxMigrationTests(unittest.TestCase):
route_columns
)
)
envelope_migration.downgrade()
occ_migration.downgrade()
route_migration.downgrade()
migration.downgrade()
@@ -97,6 +106,7 @@ class PostboxMigrationTests(unittest.TestCase):
migration.op = original
route_migration.op = route_original
occ_migration.op = occ_original
envelope_migration.op = envelope_original
finally:
engine.dispose()
+9
View File
@@ -241,6 +241,15 @@ class PostboxRouterTests(unittest.TestCase):
self.patch.stop()
self.engine.dispose()
def test_assignment_context_override_must_belong_to_principal(self) -> None:
response = self.client.get(
"/api/v1/postbox/directory",
params={"assignment_context_id": "assignment-not-granted"},
)
self.assertEqual(response.status_code, 403)
self.assertIn("not active for this principal", response.text)
def test_directory_delivery_message_and_receipt_round_trip(self) -> None:
directory = self.client.get("/api/v1/postbox/directory")
self.assertEqual(200, directory.status_code, directory.text)
+63
View File
@@ -29,7 +29,9 @@ from govoplan_core.core.organizations import (
from govoplan_core.core.postbox import (
PostboxActorRef,
PostboxDeliveryRequest,
PostboxExternalRecipientTokenRef,
PostboxTargetRef,
PostboxWrappedKeyRef,
)
from govoplan_core.db.base import Base
from govoplan_core.security.time import utc_now
@@ -1173,6 +1175,67 @@ class PostboxServiceTests(unittest.TestCase):
self.assertFalse(summaries[expired.delivery_id].currently_readable)
self.assertEqual(1, summaries[expired.delivery_id].expired_message_count)
def test_encrypted_envelope_and_external_grant_state_cross_capability(self) -> None:
self.idm.assignments.append(self.assignment)
expires_at = utc_now() + timedelta(days=1)
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-encrypted",
idempotency_key="encrypted-message",
subject="Encrypted decision",
ciphertext_ref="files:ciphertext-1",
signed_manifest_ref="files:manifest-1",
wrapped_keys=(
PostboxWrappedKeyRef(
recipient_type="function_postbox",
recipient_id=postbox.id,
key_epoch=postbox.key_epoch,
wrapped_key_ref="trust:wrapped-key-1",
algorithm="HPKE-v1",
),
),
external_recipient_tokens=(
PostboxExternalRecipientTokenRef(
token_id="grant-1",
state="available",
expires_at=expires_at,
one_time=True,
assurance_profile="email-otp",
),
),
),
)
message = self.service.get_message(
session,
tenant_id="tenant-1",
message_id=delivered.message_id,
actor=self.actor,
)
assert message is not None
self.assertEqual("files:ciphertext-1", message.ciphertext_ref)
self.assertEqual(
"trust:wrapped-key-1",
message.wrapped_keys[0].wrapped_key_ref,
)
self.assertEqual("grant-1", message.external_recipient_tokens[0].token_id)
self.assertEqual("available", message.external_recipient_tokens[0].state)
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
),
)
def test_hierarchy_linked_copy_snapshots_path_and_independent_state(
self,
) -> None: