security: bound Campaign editor metadata

This commit is contained in:
2026-07-21 17:55:30 +02:00
parent 50c509d161
commit af833ca38c
4 changed files with 274 additions and 3 deletions

View File

@@ -0,0 +1,110 @@
from __future__ import annotations
from datetime import UTC, datetime
import pytest
from pydantic import ValidationError
from govoplan_campaign.backend.campaign.mail_profile_boundary import (
campaign_editor_state_for_edit,
)
from govoplan_campaign.backend.schemas import (
CampaignVersionResponse,
CampaignVersionUpdateRequest,
)
@pytest.mark.parametrize(
"editor_state",
[
{"smtp": {"host": "smtp.example.test", "password": "secret"}},
{"transport": {"imap_password": "secret"}},
{
"review_send": {
"build_token": "forged",
"inspection_complete": True,
"reviewed_message_keys": [],
"updated_at": "2026-07-21T00:00:00+00:00",
"updated_by_user_id": "user-1",
}
},
],
)
def test_version_updates_reject_arbitrary_or_server_owned_editor_state(
editor_state: dict[str, object],
) -> None:
with pytest.raises(ValidationError, match="unsupported or transport-owned"):
CampaignVersionUpdateRequest(editor_state=editor_state)
def test_version_response_omits_legacy_secret_bearing_editor_state() -> None:
response = CampaignVersionResponse.model_validate(
{
"id": "version-1",
"campaign_id": "campaign-1",
"version_number": 1,
"schema_version": "1.0",
"editor_state": {
"opt_ins": {"inline_guidance": True},
"smtp": {"host": "smtp.internal.example", "password": "provider-secret"},
},
"created_at": datetime.now(UTC),
"updated_at": datetime.now(UTC),
}
)
assert response.editor_state == {"opt_ins": {"inline_guidance": True}}
assert "provider-secret" not in repr(response)
assert "internal.example" not in repr(response)
def test_review_build_token_is_visible_only_in_operator_diagnostics() -> None:
value = {
"id": "version-1",
"campaign_id": "campaign-1",
"version_number": 1,
"schema_version": "1.0",
"editor_state": {
"review_send": {
"build_token": "internal-build-token",
"inspection_complete": True,
"reviewed_message_keys": ["entry-1"],
"updated_at": "2026-07-21T00:00:00+00:00",
"updated_by_user_id": "reviewer-1",
}
},
"created_at": datetime.now(UTC),
"updated_at": datetime.now(UTC),
}
public = CampaignVersionResponse.model_validate(value)
operator = CampaignVersionResponse.model_validate(
value,
context={"include_diagnostics": True},
)
assert "build_token" not in public.editor_state["review_send"]
assert operator.editor_state["review_send"]["build_token"] == "internal-build-token"
def test_fork_copy_keeps_only_client_owned_bounded_metadata() -> None:
copied = campaign_editor_state_for_edit(
{
"created_from": "minimal_campaign",
"field_overrides": {"department": False},
"review_send": {
"build_token": "build-1",
"inspection_complete": True,
"reviewed_message_keys": ["entry-1"],
"updated_at": "2026-07-21T00:00:00+00:00",
"updated_by_user_id": "reviewer-1",
},
"credentials": {"password": "legacy-secret"},
}
)
assert copied == {
"created_from": "minimal_campaign",
"field_overrides": {"department": False},
}
assert "legacy-secret" not in repr(copied)