docs(portal): declare DSAR ownership boundary

This commit is contained in:
2026-08-21 02:21:53 +02:00
parent ed0357b146
commit 6f671f60f5
4 changed files with 80 additions and 0 deletions
+7
View File
@@ -38,4 +38,11 @@ The public `/portal/status/:trackingId` surface presents the bounded
configured authenticated, short-lived email-link, and permanent-link modes, configured authenticated, short-lived email-link, and permanent-link modes,
while Portal owns only the accessible presentation and reload/request actions. 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). See [docs/SERVICE_DIRECTORY_CONCEPT.md](docs/SERVICE_DIRECTORY_CONCEPT.md).
+23
View File
@@ -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 notes, or handoff details. A reload action re-fetches the authoritative
projection. Missing, disabled, revoked, expired, or unauthorized grants share projection. Missing, disabled, revoked, expired, or unauthorized grants share
a non-enumerating unavailable state. 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.
+34
View File
@@ -231,6 +231,39 @@ manifest = ModuleManifest(
), ),
}, },
documentation=( 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( DocumentationTopic(
id="portal.function-postboxes", id="portal.function-postboxes",
title="Portal-facing function Postboxes", title="Portal-facing function Postboxes",
@@ -337,6 +370,7 @@ manifest = ModuleManifest(
"Portal does not persist service definitions; the Services provider remains authoritative.", "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.", "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.", "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"), owned_concepts=("service discovery", "service presentation", "channel entry", "applicant status presentation"),
non_owned_concepts=("institutional service definition", "case lifecycle", "applicant status access decision"), non_owned_concepts=("institutional service definition", "case lifecycle", "applicant status access decision"),
+16
View File
@@ -440,6 +440,22 @@ class PortalServiceDirectoryTests(unittest.TestCase):
"application_status.projection", "application_status.projection",
{item.name for item in manifest.requires_interfaces}, {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__": if __name__ == "__main__":