Files
govoplan-campaign/tests/test_archive_encryption_governance.py

184 lines
6.6 KiB
Python

from __future__ import annotations
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_campaign.backend.archive_encryption import (
CampaignArchiveEncryptionError,
LEGACY_ZIPCRYPTO_SCOPE,
assert_archive_encryption_allowed,
effective_archive_encryption_policy,
stamp_legacy_zipcrypto_acknowledgements,
)
from govoplan_campaign.backend.db.models import Campaign
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.policy import (
CAPABILITY_POLICY_CAMPAIGN_ARCHIVE_ENCRYPTION,
CampaignArchiveEncryptionDecision,
PolicySourceStep,
)
class _PolicyProvider:
def __init__(self, methods: set[str]) -> None:
self.methods = methods
def resolve_campaign_archive_encryption(self, session=None, *, request):
del session, request
return CampaignArchiveEncryptionDecision(
allowed_password_encryption_methods=frozenset(self.methods),
allowed_password_delivery_channels=frozenset(
{"separate_mail", "sms", "letter", "phone", "in_person"}
),
policy_hash="f" * 64,
source_path=(
PolicySourceStep(
scope_type="system",
label="System archive-encryption policy",
applied_fields=("allowed_password_encryption_methods",),
policy={"allowed_password_encryption_methods": sorted(self.methods)},
),
),
)
class _Registry:
def __init__(self, provider) -> None:
self.provider = provider
def has_capability(self, name: str) -> bool:
return name == CAPABILITY_POLICY_CAMPAIGN_ARCHIVE_ENCRYPTION
def capability(self, name: str):
return self.provider if self.has_capability(name) else None
class CampaignArchiveEncryptionGovernanceTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite:///:memory:")
self.session = Session(self.engine)
self.campaign = Campaign(
id="campaign-1",
tenant_id="tenant-1",
external_id="example",
name="Example",
owner_user_id="user-1",
)
def tearDown(self) -> None:
self.session.close()
self.engine.dispose()
def test_unavailable_policy_keeps_aes_and_fails_closed_for_legacy(self) -> None:
with patch("govoplan_campaign.backend.archive_encryption.get_registry", return_value=None):
policy = effective_archive_encryption_policy(self.session, self.campaign)
self.assertFalse(policy.available)
self.assertEqual(frozenset({"aes"}), policy.allowed_password_encryption_methods)
assert_archive_encryption_allowed(
self.session,
self.campaign,
_raw_archive("aes"),
)
with self.assertRaisesRegex(CampaignArchiveEncryptionError, "blocked"):
assert_archive_encryption_allowed(
self.session,
self.campaign,
_raw_archive("zip_standard", stamped=True),
)
def test_existing_password_archive_inherits_separate_mail_channel(self) -> None:
raw = _raw_archive("aes")
raw["attachments"]["zip"]["archives"][0].pop("password_delivery_channel")
with patch(
"govoplan_campaign.backend.archive_encryption.get_registry",
return_value=None,
):
decision = assert_archive_encryption_allowed(
self.session,
self.campaign,
raw,
)
self.assertIn(
"separate_mail",
decision.allowed_password_delivery_channels,
)
def test_legacy_selection_requires_permission_and_gets_server_stamp(self) -> None:
registry = _Registry(_PolicyProvider({"aes", "zip_standard"}))
candidate = _raw_archive("zip_standard")
candidate["attachments"]["zip"]["archives"][0].update(
{
"legacy_zipcrypto_acknowledged": True,
"legacy_zipcrypto_reason": "Recipient requires built-in Windows extraction",
}
)
with patch("govoplan_campaign.backend.archive_encryption.get_registry", return_value=registry):
with self.assertRaisesRegex(CampaignArchiveEncryptionError, "Missing scope"):
stamp_legacy_zipcrypto_acknowledgements(
self.session,
self.campaign,
{},
candidate,
principal=_principal(set()),
)
stamped, evidence = stamp_legacy_zipcrypto_acknowledgements(
self.session,
self.campaign,
{},
candidate,
principal=_principal({LEGACY_ZIPCRYPTO_SCOPE}),
)
archive = stamped["attachments"]["zip"]["archives"][0]
self.assertEqual("user-1", archive["legacy_zipcrypto_acknowledged_by"])
self.assertTrue(archive["legacy_zipcrypto_acknowledged_at"])
self.assertEqual("f" * 64, evidence[0]["policy_hash"])
assert_archive_encryption_allowed(
self.session,
self.campaign,
stamped,
principal=_principal({LEGACY_ZIPCRYPTO_SCOPE}),
)
def _raw_archive(method: str, *, stamped: bool = False) -> dict:
archive = {
"id": "archive-1",
"method": method,
"password_enabled": True,
"password_delivery_channel": "separate_mail",
"legacy_zipcrypto_acknowledged": method == "zip_standard",
"legacy_zipcrypto_reason": "Windows recipient compatibility required"
if method == "zip_standard"
else None,
}
if stamped:
archive.update(
{
"legacy_zipcrypto_acknowledged_by": "user-1",
"legacy_zipcrypto_acknowledged_at": "2026-08-20T10:00:00+00:00",
}
)
return {"attachments": {"zip": {"enabled": True, "archives": [archive]}}}
def _principal(scopes: set[str]) -> ApiPrincipal:
return ApiPrincipal(
principal=PrincipalRef(
account_id="account-1",
membership_id="user-1",
tenant_id="tenant-1",
scopes=frozenset(scopes),
),
account=SimpleNamespace(id="account-1"),
user=SimpleNamespace(id="user-1"),
)
if __name__ == "__main__":
unittest.main()