feat(admin): declare governed DSAR coverage

This commit is contained in:
2026-08-21 13:17:25 +02:00
parent 550c14b92e
commit c22da17c64
3 changed files with 322 additions and 16 deletions
@@ -0,0 +1,61 @@
from __future__ import annotations
from collections.abc import Sequence
from govoplan_core.core.dsar import (
DsarErasureActionRef,
DsarExecutionResultRef,
DsarRecordRef,
DsarSubjectRef,
dsar_capability_name,
)
ADMIN_DSAR_CAPABILITY = dsar_capability_name("admin")
class AdminDsarProvider:
"""Declare the reviewed absence of subject data in Admin-owned tables."""
provider_id = "admin"
module_id = "admin"
def search_subject(
self,
session: object,
*,
tenant_id: str,
subject: DsarSubjectRef,
) -> Sequence[DsarRecordRef]:
del session, tenant_id, subject
return ()
def plan_erasure(
self,
session: object,
*,
tenant_id: str,
subject: DsarSubjectRef,
records: Sequence[DsarRecordRef],
) -> Sequence[DsarErasureActionRef]:
del session, tenant_id, subject
if records:
raise ValueError("Admin DSAR cannot plan records it does not own.")
return ()
def execute_erasure(
self,
session: object,
*,
tenant_id: str,
subject: DsarSubjectRef,
actions: Sequence[DsarErasureActionRef],
request_id: str,
) -> Sequence[DsarExecutionResultRef]:
del session, tenant_id, subject, request_id
if actions:
raise ValueError("Admin DSAR cannot execute actions it does not own.")
return ()
__all__ = ["ADMIN_DSAR_CAPABILITY", "AdminDsarProvider"]
+149 -16
View File
@@ -1,14 +1,19 @@
from __future__ import annotations from __future__ import annotations
from govoplan_admin.backend.db import models as admin_models # noqa: F401 - populate Admin ORM metadata from govoplan_admin.backend.db import models as admin_models # noqa: F401 - populate Admin ORM metadata
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER from govoplan_core.core.access import (
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
)
from govoplan_core.core.module_guards import persistent_table_uninstall_guard from govoplan_core.core.module_guards import persistent_table_uninstall_guard
from govoplan_core.core.modules import ( from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationCondition, DocumentationCondition,
DocumentationTopic, DocumentationTopic,
FrontendModule, FrontendModule,
MigrationSpec, MigrationSpec,
ModuleContext, ModuleContext,
ModuleInterfaceProvider,
ModuleManifest, ModuleManifest,
PermissionDefinition, PermissionDefinition,
RoleTemplate, RoleTemplate,
@@ -16,6 +21,10 @@ from govoplan_core.core.modules import (
from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.provider_governance import declared_module_architecture
from govoplan_core.core.views import ViewSurface from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base from govoplan_core.db.base import Base
from govoplan_admin.backend.dsar_provider import (
ADMIN_DSAR_CAPABILITY,
AdminDsarProvider,
)
def _route_factory(context: ModuleContext): def _route_factory(context: ModuleContext):
@@ -25,6 +34,10 @@ def _route_factory(context: ModuleContext):
return router return router
def _dsar_provider(_context: ModuleContext) -> AdminDsarProvider:
return AdminDsarProvider()
ADMIN_PERMISSIONS = ( ADMIN_PERMISSIONS = (
PermissionDefinition( PermissionDefinition(
scope="admin:module:read", scope="admin:module:read",
@@ -68,9 +81,53 @@ manifest = ModuleManifest(
version="0.1.18", version="0.1.18",
permissions=ADMIN_PERMISSIONS, permissions=ADMIN_PERMISSIONS,
role_templates=ADMIN_ROLE_TEMPLATES, role_templates=ADMIN_ROLE_TEMPLATES,
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
),
provides_interfaces=(
ModuleInterfaceProvider(name=ADMIN_DSAR_CAPABILITY, version="0.1.0"),
),
capability_factories={ADMIN_DSAR_CAPABILITY: _dsar_provider},
capability_documentation={
ADMIN_DSAR_CAPABILITY: CapabilityDocumentation(
label="Administration data-subject request coverage",
summary=(
"Records the reviewed absence of subject identifiers in Admin-owned "
"governance-template state."
),
contract_version="0.1.0",
),
},
route_factory=_route_factory, route_factory=_route_factory,
documentation=( documentation=(
DocumentationTopic(
id="admin.data-subject-request-coverage",
title="Administration data-subject request coverage",
summary=(
"Admin-owned governance templates contain no data-subject or operator "
"identity fields."
),
body=(
"The Admin module stores reusable governance-template definitions and "
"tenant availability assignments. These tables contain permissions, "
"template labels, and tenant identifiers, but no account, membership, "
"identity, email, author, approver, or other data-subject reference. "
"Admin therefore contributes an explicit zero-result provider so the "
"privacy workspace can distinguish reviewed non-applicability from an "
"unexplained coverage gap. Module-lifecycle and configuration-change "
"actor evidence remains owned by Core, Audit, Access, or Ops and is "
"returned by those modules' providers."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("privacy_officer", "system_admin", "auditor"),
related_modules=("core", "access", "audit", "ops"),
metadata={
"help_contexts": ["admin.privacy.data-subject-requests"],
"coverage_classification": "reviewed_no_subject_data",
},
),
DocumentationTopic( DocumentationTopic(
id="admin.workspace", id="admin.workspace",
title="Use the administration workspace", title="Use the administration workspace",
@@ -236,17 +293,83 @@ manifest = ModuleManifest(
module_id="admin", module_id="admin",
package_name="@govoplan/admin-webui", package_name="@govoplan/admin-webui",
view_surfaces=( view_surfaces=(
ViewSurface(id="admin.section.overview", module_id="admin", kind="section", label="Administration overview", order=0), ViewSurface(
ViewSurface(id="admin.section.system-settings", module_id="admin", kind="section", label="System settings", order=10), id="admin.section.overview",
ViewSurface(id="admin.section.system-language-packages", module_id="admin", kind="section", label="Language packages", order=15), module_id="admin",
ViewSurface(id="admin.section.system-configuration-changes", module_id="admin", kind="section", label="Configuration changes", order=20), kind="section",
ViewSurface(id="admin.section.system-configuration-packages", module_id="admin", kind="section", label="Configuration packages", order=30), label="Administration overview",
ViewSurface(id="admin.section.system-role-templates", module_id="admin", kind="section", label="Role templates", order=40), order=0,
ViewSurface(id="admin.section.system-groups", module_id="admin", kind="section", label="Group templates", order=50), ),
ViewSurface(id="admin.section.system-modules", module_id="admin", kind="section", label="Modules", order=85), ViewSurface(
ViewSurface(id="admin.section.system-tenant-modules", module_id="admin", kind="section", label="Tenant modules", order=86), id="admin.section.system-settings",
ViewSurface(id="admin.section.tenant-data-subject-requests", module_id="admin", kind="section", label="Data-subject requests", order=55), module_id="admin",
ViewSurface(id="admin.section.tenant-modules", module_id="admin", kind="section", label="Modules", order=60), kind="section",
label="System settings",
order=10,
),
ViewSurface(
id="admin.section.system-language-packages",
module_id="admin",
kind="section",
label="Language packages",
order=15,
),
ViewSurface(
id="admin.section.system-configuration-changes",
module_id="admin",
kind="section",
label="Configuration changes",
order=20,
),
ViewSurface(
id="admin.section.system-configuration-packages",
module_id="admin",
kind="section",
label="Configuration packages",
order=30,
),
ViewSurface(
id="admin.section.system-role-templates",
module_id="admin",
kind="section",
label="Role templates",
order=40,
),
ViewSurface(
id="admin.section.system-groups",
module_id="admin",
kind="section",
label="Group templates",
order=50,
),
ViewSurface(
id="admin.section.system-modules",
module_id="admin",
kind="section",
label="Modules",
order=85,
),
ViewSurface(
id="admin.section.system-tenant-modules",
module_id="admin",
kind="section",
label="Tenant modules",
order=86,
),
ViewSurface(
id="admin.section.tenant-data-subject-requests",
module_id="admin",
kind="section",
label="Data-subject requests",
order=55,
),
ViewSurface(
id="admin.section.tenant-modules",
module_id="admin",
kind="section",
label="Modules",
order=60,
),
), ),
), ),
migration_spec=MigrationSpec(module_id="admin", metadata=Base.metadata), migration_spec=MigrationSpec(module_id="admin", metadata=Base.metadata),
@@ -263,9 +386,19 @@ manifest = ModuleManifest(
maturity="vertical_slice", maturity="vertical_slice",
documentation_ref="README.md", documentation_ref="README.md",
test_ref="tests/test_catalog_plan.py", test_ref="tests/test_catalog_plan.py",
known_limits=("Some module-specific administration surfaces still own their own navigation and release evidence.",), known_limits=(
owned_concepts=("administration workspace", "configuration package workflow", "module lifecycle request"), "Some module-specific administration surfaces still own their own navigation and release evidence.",
non_owned_concepts=("module installation effect", "access policy", "module-owned settings"), ),
owned_concepts=(
"administration workspace",
"configuration package workflow",
"module lifecycle request",
),
non_owned_concepts=(
"module installation effect",
"access policy",
"module-owned settings",
),
operations_docs=("README.md",), operations_docs=("README.md",),
), ),
) )
+112
View File
@@ -0,0 +1,112 @@
from __future__ import annotations
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_admin.backend.dsar_provider import (
ADMIN_DSAR_CAPABILITY,
AdminDsarProvider,
)
from govoplan_admin.backend.manifest import manifest
from govoplan_core.core.dsar import DsarProvider, DsarSubjectRef
from govoplan_core.db.base import Base
from govoplan_core.privacy.dsar_workflow import (
create_data_subject_request,
search_data_subject_request,
)
class _Registry:
def __init__(self, provider: AdminDsarProvider) -> None:
self.provider = provider
def capability_names(self):
return (ADMIN_DSAR_CAPABILITY,)
def capability_owner(self, name):
if name != ADMIN_DSAR_CAPABILITY:
raise KeyError(name)
return "admin"
def tenant_entitlement_resolver(self):
class _Resolver:
@staticmethod
def resolve(session, tenant_id):
del session, tenant_id
return type("State", (), {"effective_modules": ("admin",)})()
return _Resolver()
def require_tenant_capability(self, name, session, **kwargs):
del session, kwargs
if name != ADMIN_DSAR_CAPABILITY:
raise KeyError(name)
return self.provider
def manifests(self):
return (type("Manifest", (), {"id": "admin"})(),)
class AdminDsarProviderTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(self.engine)
self.session = Session(self.engine)
self.provider = AdminDsarProvider()
def tearDown(self) -> None:
self.session.close()
self.engine.dispose()
def test_provider_explicitly_reports_no_subject_records(self) -> None:
self.assertIsInstance(self.provider, DsarProvider)
subject = DsarSubjectRef(account_id="account-1")
self.assertEqual(
(),
self.provider.search_subject(
self.session, tenant_id="tenant-1", subject=subject
),
)
self.assertEqual(
(),
self.provider.plan_erasure(
self.session,
tenant_id="tenant-1",
subject=subject,
records=(),
),
)
def test_manifest_and_workflow_record_reviewed_coverage(self) -> None:
self.assertIn(ADMIN_DSAR_CAPABILITY, manifest.capability_factories)
self.assertIn(
"admin.data-subject-request-coverage",
{topic.id for topic in manifest.documentation},
)
row = create_data_subject_request(
self.session,
tenant_id="tenant-1",
reference="DSAR-ADMIN-1",
request_kind="access",
subject=DsarSubjectRef(account_id="account-1"),
purpose="Administration coverage review",
legal_basis=None,
due_at=None,
requested_by_account_id="operator-1",
)
search_data_subject_request(
self.session,
registry=_Registry(self.provider),
row=row,
expected_revision=row.resource_revision,
)
self.assertEqual("searched", row.status)
self.assertEqual(0, row.search_result["record_count"])
self.assertEqual(["admin"], row.coverage["covered_modules"])
self.assertEqual([], row.coverage["modules_without_provider"])
if __name__ == "__main__":
unittest.main()