feat: protect postbox message content

This commit is contained in:
2026-08-02 03:40:57 +02:00
parent 7d310d5c33
commit 8f8d259a76
12 changed files with 611 additions and 29 deletions
+82
View File
@@ -2,6 +2,8 @@ from __future__ import annotations
import unittest
from datetime import timedelta
from types import SimpleNamespace
from unittest.mock import patch
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
@@ -51,6 +53,7 @@ from govoplan_postbox.backend.db.models import (
PostboxTemplate,
PostboxTemplateRevision,
)
from govoplan_postbox.backend.content_protection import PostboxContentProtectionError
from govoplan_postbox.backend.service import PostboxError, PostboxService
@@ -1236,6 +1239,85 @@ class PostboxServiceTests(unittest.TestCase):
),
)
def test_server_envelope_body_is_not_persisted_in_plaintext_and_fails_closed(
self,
) -> None:
self.idm.assignments.append(self.assignment)
protected = SimpleNamespace(
ciphertext=b"encrypted-message-body",
envelope=SimpleNamespace(
envelope_id="envelope-1",
ciphertext_ref="postbox-db://message/body",
algorithm_suite="AES-256-GCM",
wrapped_key_refs=("wrapped-key-1",),
),
)
with Session(self.engine) as session:
postbox = self.service.create_exact_postbox(
session,
tenant_id="tenant-1",
name="Protected intake",
organization_unit_id="unit-1",
function_id="function-1",
address_key=None,
description=None,
classification="internal",
actor_id="admin-1",
encryption_profile="server_envelope_v1",
encryption_vault_id="vault-1",
)
with patch(
"govoplan_postbox.backend.content_protection.protect_message_body",
return_value=protected,
):
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-protected",
idempotency_key="protected-message",
subject="Protected notice",
body_text="clear message body",
),
)
stored = session.get(PostboxMessage, delivered.message_id)
assert stored is not None
self.assertIsNone(stored.body_text)
self.assertEqual(b"encrypted-message-body", stored.body_ciphertext)
self.assertEqual("envelope-1", stored.encryption_envelope_id)
with patch(
"govoplan_postbox.backend.content_protection.unprotect_message_body",
return_value="clear message body",
):
opened = self.service.get_message(
session,
tenant_id="tenant-1",
message_id=delivered.message_id,
actor=self.actor,
)
assert opened is not None
self.assertEqual("clear message body", opened.body_text)
with patch(
"govoplan_postbox.backend.content_protection.unprotect_message_body",
side_effect=PostboxContentProtectionError("provider unavailable"),
):
with self.assertRaisesRegex(
PostboxError,
"provider unavailable",
):
self.service.get_message(
session,
tenant_id="tenant-1",
message_id=delivered.message_id,
actor=self.actor,
)
def test_hierarchy_linked_copy_snapshots_path_and_independent_state(
self,
) -> None: