Add governed tabular and LDAP address sources
This commit is contained in:
@@ -38,11 +38,15 @@ from govoplan_core.core.provider_governance import (
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_addresses.backend.provider_state import (
|
||||
CARDDAV_PROVIDER_ID,
|
||||
LDAP_PROVIDER_ID,
|
||||
carddav_provider_states,
|
||||
ldap_provider_states,
|
||||
)
|
||||
|
||||
|
||||
_addresses_table_retirement_provider = drop_table_retirement_provider(
|
||||
addresses_models.AddressImportRun,
|
||||
addresses_models.AddressImportProfile,
|
||||
addresses_models.ContactFieldProvenance,
|
||||
addresses_models.ContactRedirect,
|
||||
addresses_models.ContactMergeRecord,
|
||||
@@ -155,6 +159,8 @@ ROLE_TEMPLATES = (
|
||||
def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
|
||||
from govoplan_addresses.backend.db.models import (
|
||||
AddressBook,
|
||||
AddressImportProfile,
|
||||
AddressImportRun,
|
||||
AddressList,
|
||||
AddressSyncSource,
|
||||
Contact,
|
||||
@@ -171,6 +177,8 @@ def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
|
||||
"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(),
|
||||
}
|
||||
|
||||
|
||||
@@ -233,12 +241,59 @@ CARDDAV_PROVIDER = ExternalProviderDeclaration(
|
||||
)
|
||||
|
||||
|
||||
LDAP_PROVIDER = ExternalProviderDeclaration(
|
||||
id=LDAP_PROVIDER_ID,
|
||||
module_id="addresses",
|
||||
label="Read-only LDAP and Active Directory contacts",
|
||||
maturity="synchronize",
|
||||
operations=("discover", "read", "preview", "synchronize"),
|
||||
objects=(
|
||||
ProviderObjectDeclaration(
|
||||
object_type="contact",
|
||||
field_groups=("identity", "name", "organization", "postal", "email", "phone", "source_metadata"),
|
||||
authority_modes=("external_authoritative", "external_mirror"),
|
||||
default_authority_mode="external_authoritative",
|
||||
),
|
||||
),
|
||||
behavior=ProviderBehaviorDeclaration(
|
||||
revision_tokens="Stable LDAP source keys plus modifyTimestamp, uSNChanged, entryCSN, or a deterministic attribute digest are retained.",
|
||||
concurrency="LDAP is authoritative and read-only; local projections are replaced only from a complete reviewed plan.",
|
||||
freshness="Last attempt, last success, remote revision, and stale provider health remain visible.",
|
||||
health="TLS, bind, discovery, paging, mapping, truncation, and malformed-entry failures are separate diagnostics.",
|
||||
max_read_items=10000,
|
||||
idempotency="The source binding, stable key, and revision prevent duplicate contact projections.",
|
||||
retry="Failed reads are retried only by a new operator or scheduled sync attempt with bounded timeouts.",
|
||||
timeout_seconds=120,
|
||||
conflicts="Duplicate source keys, malformed mappings, and locally changed projections block or require a fresh plan.",
|
||||
outcome_unknown="Read failures never infer external deletions and retain prior local projections as stale.",
|
||||
outcome_unknown_supported=True,
|
||||
evidence="Source keys, revisions, mapping configuration, diagnostics, tombstones, and normalized field provenance are retained.",
|
||||
audit_event_types=(
|
||||
"addresses.sync_source_created",
|
||||
"addresses.sync_previewed",
|
||||
"addresses.sync_completed",
|
||||
),
|
||||
correction="Correct the directory or mapping, then run a new full preview and synchronization.",
|
||||
rollback="Prior projections remain reconstructable from source revision and contact change evidence; external LDAP is never mutated.",
|
||||
compensation="A later authoritative refresh restores corrected projections.",
|
||||
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"),
|
||||
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.",
|
||||
),
|
||||
capability_names=(CAPABILITY_ADDRESSES_LOOKUP, CAPABILITY_ADDRESSES_CONTACT_WRITER),
|
||||
documentation_topic_ids=("addresses.ldap-directory",),
|
||||
)
|
||||
|
||||
|
||||
manifest = ModuleManifest(
|
||||
id="addresses",
|
||||
name="Addresses",
|
||||
version="0.1.9",
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
optional_dependencies=("campaigns", "mail", "forms", "reporting", "portal", "postbox"),
|
||||
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"),
|
||||
@@ -288,6 +343,8 @@ manifest = ModuleManifest(
|
||||
},
|
||||
uninstall_guard_providers=(
|
||||
persistent_table_uninstall_guard(
|
||||
addresses_models.AddressImportRun,
|
||||
addresses_models.AddressImportProfile,
|
||||
addresses_models.AddressSyncDiagnostic,
|
||||
addresses_models.AddressSyncConflict,
|
||||
addresses_models.AddressSyncTombstone,
|
||||
@@ -340,6 +397,40 @@ manifest = ModuleManifest(
|
||||
related_modules=("dist_lists", "campaigns", "policy", "templates"),
|
||||
order=31,
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="addresses.tabular-imports",
|
||||
title="CSV and XLSX contact imports",
|
||||
summary="Preview and apply reusable, versioned contact mappings without silent row loss.",
|
||||
body=(
|
||||
"CSV and XLSX files can be mapped with scoped, reusable profile versions. Each preview validates headers, "
|
||||
"encodings, source keys, duplicates, blank values, workbook limits, and contact identity before any mutation. "
|
||||
"The reviewed input hash and plan hash are retained with row-level effects and diagnostics. Apply is idempotent, "
|
||||
"rejects contacts changed after preview, and records sufficient evidence for a guarded rollback. XLSX formulas, "
|
||||
"macros, and legacy workbook formats are never executed or imported."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("tenant_admin", "module_admin", "power_user"),
|
||||
related_modules=("connectors", "datasources", "dataflow", "files", "audit"),
|
||||
order=33,
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="addresses.ldap-directory",
|
||||
title="LDAP and Active Directory address sources",
|
||||
summary="Project authoritative directory contacts through a bounded, read-only synchronization source.",
|
||||
body=(
|
||||
"LDAP sources use LDAPS or StartTLS and reusable credential envelopes. Discovery finds available base DNs; "
|
||||
"the source profile then controls a bounded paged filter and explicit attribute mapping. Preview never mutates "
|
||||
"contacts. A complete successful read may create, update, or tombstone local projections; truncated or failed "
|
||||
"reads suppress absence-based deletes and mark the source stale. Stable source keys, revisions, normalized fields, "
|
||||
"and provenance remain attached to every retained contact."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("tenant_admin", "operator", "module_admin"),
|
||||
related_modules=("connectors", "idm", "access", "policy", "audit"),
|
||||
order=34,
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="addresses.quality-and-merge",
|
||||
title="Contact quality, duplicates, and reversible merges",
|
||||
@@ -360,13 +451,18 @@ manifest = ModuleManifest(
|
||||
order=32,
|
||||
),
|
||||
),
|
||||
external_providers=(CARDDAV_PROVIDER,),
|
||||
external_providers=(CARDDAV_PROVIDER, LDAP_PROVIDER),
|
||||
external_provider_state_providers=(
|
||||
ExternalProviderStateProviderRegistration(
|
||||
module_id="addresses",
|
||||
provider_id=CARDDAV_PROVIDER_ID,
|
||||
provider=carddav_provider_states,
|
||||
),
|
||||
ExternalProviderStateProviderRegistration(
|
||||
module_id="addresses",
|
||||
provider_id=LDAP_PROVIDER_ID,
|
||||
provider=ldap_provider_states,
|
||||
),
|
||||
),
|
||||
architecture=declared_module_architecture(
|
||||
layer="communication_participation",
|
||||
|
||||
Reference in New Issue
Block a user