security: bound Campaign editor metadata
This commit is contained in:
110
tests/test_editor_state_security.py
Normal file
110
tests/test_editor_state_security.py
Normal 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)
|
||||
Reference in New Issue
Block a user