feat(campaign): explain governed child access
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import json
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
@@ -12,6 +12,8 @@ from govoplan_access.backend.db.models import Account, Group, User
|
||||
from govoplan_campaign.backend.capabilities import (
|
||||
CampaignAccessService,
|
||||
CampaignOwnershipService,
|
||||
campaign_import_execution_resource_id,
|
||||
campaign_protocol_artifact_resource_id,
|
||||
campaign_report_resource_id,
|
||||
campaign_version_child_resource_id,
|
||||
)
|
||||
@@ -28,11 +30,12 @@ from govoplan_campaign.backend.db.models import (
|
||||
ImapAppendAttempt,
|
||||
PostboxDeliveryAttempt,
|
||||
PrintOutputAttempt,
|
||||
RecipientImportMappingProfile,
|
||||
SendAttempt,
|
||||
)
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.change_sequence import ChangeSequenceEntry
|
||||
from govoplan_core.core.ownership import OwnershipSubjectRef, OwnershipTransferError
|
||||
from govoplan_core.core.ownership import OwnershipSubjectRef, OwnershipTransfer, OwnershipTransferError
|
||||
from govoplan_core.db.base import Base
|
||||
|
||||
|
||||
@@ -565,6 +568,141 @@ class CampaignAccessProviderTests(unittest.TestCase):
|
||||
self.assertEqual("unavailable_or_hidden", unavailable[0].details["evidence_availability"])
|
||||
self.assertNotIn("hidden postbox address", json.dumps(unavailable[0].details))
|
||||
|
||||
def test_governance_import_and_protocol_explanation_matrix(self) -> None:
|
||||
session = _session()
|
||||
self.addCleanup(_close_session, session)
|
||||
_seed_access_subjects(session)
|
||||
now = datetime.now(UTC)
|
||||
campaign = Campaign(
|
||||
id="campaign-governed-children", tenant_id=TENANT_ID,
|
||||
owner_user_id=USER_ID, external_id="governed-children", name="Governed children",
|
||||
)
|
||||
share = CampaignShare(
|
||||
id="share-governed-child", tenant_id=TENANT_ID, campaign_id=campaign.id,
|
||||
target_type="user", target_id=OTHER_USER_ID, permission="read",
|
||||
)
|
||||
version = CampaignVersion(
|
||||
id="version-governed-children", campaign_id=campaign.id, version_number=2,
|
||||
raw_json={"entries": {"inline": [], "imports": [{
|
||||
"id": "import-stable-id", "imported_at": "2026-08-20T08:00:00+00:00",
|
||||
"mode": "replace", "source_type": "addresses", "source_id": "address-source-42",
|
||||
"source_revision": "revision-7",
|
||||
"source_provenance": {"secret_source_row": "do-not-disclose-row"},
|
||||
"filename": "hidden-import.csv",
|
||||
"mapping": [{"header": "Secret column", "field_name": "secret"}],
|
||||
}]}},
|
||||
validation_summary={"secret_issue": "do-not-disclose-validation"},
|
||||
build_summary={"secret_recipient": "do-not-disclose-build"},
|
||||
execution_snapshot={"rows": ["do-not-disclose-snapshot"]},
|
||||
execution_snapshot_hash="a" * 64, execution_snapshot_at=now,
|
||||
editor_state={"review_send": {"issue_decisions": [
|
||||
{"reason": "do-not-disclose-review-reason"}
|
||||
]}},
|
||||
)
|
||||
profile = RecipientImportMappingProfile(
|
||||
id="mapping-profile-governed-child", tenant_id=TENANT_ID, owner_user_id=USER_ID,
|
||||
name="Hidden profile name", column_count=2,
|
||||
headers=["Secret A", "Secret B"], normalized_headers=["secret a", "secret b"],
|
||||
ordered_header_fingerprint="b" * 64, unordered_header_fingerprint="c" * 64,
|
||||
delimiter=";", header_rows=1, quoted=True, value_separators=",;|",
|
||||
mappings=[{"header": "Secret A", "field": "hidden"}],
|
||||
)
|
||||
transfer = OwnershipTransfer(
|
||||
id="transfer-governed-child", tenant_id=TENANT_ID,
|
||||
resource_module="campaigns", resource_type="campaign", resource_id=campaign.id,
|
||||
kind="transfer", status="pending_target",
|
||||
current_owner_type="user", current_owner_id=USER_ID,
|
||||
target_owner_type="user", target_owner_id=OTHER_USER_ID,
|
||||
initiated_by_type="user", initiated_by_id=USER_ID,
|
||||
reason="do-not-disclose-transfer-reason", required_approvals=1,
|
||||
approvals=[], decisions=[], idempotency_key="transfer-idempotency",
|
||||
canonical_request_hash="d" * 64, expires_at=now + timedelta(days=1),
|
||||
revision=3, metadata_={"secret": "do-not-disclose-transfer-metadata"},
|
||||
)
|
||||
session.add_all([campaign, share, version, profile, transfer])
|
||||
session.commit()
|
||||
|
||||
service = CampaignAccessService()
|
||||
principal = _principal(scopes={
|
||||
"campaigns:campaign:read", "campaigns:campaign:share",
|
||||
"campaigns:recipient:read", "campaigns:recipient:import",
|
||||
"campaigns:campaign:review", "campaigns:diagnostic:read",
|
||||
"campaigns:report:read",
|
||||
})
|
||||
import_id = campaign_import_execution_resource_id(
|
||||
version_id=version.id, import_id="import-stable-id",
|
||||
)
|
||||
protocol_ids = [
|
||||
campaign_protocol_artifact_resource_id(version_id=version.id, artifact_kind=kind)
|
||||
for kind in ("validation", "build", "execution_snapshot", "review")
|
||||
]
|
||||
cases = [
|
||||
("campaign_share", share.id, "campaigns.share_record"),
|
||||
("campaign_ownership_transfer", transfer.id, "campaigns.ownership_transfer"),
|
||||
("campaign_import_execution", import_id, "campaigns.import_execution"),
|
||||
*(("campaign_protocol_artifact", item, "campaigns.protocol_artifact") for item in protocol_ids),
|
||||
]
|
||||
hidden_values = (
|
||||
OTHER_USER_ID, "do-not-disclose-row", "hidden-import.csv", "Secret column",
|
||||
"do-not-disclose-validation", "do-not-disclose-build", "do-not-disclose-snapshot",
|
||||
"do-not-disclose-review-reason", "do-not-disclose-transfer-reason",
|
||||
"do-not-disclose-transfer-metadata",
|
||||
)
|
||||
for resource_type, resource_id, source in cases:
|
||||
items = service.explain_resource_provenance(
|
||||
session, principal, resource_type=resource_type, resource_id=resource_id,
|
||||
action="campaigns:diagnostic:read",
|
||||
)
|
||||
child = next(item for item in items if item.source == source)
|
||||
self.assertEqual(campaign.id, child.details["campaign_id"])
|
||||
serialized = json.dumps(child.details, sort_keys=True)
|
||||
for hidden_value in hidden_values:
|
||||
self.assertNotIn(hidden_value, serialized)
|
||||
|
||||
import_item = next(item for item in service.explain_resource_provenance(
|
||||
session, principal, resource_type="campaign_import_execution",
|
||||
resource_id=import_id, action="campaigns:recipient:import",
|
||||
) if item.source == "campaigns.import_execution")
|
||||
self.assertEqual("address-source-42", import_item.details["source_id"])
|
||||
self.assertEqual("revision-7", import_item.details["source_revision"])
|
||||
self.assertFalse(import_item.details["source_rows_disclosed"])
|
||||
|
||||
profile_items = service.explain_resource_provenance(
|
||||
session, principal, resource_type="campaign_import_mapping_profile",
|
||||
resource_id=profile.id, action="campaigns:recipient:import",
|
||||
)
|
||||
profile_item = next(item for item in profile_items if item.source == "campaigns.import_mapping_profile")
|
||||
self.assertEqual("independently_owned", profile_item.details["authorization_mode"])
|
||||
self.assertTrue(any(item.kind == "owner" for item in profile_items))
|
||||
self.assertNotIn("Secret A", json.dumps(profile_item.details))
|
||||
self.assertNotIn("Hidden profile name", json.dumps(profile_item.to_dict()))
|
||||
|
||||
for resource_type, optional_module in (
|
||||
("campaign_template", "templates"),
|
||||
("campaign_template_revision", "templates"),
|
||||
("campaign_export_package", "reporting"),
|
||||
):
|
||||
unavailable = service.explain_resource_provenance(
|
||||
session, principal, resource_type=resource_type,
|
||||
resource_id="opaque-optional-resource", action="campaigns:diagnostic:read",
|
||||
)[0]
|
||||
self.assertEqual("campaigns.not_found", unavailable.source)
|
||||
self.assertEqual(optional_module, unavailable.details["optional_module"])
|
||||
self.assertEqual("independently_governed_by_optional_module", unavailable.details["reason"])
|
||||
self.assertEqual("unavailable_or_hidden", unavailable.details["evidence_availability"])
|
||||
|
||||
unsupported = service.explain_resource_provenance(
|
||||
session, principal, resource_type="campaign_protocol_artifact",
|
||||
resource_id=f"{version.id}:unsupported", action="campaigns:diagnostic:read",
|
||||
)
|
||||
self.assertEqual("unsupported_artifact_kind", unsupported[0].details["reason"])
|
||||
cross_tenant = service.explain_resource_provenance(
|
||||
session, _principal(tenant_id="tenant-2"),
|
||||
resource_type="campaign_import_mapping_profile", resource_id=profile.id,
|
||||
action="campaigns:recipient:import",
|
||||
)
|
||||
self.assertEqual("campaigns.not_found", cross_tenant[0].source)
|
||||
|
||||
def test_campaign_ownership_provider_requires_group_acceptance_authority(self) -> None:
|
||||
session = _session()
|
||||
self.addCleanup(_close_session, session)
|
||||
@@ -791,6 +929,8 @@ def _session():
|
||||
ImapAppendAttempt.__table__,
|
||||
PostboxDeliveryAttempt.__table__,
|
||||
PrintOutputAttempt.__table__,
|
||||
RecipientImportMappingProfile.__table__,
|
||||
OwnershipTransfer.__table__,
|
||||
ChangeSequenceEntry.__table__,
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user