From 6f671f60f54d594c18f430ef487e54d1d4825498 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Fri, 21 Aug 2026 02:21:53 +0200 Subject: [PATCH] docs(portal): declare DSAR ownership boundary --- README.md | 7 +++++ docs/SERVICE_DIRECTORY_CONCEPT.md | 23 +++++++++++++++++ src/govoplan_portal/backend/manifest.py | 34 +++++++++++++++++++++++++ tests/test_service_directory.py | 16 ++++++++++++ 4 files changed, 80 insertions(+) diff --git a/README.md b/README.md index 914fe37..146eec5 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,11 @@ The public `/portal/status/:trackingId` surface presents the bounded configured authenticated, short-lived email-link, and permanent-link modes, while Portal owns only the accessible presentation and reload/request actions. +Portal deliberately does not publish a DSAR provider because it persists no +service-directory, launch, Postbox, application-status, applicant, or session +records. Services owns definitions, each launch target owns its effects, +Postbox owns mailbox data, and Forms Runtime owns status grants and submission +data. Core and the deployment operator remain responsible for request/security +logs. This reviewed boundary avoids duplicate or contradictory privacy exports. + See [docs/SERVICE_DIRECTORY_CONCEPT.md](docs/SERVICE_DIRECTORY_CONCEPT.md). diff --git a/docs/SERVICE_DIRECTORY_CONCEPT.md b/docs/SERVICE_DIRECTORY_CONCEPT.md index 7b09c3e..41027cb 100644 --- a/docs/SERVICE_DIRECTORY_CONCEPT.md +++ b/docs/SERVICE_DIRECTORY_CONCEPT.md @@ -149,3 +149,26 @@ must not infer missing milestones or expose values, people, evidence, internal notes, or handoff details. A reload action re-fetches the authoritative projection. Missing, disabled, revoked, expired, or unauthorized grants share a non-enumerating unavailable state. + +## Data-subject request ownership + +Portal has no module-owned persistence and therefore does not contribute a +`privacy.dsar.portal` provider. Its routes resolve and render bounded +provider-owned projections during each request; they do not copy service +definitions, launch parameters or results, Postbox entries, status grants, +submission values, email addresses, or applicant identities into Portal. + +Data-subject request coverage follows the authoritative owner: + +- Services covers configuration-author attribution for versioned service + definitions; +- Cases, Forms Runtime, and Workflow Engine cover launch effects and domain + instances; +- Postbox covers mailbox records; and +- Forms Runtime covers status-access grants, token lifecycle, confirmations, + acknowledgements, and submitted Form data. + +Authentication state, request/security logs, and infrastructure telemetry are +Core or deployment-operator concerns, not Portal records. If Portal later gains +durable personalization, analytics, saved searches, contact data, or session +persistence, that change must add a tenant-scoped DSAR provider before release. diff --git a/src/govoplan_portal/backend/manifest.py b/src/govoplan_portal/backend/manifest.py index b3431d5..02aa69d 100644 --- a/src/govoplan_portal/backend/manifest.py +++ b/src/govoplan_portal/backend/manifest.py @@ -231,6 +231,39 @@ manifest = ModuleManifest( ), }, documentation=( + DocumentationTopic( + id="portal.data-subject-requests", + title="Portal data-subject request boundary", + summary="Understand why Portal has no separate privacy export and which authoritative modules own the projected data.", + body=( + "Portal persists no service-directory, service-launch, Postbox, application-status, applicant, or session records, so it deliberately publishes no duplicate data-subject request provider. Services owns definition attribution; Cases, Forms Runtime, and Workflow Engine own launch effects; Postbox owns mailbox records; and Forms Runtime owns submission and status-access data. Core and the deployment operator own authentication state, request/security logs, and infrastructure telemetry. " + "If durable personalization, analytics, saved searches, contact data, or sessions are added to Portal, a tenant-scoped privacy provider is required before release." + ), + layer="available", + documentation_types=("admin", "user"), + audience=("user", "operator", "module_admin", "auditor"), + links=( + DocumentationLink( + label="Portal ownership boundary", + href="govoplan-portal/docs/SERVICE_DIRECTORY_CONCEPT.md", + kind="repository", + ), + ), + related_modules=( + "core", + "services", + "cases", + "forms_runtime", + "workflow_engine", + "postbox", + ), + metadata={ + "kind": "reference", + "help_contexts": ["portal.data-subject-requests"], + "dsar_coverage": "not_applicable_no_persistence", + }, + order=10, + ), DocumentationTopic( id="portal.function-postboxes", title="Portal-facing function Postboxes", @@ -337,6 +370,7 @@ manifest = ModuleManifest( "Portal does not persist service definitions; the Services provider remains authoritative.", "Case, Forms Runtime, and Workflow Engine own launch effects. Portal keeps entries unavailable whenever the selected owner capability is absent.", "Forms Runtime owns applicant-status access, redaction, and timeline semantics; Portal only presents that projection. Payment and decision-document actions are not yet included in the first status surface.", + "Portal owns no durable subject records and therefore has no DSAR provider; adding persistence requires introducing one before release.", ), owned_concepts=("service discovery", "service presentation", "channel entry", "applicant status presentation"), non_owned_concepts=("institutional service definition", "case lifecycle", "applicant status access decision"), diff --git a/tests/test_service_directory.py b/tests/test_service_directory.py index 2dff6ce..7907387 100644 --- a/tests/test_service_directory.py +++ b/tests/test_service_directory.py @@ -440,6 +440,22 @@ class PortalServiceDirectoryTests(unittest.TestCase): "application_status.projection", {item.name for item in manifest.requires_interfaces}, ) + self.assertFalse( + any( + name.startswith("privacy.dsar.") + for name in manifest.capability_factories + ) + ) + dsar_topic = next( + item + for item in manifest.documentation + if item.id == "portal.data-subject-requests" + ) + self.assertEqual( + "not_applicable_no_persistence", + dsar_topic.metadata["dsar_coverage"], + ) + self.assertTrue({"admin", "user"}.issubset(dsar_topic.documentation_types)) if __name__ == "__main__":