feat: enforce Mail-owned campaign transport boundary
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
||||
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
|
||||
from govoplan_core.core.modules import (
|
||||
@@ -23,6 +26,40 @@ from govoplan_mail.backend.documentation import documentation_topics
|
||||
from govoplan_mail.backend.db import models as mail_models # noqa: F401 - populate Mail ORM metadata
|
||||
|
||||
|
||||
_mail_table_retirement_provider = drop_table_retirement_provider(
|
||||
mail_models.MailServerProfile,
|
||||
mail_models.MailProfilePolicy,
|
||||
mail_models.MailMailboxFolderIndex,
|
||||
mail_models.MailMailboxMessageIndex,
|
||||
label="Mail",
|
||||
)
|
||||
|
||||
|
||||
def _mail_retirement_provider(session: object | None, module_id: str):
|
||||
plan = _mail_table_retirement_provider(session, module_id)
|
||||
base_executor = plan.destroy_data_executor
|
||||
if base_executor is None:
|
||||
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 Mail credential retirement.")
|
||||
if inspect(execute_session.get_bind()).has_table(mail_models.MailServerProfile.__tablename__):
|
||||
from govoplan_mail.backend.mail_profiles import delete_mail_profile_credentials_for_retirement
|
||||
|
||||
delete_mail_profile_credentials_for_retirement(execute_session)
|
||||
base_executor(execute_session, execute_module_id)
|
||||
|
||||
return replace(
|
||||
plan,
|
||||
destroy_data_warnings=(
|
||||
*plan.destroy_data_warnings,
|
||||
"Mail-owned encrypted SMTP/IMAP passwords are scrubbed with non-secret audit records immediately before tables are dropped; any scrub or audit failure blocks retirement.",
|
||||
),
|
||||
destroy_data_executor=executor,
|
||||
)
|
||||
|
||||
|
||||
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
||||
module_id, resource, action = scope.split(":", 2)
|
||||
return PermissionDefinition(
|
||||
@@ -89,11 +126,11 @@ def _mail_router(context: ModuleContext):
|
||||
manifest = ModuleManifest(
|
||||
id="mail",
|
||||
name="Mail",
|
||||
version="0.1.8",
|
||||
version="0.1.9",
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
optional_dependencies=("campaigns", "addresses"),
|
||||
provides_interfaces=(
|
||||
ModuleInterfaceProvider(name="mail.campaign_delivery", version="0.1.6"),
|
||||
ModuleInterfaceProvider(name="mail.campaign_delivery", version="0.2.0"),
|
||||
),
|
||||
requires_interfaces=(
|
||||
ModuleInterfaceRequirement(
|
||||
@@ -123,14 +160,8 @@ manifest = ModuleManifest(
|
||||
metadata=Base.metadata,
|
||||
script_location=str(Path(__file__).with_name("migrations") / "versions"),
|
||||
retirement_supported=True,
|
||||
retirement_provider=drop_table_retirement_provider(
|
||||
mail_models.MailServerProfile,
|
||||
mail_models.MailProfilePolicy,
|
||||
mail_models.MailMailboxFolderIndex,
|
||||
mail_models.MailMailboxMessageIndex,
|
||||
label="Mail",
|
||||
),
|
||||
retirement_notes="Destructive retirement drops mail-owned database tables after the installer captures a database snapshot.",
|
||||
retirement_provider=_mail_retirement_provider,
|
||||
retirement_notes="Destructive retirement first scrubs and audits Mail-owned credentials, then drops Mail-owned database tables after the installer captures a database snapshot.",
|
||||
),
|
||||
uninstall_guard_providers=(
|
||||
persistent_table_uninstall_guard(
|
||||
@@ -149,7 +180,7 @@ manifest = ModuleManifest(
|
||||
id="mail.profiles-and-policy",
|
||||
title="Mail profiles and policy hierarchy",
|
||||
summary="Mail sending is configured through reusable SMTP/IMAP profiles and an effective policy assembled from system, tenant, owner, and campaign scope where applicable.",
|
||||
body="The active policy decides whether users can only choose approved profiles or whether user, group, and campaign scopes may define their own mail-server settings. Runtime documentation adds the current tenant posture when the actor may read mail profile policy.",
|
||||
body="The active policy decides whether users can only choose approved profiles or whether user, group, and campaign scopes may define additional reusable profiles. Runtime documentation adds the current tenant posture when the actor may read mail profile policy.",
|
||||
layer="configured",
|
||||
documentation_types=("admin",),
|
||||
audience=("tenant_admin", "mail_admin", "campaign_admin"),
|
||||
@@ -170,6 +201,51 @@ manifest = ModuleManifest(
|
||||
related_modules=("campaigns",),
|
||||
unlocks=("Campaign-local delivery rules become visible when govoplan-campaign is installed.",),
|
||||
configuration_keys=("mail_profile_policy",),
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"route": "/mail",
|
||||
"screen": "Mail profiles and policy",
|
||||
"section": "Effective profile policy",
|
||||
"related_topic_ids": [
|
||||
"mail.profile-ownership-and-consumers",
|
||||
"campaigns.mail-profile-governance",
|
||||
],
|
||||
},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="mail.profile-ownership-and-consumers",
|
||||
title="Mail owns transport profiles and credentials",
|
||||
summary="Other modules select authorized Mail profiles by stable identifier; they do not copy SMTP/IMAP settings or secrets.",
|
||||
body="Mail encrypts credentials, tests connections, evaluates profile scope and policy, and performs revision-gated transport effects without returning resolved configuration. Random persisted revisions rotate when normalized transport or account identity changes, while password-only rotation remains transparent to built business intent. Campaign stores only server.mail_profile_id plus sanitized delivery evidence. Campaign-local transport fields are rejected, and legacy campaign records fail closed until an explicit profile migration preserves the source audit record and updates an editable version.",
|
||||
layer="available",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("mail_user", "mail_admin", "campaign_manager", "campaign_sender"),
|
||||
order=40,
|
||||
conditions=(
|
||||
DocumentationCondition(
|
||||
required_modules=("mail",),
|
||||
any_scopes=("mail:profile:read", "mail:profile:use", "mail:profile:write"),
|
||||
),
|
||||
),
|
||||
links=(
|
||||
DocumentationLink(label="Mail profiles", href="/mail", kind="runtime"),
|
||||
DocumentationLink(label="Mail profiles API", href="/api/v1/mail/profiles", kind="api"),
|
||||
DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"),
|
||||
),
|
||||
related_modules=("campaigns", "access"),
|
||||
unlocks=("Reusable, governed delivery identities without cross-module secret duplication.",),
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"route": "/mail",
|
||||
"screen": "Mail profiles",
|
||||
"section": "Profile ownership, credentials, and consumers",
|
||||
"related_topic_ids": [
|
||||
"mail.profiles-and-policy",
|
||||
"campaigns.mail-profile-user-journey",
|
||||
"campaigns.mail-profile-governance",
|
||||
"campaigns.mail-profile-operations",
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
documentation_providers=(documentation_topics,),
|
||||
|
||||
Reference in New Issue
Block a user