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 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.modules import (
|
||||
CapabilityDocumentation,
|
||||
DocumentationCondition,
|
||||
DocumentationTopic,
|
||||
FrontendModule,
|
||||
MigrationSpec,
|
||||
ModuleContext,
|
||||
ModuleInterfaceProvider,
|
||||
ModuleManifest,
|
||||
PermissionDefinition,
|
||||
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.views import ViewSurface
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_admin.backend.dsar_provider import (
|
||||
ADMIN_DSAR_CAPABILITY,
|
||||
AdminDsarProvider,
|
||||
)
|
||||
|
||||
|
||||
def _route_factory(context: ModuleContext):
|
||||
@@ -25,6 +34,10 @@ def _route_factory(context: ModuleContext):
|
||||
return router
|
||||
|
||||
|
||||
def _dsar_provider(_context: ModuleContext) -> AdminDsarProvider:
|
||||
return AdminDsarProvider()
|
||||
|
||||
|
||||
ADMIN_PERMISSIONS = (
|
||||
PermissionDefinition(
|
||||
scope="admin:module:read",
|
||||
@@ -68,9 +81,53 @@ manifest = ModuleManifest(
|
||||
version="0.1.18",
|
||||
permissions=ADMIN_PERMISSIONS,
|
||||
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,
|
||||
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(
|
||||
id="admin.workspace",
|
||||
title="Use the administration workspace",
|
||||
@@ -236,17 +293,83 @@ manifest = ModuleManifest(
|
||||
module_id="admin",
|
||||
package_name="@govoplan/admin-webui",
|
||||
view_surfaces=(
|
||||
ViewSurface(id="admin.section.overview", module_id="admin", kind="section", label="Administration overview", order=0),
|
||||
ViewSurface(id="admin.section.system-settings", module_id="admin", 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),
|
||||
ViewSurface(
|
||||
id="admin.section.overview",
|
||||
module_id="admin",
|
||||
kind="section",
|
||||
label="Administration overview",
|
||||
order=0,
|
||||
),
|
||||
ViewSurface(
|
||||
id="admin.section.system-settings",
|
||||
module_id="admin",
|
||||
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),
|
||||
@@ -263,9 +386,19 @@ manifest = ModuleManifest(
|
||||
maturity="vertical_slice",
|
||||
documentation_ref="README.md",
|
||||
test_ref="tests/test_catalog_plan.py",
|
||||
known_limits=("Some module-specific administration surfaces still own their own navigation and release evidence.",),
|
||||
owned_concepts=("administration workspace", "configuration package workflow", "module lifecycle request"),
|
||||
non_owned_concepts=("module installation effect", "access policy", "module-owned settings"),
|
||||
known_limits=(
|
||||
"Some module-specific administration surfaces still own their own navigation and release evidence.",
|
||||
),
|
||||
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",),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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