feat: add governed Scheduling DSAR coverage

This commit is contained in:
2026-08-20 23:35:04 +02:00
parent 539e3cbb5e
commit 0fd297231b
4 changed files with 1564 additions and 1 deletions
+81 -1
View File
@@ -6,8 +6,10 @@ from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPA
from govoplan_core.core.calendar import CAPABILITY_CALENDAR_SCHEDULING
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationTopic,
DocumentationCondition,
DocumentationLink,
FrontendModule,
FrontendRoute,
MigrationSpec,
@@ -36,10 +38,11 @@ from govoplan_core.core.policy import CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_P
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
from govoplan_scheduling.backend.db import models as scheduling_models # noqa: F401 - populate Scheduling ORM metadata
from govoplan_scheduling.backend.dsar_provider import SCHEDULING_DSAR_CAPABILITY
MODULE_ID = "scheduling"
MODULE_NAME = "Scheduling"
MODULE_VERSION = "0.1.18"
MODULE_VERSION = "0.1.19"
READ_SCOPE = "scheduling:schedule:read"
WRITE_SCOPE = "scheduling:schedule:write"
ADMIN_SCOPE = "scheduling:schedule:admin"
@@ -102,6 +105,63 @@ DOCUMENTATION = (
related_modules=("poll", "evaluation", "calendar", "appointments", "mail", "notifications", "portal"),
metadata={"seed": True},
),
DocumentationTopic(
id="scheduling.privacy.data-subject-requests",
title="Review Scheduling data in a data-subject request",
summary="Collect tenant-scoped participation and coordination metadata while leaving Poll responses and Calendar effects with their owners.",
body=(
"Scheduling's DSAR provider searches the effective tenant by normalized participant email, membership, identity or bound-account references, and namespaced Scheduling request or participant references. "
"It isolates the matching participant, request and candidate-slot context, and matching notification envelopes. It does not export Poll or invitation identifiers, participation-gateway state, reusable enrollment links or proof hashes, anonymous-password hashes, Calendar event or hold identifiers, free/busy conflict detail, notification payloads or errors, token material, opaque metadata, or unrelated participants. Poll remains authoritative for actual availability choices and response-retirement evidence; Calendar remains authoritative for event, hold, and synchronization state. "
"Decided, handed-off, cancelled, archived, responded, notified, or removed state is retained with a reason. Active shared scheduling content requires coordinated manual review. The provider can anonymize and retire only a participant who has no invitation, response, enrollment, notification, or terminal decision evidence, and revalidates all of those conditions under tenant-bound row locks before acting."
),
layer="configured",
documentation_types=("admin",),
audience=("privacy_officer", "scheduling_manager", "records_manager", "operator"),
order=5,
conditions=(
DocumentationCondition(
required_modules=("scheduling", "access"),
any_scopes=(
"access:privacy:read",
"access:privacy:manage",
"access:privacy:erase",
),
),
),
links=(
DocumentationLink(
label="Data-subject requests",
href="/admin?section=tenant-data-subject-requests",
kind="runtime",
),
DocumentationLink(
label="Scheduling module guide",
href="govoplan-scheduling/README.md",
kind="repository",
),
),
related_modules=("access", "audit", "poll", "calendar", "notifications"),
metadata={
"kind": "workflow",
"route": "/admin?section=tenant-data-subject-requests",
"screen": "Data-subject requests",
"help_contexts": ["admin.privacy.data-subject-requests"],
"prerequisites": [
"The privacy request and Scheduling selectors have been independently authorized and corroborated.",
"The reviewer can coordinate with Poll and Calendar owners when a response or event is involved.",
],
"steps": [
"Run the Scheduling provider search and review participant, request, slot, and notification dispositions.",
"Run the Poll provider for actual availability choices and the Calendar provider for event or hold state.",
"Retain terminal decision and delivery evidence with its reason.",
"Execute anonymization only for an approved participant classified as unengaged after revalidation.",
],
"limitations": [
"Poll choices and invitation evidence are not copied into Scheduling's export.",
"Participant erasure remains manual whenever shared responses, delivery, self-enrollment, Calendar effects, or terminal decisions exist.",
],
},
),
DocumentationTopic(
id="scheduling.find-and-decide-meeting-time",
title="Find and decide a meeting time",
@@ -265,6 +325,13 @@ def _scheduling_router(context: ModuleContext):
return router
def _scheduling_dsar_provider(context: ModuleContext) -> object:
del context
from govoplan_scheduling.backend.dsar_provider import SchedulingDsarProvider
return SchedulingDsarProvider()
def _public_tenant_resolver(request: object, session: object) -> str | None:
path_params = getattr(request, "path_params", {})
request_id = str(path_params.get("request_id") or "").strip()
@@ -344,6 +411,7 @@ manifest = ModuleManifest(
provides_interfaces=(
ModuleInterfaceProvider(name="scheduling.candidate_slots", version=MODULE_VERSION),
ModuleInterfaceProvider(name="scheduling.decision_handoff", version=MODULE_VERSION),
ModuleInterfaceProvider(name=SCHEDULING_DSAR_CAPABILITY, version="0.1.0"),
),
requires_interfaces=(
ModuleInterfaceRequirement(name="poll.option_ordering", version_min="0.1.11", version_max_exclusive="0.2.0"),
@@ -434,6 +502,18 @@ manifest = ModuleManifest(
),
),
documentation=DOCUMENTATION,
capability_factories={
SCHEDULING_DSAR_CAPABILITY: _scheduling_dsar_provider,
},
capability_documentation={
SCHEDULING_DSAR_CAPABILITY: CapabilityDocumentation(
label="Scheduling data-subject request provider",
summary="Finds isolated Scheduling participation and coordination metadata and classifies governed erasure actions.",
contract_version="0.1.0",
documentation_types=("admin",),
audience=("privacy_officer", "scheduling_manager", "records_manager"),
),
},
architecture=declared_module_architecture(
layer="communication_participation",
kind="domain",