feat(admin): declare governed DSAR coverage
This commit is contained in:
@@ -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"]
|
||||||
@@ -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",),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user