feat(addresses): add governed DSAR coverage
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -11,12 +11,21 @@ from govoplan_addresses.backend.capabilities import (
|
||||
CAPABILITY_ADDRESSES_RECIPIENT_SOURCE,
|
||||
)
|
||||
from govoplan_addresses.backend.db import models as addresses_models # noqa: F401 - populate address ORM metadata
|
||||
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
||||
from govoplan_core.core.contact_points import CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION
|
||||
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
|
||||
from govoplan_core.core.access import (
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||
)
|
||||
from govoplan_core.core.contact_points import (
|
||||
CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION,
|
||||
)
|
||||
from govoplan_core.core.module_guards import (
|
||||
drop_table_retirement_provider,
|
||||
persistent_table_uninstall_guard,
|
||||
)
|
||||
from govoplan_core.core.people import CAPABILITY_ADDRESSES_PEOPLE_SEARCH
|
||||
from govoplan_core.core.distribution_lists import CAPABILITY_RECIPIENT_CHANNEL_FACTS
|
||||
from govoplan_core.core.modules import (
|
||||
CapabilityDocumentation,
|
||||
DocumentationTopic,
|
||||
FrontendModule,
|
||||
FrontendRoute,
|
||||
@@ -38,6 +47,7 @@ from govoplan_core.core.provider_governance import (
|
||||
)
|
||||
from govoplan_core.core.views import ViewSurface
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_addresses.backend.dsar_provider import ADDRESSES_DSAR_CAPABILITY
|
||||
from govoplan_addresses.backend.provider_state import (
|
||||
CARDDAV_PROVIDER_ID,
|
||||
LDAP_PROVIDER_ID,
|
||||
@@ -46,6 +56,13 @@ from govoplan_addresses.backend.provider_state import (
|
||||
)
|
||||
|
||||
|
||||
def _addresses_dsar_provider(context: ModuleContext) -> object:
|
||||
del context
|
||||
from govoplan_addresses.backend.dsar_provider import AddressesDsarProvider
|
||||
|
||||
return AddressesDsarProvider()
|
||||
|
||||
|
||||
_addresses_table_retirement_provider = drop_table_retirement_provider(
|
||||
addresses_models.AddressImportRun,
|
||||
addresses_models.AddressImportProfile,
|
||||
@@ -77,10 +94,18 @@ def _addresses_retirement_provider(session: object | None, module_id: str):
|
||||
return plan
|
||||
|
||||
def executor(execute_session: object, execute_module_id: str) -> None:
|
||||
if not hasattr(execute_session, "get_bind") or not hasattr(execute_session, "query"):
|
||||
raise RuntimeError("No database session is available for Addresses credential retirement.")
|
||||
if inspect(execute_session.get_bind()).has_table(addresses_models.AddressSyncSource.__tablename__):
|
||||
from govoplan_addresses.backend.service import audit_address_credentials_for_retirement
|
||||
if not hasattr(execute_session, "get_bind") or not hasattr(
|
||||
execute_session, "query"
|
||||
):
|
||||
raise RuntimeError(
|
||||
"No database session is available for Addresses credential retirement."
|
||||
)
|
||||
if inspect(execute_session.get_bind()).has_table(
|
||||
addresses_models.AddressSyncSource.__tablename__
|
||||
):
|
||||
from govoplan_addresses.backend.service import (
|
||||
audit_address_credentials_for_retirement,
|
||||
)
|
||||
|
||||
audit_address_credentials_for_retirement(execute_session)
|
||||
base_executor(execute_session, execute_module_id)
|
||||
@@ -110,21 +135,77 @@ def _permission(scope: str, label: str, description: str) -> PermissionDefinitio
|
||||
|
||||
|
||||
PERMISSIONS = (
|
||||
_permission("addresses:address_book:read", "View address books", "List address books visible to the current principal."),
|
||||
_permission("addresses:address_book:write", "Manage address books", "Create and edit local address books."),
|
||||
_permission("addresses:address_book:delete", "Delete address books", "Soft-delete local address books."),
|
||||
_permission("addresses:address_book:admin", "Administer address books", "Manage system-scoped address books and future sync sources."),
|
||||
_permission("addresses:address_list:read", "View address lists", "List reusable address lists and their entries."),
|
||||
_permission("addresses:address_list:write", "Manage address lists", "Create and edit reusable address lists."),
|
||||
_permission("addresses:address_list:delete", "Delete address lists", "Soft-delete reusable address lists."),
|
||||
_permission("addresses:contact:read", "View contacts", "List and lookup contacts in visible address books."),
|
||||
_permission("addresses:contact:write", "Manage contacts", "Create and edit local contacts."),
|
||||
_permission("addresses:contact:delete", "Delete contacts", "Soft-delete local contacts."),
|
||||
_permission("addresses:governance:read", "View communication governance", "Inspect effective-dated consent, suppression, and channel-preference facts."),
|
||||
_permission("addresses:governance:write", "Manage communication governance", "Record and end consent, suppression, and channel-preference facts."),
|
||||
_permission("addresses:sync:read", "View address sync", "Inspect address sync sources, conflicts, tombstones, and diagnostics."),
|
||||
_permission("addresses:sync:write", "Manage address sync", "Bind address books to external sources and record sync state."),
|
||||
_permission("addresses:sync:admin", "Administer address sync", "Administer address sync connectors and future destructive sync operations."),
|
||||
_permission(
|
||||
"addresses:address_book:read",
|
||||
"View address books",
|
||||
"List address books visible to the current principal.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:address_book:write",
|
||||
"Manage address books",
|
||||
"Create and edit local address books.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:address_book:delete",
|
||||
"Delete address books",
|
||||
"Soft-delete local address books.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:address_book:admin",
|
||||
"Administer address books",
|
||||
"Manage system-scoped address books and future sync sources.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:address_list:read",
|
||||
"View address lists",
|
||||
"List reusable address lists and their entries.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:address_list:write",
|
||||
"Manage address lists",
|
||||
"Create and edit reusable address lists.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:address_list:delete",
|
||||
"Delete address lists",
|
||||
"Soft-delete reusable address lists.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:contact:read",
|
||||
"View contacts",
|
||||
"List and lookup contacts in visible address books.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:contact:write", "Manage contacts", "Create and edit local contacts."
|
||||
),
|
||||
_permission(
|
||||
"addresses:contact:delete", "Delete contacts", "Soft-delete local contacts."
|
||||
),
|
||||
_permission(
|
||||
"addresses:governance:read",
|
||||
"View communication governance",
|
||||
"Inspect effective-dated consent, suppression, and channel-preference facts.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:governance:write",
|
||||
"Manage communication governance",
|
||||
"Record and end consent, suppression, and channel-preference facts.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:sync:read",
|
||||
"View address sync",
|
||||
"Inspect address sync sources, conflicts, tombstones, and diagnostics.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:sync:write",
|
||||
"Manage address sync",
|
||||
"Bind address books to external sources and record sync state.",
|
||||
),
|
||||
_permission(
|
||||
"addresses:sync:admin",
|
||||
"Administer address sync",
|
||||
"Administer address sync connectors and future destructive sync operations.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -153,7 +234,13 @@ ROLE_TEMPLATES = (
|
||||
slug="address_book_reader",
|
||||
name="Address book reader",
|
||||
description="Read visible address books and contacts.",
|
||||
permissions=("addresses:address_book:read", "addresses:address_list:read", "addresses:contact:read", "addresses:governance:read", "addresses:sync:read"),
|
||||
permissions=(
|
||||
"addresses:address_book:read",
|
||||
"addresses:address_list:read",
|
||||
"addresses:contact:read",
|
||||
"addresses:governance:read",
|
||||
"addresses:sync:read",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -172,15 +259,42 @@ def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
|
||||
)
|
||||
|
||||
return {
|
||||
"address_books": session.query(AddressBook).filter(AddressBook.tenant_id == tenant_id, AddressBook.deleted_at.is_(None)).count(),
|
||||
"address_lists": session.query(AddressList).filter(AddressList.tenant_id == tenant_id, AddressList.deleted_at.is_(None)).count(),
|
||||
"contacts": session.query(Contact).filter(Contact.tenant_id == tenant_id, Contact.deleted_at.is_(None)).count(),
|
||||
"active_contact_merges": session.query(ContactMergeRecord).filter(ContactMergeRecord.tenant_id == tenant_id, ContactMergeRecord.status == "active").count(),
|
||||
"contact_quality_decisions": session.query(ContactPointQualityDecision).filter(ContactPointQualityDecision.tenant_id == tenant_id).count(),
|
||||
"contact_point_snapshots": session.query(ContactPointSnapshot).filter(ContactPointSnapshot.tenant_id == tenant_id).count(),
|
||||
"sync_sources": session.query(AddressSyncSource).filter(AddressSyncSource.tenant_id == tenant_id, AddressSyncSource.enabled.is_(True)).count(),
|
||||
"address_import_profiles": session.query(AddressImportProfile).filter(AddressImportProfile.tenant_id == tenant_id, AddressImportProfile.is_current.is_(True)).count(),
|
||||
"address_import_runs": session.query(AddressImportRun).filter(AddressImportRun.tenant_id == tenant_id).count(),
|
||||
"address_books": session.query(AddressBook)
|
||||
.filter(AddressBook.tenant_id == tenant_id, AddressBook.deleted_at.is_(None))
|
||||
.count(),
|
||||
"address_lists": session.query(AddressList)
|
||||
.filter(AddressList.tenant_id == tenant_id, AddressList.deleted_at.is_(None))
|
||||
.count(),
|
||||
"contacts": session.query(Contact)
|
||||
.filter(Contact.tenant_id == tenant_id, Contact.deleted_at.is_(None))
|
||||
.count(),
|
||||
"active_contact_merges": session.query(ContactMergeRecord)
|
||||
.filter(
|
||||
ContactMergeRecord.tenant_id == tenant_id,
|
||||
ContactMergeRecord.status == "active",
|
||||
)
|
||||
.count(),
|
||||
"contact_quality_decisions": session.query(ContactPointQualityDecision)
|
||||
.filter(ContactPointQualityDecision.tenant_id == tenant_id)
|
||||
.count(),
|
||||
"contact_point_snapshots": session.query(ContactPointSnapshot)
|
||||
.filter(ContactPointSnapshot.tenant_id == tenant_id)
|
||||
.count(),
|
||||
"sync_sources": session.query(AddressSyncSource)
|
||||
.filter(
|
||||
AddressSyncSource.tenant_id == tenant_id,
|
||||
AddressSyncSource.enabled.is_(True),
|
||||
)
|
||||
.count(),
|
||||
"address_import_profiles": session.query(AddressImportProfile)
|
||||
.filter(
|
||||
AddressImportProfile.tenant_id == tenant_id,
|
||||
AddressImportProfile.is_current.is_(True),
|
||||
)
|
||||
.count(),
|
||||
"address_import_runs": session.query(AddressImportRun)
|
||||
.filter(AddressImportRun.tenant_id == tenant_id)
|
||||
.count(),
|
||||
}
|
||||
|
||||
|
||||
@@ -200,13 +314,28 @@ CARDDAV_PROVIDER = ExternalProviderDeclaration(
|
||||
ProviderObjectDeclaration(
|
||||
object_type="address_book",
|
||||
field_groups=("identity", "display", "sync_state"),
|
||||
authority_modes=("external_authoritative", "external_mirror", "governed_sync"),
|
||||
authority_modes=(
|
||||
"external_authoritative",
|
||||
"external_mirror",
|
||||
"governed_sync",
|
||||
),
|
||||
default_authority_mode="external_mirror",
|
||||
),
|
||||
ProviderObjectDeclaration(
|
||||
object_type="contact",
|
||||
field_groups=("identity", "name", "postal", "email", "phone", "source_metadata"),
|
||||
authority_modes=("external_authoritative", "external_mirror", "governed_sync"),
|
||||
field_groups=(
|
||||
"identity",
|
||||
"name",
|
||||
"postal",
|
||||
"email",
|
||||
"phone",
|
||||
"source_metadata",
|
||||
),
|
||||
authority_modes=(
|
||||
"external_authoritative",
|
||||
"external_mirror",
|
||||
"governed_sync",
|
||||
),
|
||||
default_authority_mode="governed_sync",
|
||||
),
|
||||
),
|
||||
@@ -252,7 +381,15 @@ LDAP_PROVIDER = ExternalProviderDeclaration(
|
||||
objects=(
|
||||
ProviderObjectDeclaration(
|
||||
object_type="contact",
|
||||
field_groups=("identity", "name", "organization", "postal", "email", "phone", "source_metadata"),
|
||||
field_groups=(
|
||||
"identity",
|
||||
"name",
|
||||
"organization",
|
||||
"postal",
|
||||
"email",
|
||||
"phone",
|
||||
"source_metadata",
|
||||
),
|
||||
authority_modes=("external_authoritative", "external_mirror"),
|
||||
default_authority_mode="external_authoritative",
|
||||
),
|
||||
@@ -281,7 +418,11 @@ LDAP_PROVIDER = ExternalProviderDeclaration(
|
||||
reconciliation="Only a complete paged search may infer an absent source object and create a local tombstone.",
|
||||
outage="Existing contacts remain available and visibly stale; an unavailable directory never causes deletes.",
|
||||
classifications=("personal", "confidential", "restricted"),
|
||||
purposes=("directory projection", "recipient resolution", "identity-linked contact discovery"),
|
||||
purposes=(
|
||||
"directory projection",
|
||||
"recipient resolution",
|
||||
"identity-linked contact discovery",
|
||||
),
|
||||
retention="Address, audit, and records policies govern local projections and tombstone evidence.",
|
||||
secret_handling="Bind secrets remain in reusable credential envelopes; URLs, previews, and diagnostics contain no credentials.",
|
||||
),
|
||||
@@ -294,26 +435,71 @@ manifest = ModuleManifest(
|
||||
id="addresses",
|
||||
name="Addresses",
|
||||
version="0.1.18",
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
optional_dependencies=("campaigns", "mail", "forms", "reporting", "portal", "postbox", "connectors"),
|
||||
required_capabilities=(
|
||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
),
|
||||
optional_dependencies=(
|
||||
"campaigns",
|
||||
"mail",
|
||||
"forms",
|
||||
"reporting",
|
||||
"portal",
|
||||
"postbox",
|
||||
"connectors",
|
||||
),
|
||||
provides_interfaces=(
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_LOOKUP, version="0.1.8"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_PEOPLE_SEARCH, version="0.1.0"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_RECIPIENT_SOURCE, version="0.1.9"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION, version="1.0.0"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_CONTACT_WRITER, version="0.1.8"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_RECIPIENT_CHANNEL_FACTS, version="0.1.0"),
|
||||
ModuleInterfaceProvider(
|
||||
name=CAPABILITY_ADDRESSES_PEOPLE_SEARCH, version="0.1.0"
|
||||
),
|
||||
ModuleInterfaceProvider(
|
||||
name=CAPABILITY_ADDRESSES_RECIPIENT_SOURCE, version="0.1.9"
|
||||
),
|
||||
ModuleInterfaceProvider(
|
||||
name=CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION, version="1.0.0"
|
||||
),
|
||||
ModuleInterfaceProvider(
|
||||
name=CAPABILITY_ADDRESSES_CONTACT_WRITER, version="0.1.8"
|
||||
),
|
||||
ModuleInterfaceProvider(
|
||||
name=CAPABILITY_RECIPIENT_CHANNEL_FACTS, version="0.1.0"
|
||||
),
|
||||
ModuleInterfaceProvider(name=ADDRESSES_DSAR_CAPABILITY, version="0.1.0"),
|
||||
),
|
||||
permissions=PERMISSIONS,
|
||||
route_factory=_addresses_router,
|
||||
role_templates=ROLE_TEMPLATES,
|
||||
tenant_summary_providers=(_tenant_summary,),
|
||||
nav_items=(NavItem(path="/address-book", label="Address Book", icon="book-user", required_any=("addresses:contact:read",), order=80),),
|
||||
nav_items=(
|
||||
NavItem(
|
||||
path="/address-book",
|
||||
label="Address Book",
|
||||
icon="book-user",
|
||||
required_any=("addresses:contact:read",),
|
||||
order=80,
|
||||
),
|
||||
),
|
||||
frontend=FrontendModule(
|
||||
module_id="addresses",
|
||||
package_name="@govoplan/addresses-webui",
|
||||
routes=(FrontendRoute(path="/address-book", component="AddressBookPage", required_any=("addresses:contact:read",), order=80),),
|
||||
nav_items=(NavItem(path="/address-book", label="Address Book", icon="book-user", required_any=("addresses:contact:read",), order=80),),
|
||||
routes=(
|
||||
FrontendRoute(
|
||||
path="/address-book",
|
||||
component="AddressBookPage",
|
||||
required_any=("addresses:contact:read",),
|
||||
order=80,
|
||||
),
|
||||
),
|
||||
nav_items=(
|
||||
NavItem(
|
||||
path="/address-book",
|
||||
label="Address Book",
|
||||
icon="book-user",
|
||||
required_any=("addresses:contact:read",),
|
||||
order=80,
|
||||
),
|
||||
),
|
||||
product_areas=(
|
||||
ProductAreaContribution(
|
||||
id="people-responsibility",
|
||||
@@ -321,17 +507,56 @@ manifest = ModuleManifest(
|
||||
label="i18n:govoplan-core.product_area.people_responsibility",
|
||||
icon="users",
|
||||
description="i18n:govoplan-core.product_area.people_responsibility_description",
|
||||
surface_ids=("addresses.nav.address.book", "addresses.route.address.book"),
|
||||
surface_ids=(
|
||||
"addresses.nav.address.book",
|
||||
"addresses.route.address.book",
|
||||
),
|
||||
order=70,
|
||||
),
|
||||
),
|
||||
view_surfaces=(
|
||||
ViewSurface(id="addresses.page", module_id="addresses", kind="route", label="Address Book", order=80),
|
||||
ViewSurface(id="addresses.sources", module_id="addresses", kind="section", label="Address sources", order=10),
|
||||
ViewSurface(id="addresses.contacts", module_id="addresses", kind="section", label="Contacts", order=20),
|
||||
ViewSurface(id="addresses.detail", module_id="addresses", kind="section", label="Contact detail", order=30),
|
||||
ViewSurface(id="addresses.governance", module_id="addresses", kind="action", label="Communication governance", order=40),
|
||||
ViewSurface(id="addresses.sync", module_id="addresses", kind="action", label="Address synchronization", order=50),
|
||||
ViewSurface(
|
||||
id="addresses.page",
|
||||
module_id="addresses",
|
||||
kind="route",
|
||||
label="Address Book",
|
||||
order=80,
|
||||
),
|
||||
ViewSurface(
|
||||
id="addresses.sources",
|
||||
module_id="addresses",
|
||||
kind="section",
|
||||
label="Address sources",
|
||||
order=10,
|
||||
),
|
||||
ViewSurface(
|
||||
id="addresses.contacts",
|
||||
module_id="addresses",
|
||||
kind="section",
|
||||
label="Contacts",
|
||||
order=20,
|
||||
),
|
||||
ViewSurface(
|
||||
id="addresses.detail",
|
||||
module_id="addresses",
|
||||
kind="section",
|
||||
label="Contact detail",
|
||||
order=30,
|
||||
),
|
||||
ViewSurface(
|
||||
id="addresses.governance",
|
||||
module_id="addresses",
|
||||
kind="action",
|
||||
label="Communication governance",
|
||||
order=40,
|
||||
),
|
||||
ViewSurface(
|
||||
id="addresses.sync",
|
||||
module_id="addresses",
|
||||
kind="action",
|
||||
label="Address synchronization",
|
||||
order=50,
|
||||
),
|
||||
),
|
||||
),
|
||||
migration_spec=MigrationSpec(
|
||||
@@ -343,8 +568,13 @@ manifest = ModuleManifest(
|
||||
retirement_notes="Destructive retirement drops address-owned database tables after the installer captures a database snapshot.",
|
||||
),
|
||||
capability_factories={
|
||||
CAPABILITY_ADDRESSES_LOOKUP: lambda context: __import__("govoplan_addresses.backend.capabilities", fromlist=["lookup_capability"]).lookup_capability(context),
|
||||
CAPABILITY_ADDRESSES_PEOPLE_SEARCH: lambda context: __import__("govoplan_addresses.backend.capabilities", fromlist=["people_search_capability"]).people_search_capability(context),
|
||||
CAPABILITY_ADDRESSES_LOOKUP: lambda context: __import__(
|
||||
"govoplan_addresses.backend.capabilities", fromlist=["lookup_capability"]
|
||||
).lookup_capability(context),
|
||||
CAPABILITY_ADDRESSES_PEOPLE_SEARCH: lambda context: __import__(
|
||||
"govoplan_addresses.backend.capabilities",
|
||||
fromlist=["people_search_capability"],
|
||||
).people_search_capability(context),
|
||||
CAPABILITY_ADDRESSES_RECIPIENT_SOURCE: lambda context: __import__(
|
||||
"govoplan_addresses.backend.capabilities",
|
||||
fromlist=["recipient_source_capability"],
|
||||
@@ -361,6 +591,20 @@ manifest = ModuleManifest(
|
||||
"govoplan_addresses.backend.capabilities",
|
||||
fromlist=["contact_point_resolution_capability"],
|
||||
).contact_point_resolution_capability(context),
|
||||
ADDRESSES_DSAR_CAPABILITY: _addresses_dsar_provider,
|
||||
},
|
||||
capability_documentation={
|
||||
ADDRESSES_DSAR_CAPABILITY: CapabilityDocumentation(
|
||||
label="Addresses data-subject request provider",
|
||||
summary=(
|
||||
"Finds bounded contact, contact-point, address-list, governance, "
|
||||
"provenance, synchronization, and operator-attribution data without "
|
||||
"exporting raw source payloads, connector state, or opaque evidence."
|
||||
),
|
||||
contract_version="0.1.0",
|
||||
documentation_types=("admin",),
|
||||
audience=("privacy_officer", "addresses_admin", "records_manager"),
|
||||
),
|
||||
},
|
||||
uninstall_guard_providers=(
|
||||
persistent_table_uninstall_guard(
|
||||
@@ -387,6 +631,57 @@ manifest = ModuleManifest(
|
||||
),
|
||||
),
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="addresses.privacy.data-subject-requests",
|
||||
title="Review Addresses data in a data-subject request",
|
||||
summary=(
|
||||
"Collect tenant-scoped contact data while preserving shared address, "
|
||||
"recipient, synchronization, and provenance evidence."
|
||||
),
|
||||
body=(
|
||||
"Addresses searches corroborated email and account selectors plus "
|
||||
"namespaced contact and contact-point references. A matching contact "
|
||||
"exports bounded identity, email, telephone, and postal values together "
|
||||
"with its address-list use and minimized governance, quality, provenance, "
|
||||
"merge, redirect, and synchronization evidence. Account matches add only "
|
||||
"minimized operator attribution for governed configuration and evidence. "
|
||||
"The provider excludes raw imported or synchronized source payloads, "
|
||||
"connector tokens and revisions that could act as credentials, opaque "
|
||||
"metadata, snapshot request and resolution payloads, import plans, merge "
|
||||
"before/after payloads, unrelated contacts, and other tenants. Quality, "
|
||||
"governance, provenance, merge, redirect, synchronization, import, "
|
||||
"snapshot, and operator evidence is retained with an explicit reason. "
|
||||
"Because reusable contacts can be shared, synchronized, merged, or "
|
||||
"referenced by immutable recipient snapshots, the DSAR provider never "
|
||||
"deletes them automatically. An authorized operator must review "
|
||||
"dependencies and use the normal Addresses correction, archive, source, "
|
||||
"merge, or governance workflow."
|
||||
),
|
||||
layer="static",
|
||||
documentation_types=("admin",),
|
||||
audience=(
|
||||
"privacy_officer",
|
||||
"addresses_admin",
|
||||
"records_manager",
|
||||
"operator",
|
||||
),
|
||||
related_modules=(
|
||||
"access",
|
||||
"audit",
|
||||
"campaigns",
|
||||
"dist_lists",
|
||||
"records",
|
||||
),
|
||||
order=29,
|
||||
metadata={
|
||||
"seed": True,
|
||||
"help_contexts": [
|
||||
"addresses.contacts",
|
||||
"addresses.governance",
|
||||
"addresses.action.archive",
|
||||
],
|
||||
},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="addresses.boundary",
|
||||
title="Reusable address ownership",
|
||||
@@ -399,7 +694,14 @@ manifest = ModuleManifest(
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("tenant_admin", "operator", "module_admin"),
|
||||
related_modules=("campaigns", "mail", "forms", "reporting", "portal", "postbox"),
|
||||
related_modules=(
|
||||
"campaigns",
|
||||
"mail",
|
||||
"forms",
|
||||
"reporting",
|
||||
"portal",
|
||||
"postbox",
|
||||
),
|
||||
order=30,
|
||||
metadata={
|
||||
"seed": True,
|
||||
@@ -482,7 +784,11 @@ manifest = ModuleManifest(
|
||||
order=34,
|
||||
metadata={
|
||||
"seed": True,
|
||||
"help_contexts": ["addresses.action.import", "addresses.contacts", "addresses.sources"],
|
||||
"help_contexts": [
|
||||
"addresses.action.import",
|
||||
"addresses.contacts",
|
||||
"addresses.sources",
|
||||
],
|
||||
},
|
||||
),
|
||||
DocumentationTopic(
|
||||
@@ -537,7 +843,14 @@ manifest = ModuleManifest(
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("tenant_admin", "operator", "module_admin", "power_user"),
|
||||
related_modules=("dist_lists", "connectors", "datasources", "campaigns", "policy", "audit"),
|
||||
related_modules=(
|
||||
"dist_lists",
|
||||
"connectors",
|
||||
"datasources",
|
||||
"campaigns",
|
||||
"policy",
|
||||
"audit",
|
||||
),
|
||||
order=36,
|
||||
metadata={
|
||||
"seed": True,
|
||||
@@ -579,15 +892,27 @@ manifest = ModuleManifest(
|
||||
maturity="vertical_slice",
|
||||
documentation_ref="docs/ADDRESS_MODULE_ARCHITECTURE.md",
|
||||
test_ref="tests/test_addresses_service.py",
|
||||
known_limits=("External address-book synchronization remains a bounded connector slice rather than a supported provider profile.",),
|
||||
known_limits=(
|
||||
"External address-book synchronization remains a bounded connector slice rather than a supported provider profile.",
|
||||
),
|
||||
supported_authority_modes=(
|
||||
"native_authoritative",
|
||||
"external_authoritative",
|
||||
"external_mirror",
|
||||
"governed_sync",
|
||||
),
|
||||
owned_concepts=("contact point", "address book", "contact consent", "recipient source"),
|
||||
non_owned_concepts=("identity", "organization", "campaign recipient snapshot", "procedure party"),
|
||||
owned_concepts=(
|
||||
"contact point",
|
||||
"address book",
|
||||
"contact consent",
|
||||
"recipient source",
|
||||
),
|
||||
non_owned_concepts=(
|
||||
"identity",
|
||||
"organization",
|
||||
"campaign recipient snapshot",
|
||||
"procedure party",
|
||||
),
|
||||
target_tested_providers=(CARDDAV_PROVIDER_ID,),
|
||||
security_docs=("docs/ADDRESS_MODULE_ARCHITECTURE.md",),
|
||||
operations_docs=("README.md",),
|
||||
|
||||
Reference in New Issue
Block a user