feat: inventory SMTP infrastructure dependencies
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -5,7 +5,7 @@ from dataclasses import dataclass, replace
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import or_, select
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.configuration_packages import (
|
||||
@@ -24,6 +24,8 @@ from govoplan_core.core.configuration_packages import (
|
||||
from govoplan_core.core.infrastructure_capabilities import (
|
||||
InfrastructureCapability,
|
||||
InfrastructureCapabilityReceipt,
|
||||
InfrastructureDependency,
|
||||
InfrastructureDependencyProvider,
|
||||
)
|
||||
from govoplan_core.security.credential_envelopes import (
|
||||
CredentialAccessContext,
|
||||
@@ -51,6 +53,9 @@ from govoplan_mail.backend.server_hierarchy import (
|
||||
|
||||
|
||||
MAIL_CONFIGURATION_CAPABILITY = "mail.configuration"
|
||||
MAIL_INFRASTRUCTURE_DEPENDENCY_CAPABILITY = (
|
||||
"infrastructure.dependency_inventory.mail"
|
||||
)
|
||||
SMTP_PROFILE_FRAGMENT = "smtp_profile"
|
||||
_SYSTEM_CONFIGURATION_SCOPES = frozenset(
|
||||
{"system:settings:write", "system:governance:write"}
|
||||
@@ -132,8 +137,12 @@ class _ProfileTarget:
|
||||
on_conflict: str
|
||||
|
||||
|
||||
class SqlMailConfigurationProvider(ConfigurationProvider):
|
||||
class SqlMailConfigurationProvider(
|
||||
ConfigurationProvider,
|
||||
InfrastructureDependencyProvider,
|
||||
):
|
||||
module_id = "mail"
|
||||
capability_ids = ("mail.smtp",)
|
||||
|
||||
def describe(self) -> ConfigurationProviderDescription:
|
||||
return ConfigurationProviderDescription(
|
||||
@@ -204,6 +213,84 @@ class SqlMailConfigurationProvider(ConfigurationProvider):
|
||||
item for item in import_result.diagnostics if item.severity == "blocker"
|
||||
)
|
||||
|
||||
def infrastructure_dependencies(self) -> tuple[InfrastructureDependency, ...]:
|
||||
with get_database().session() as session:
|
||||
return _smtp_infrastructure_dependencies(session)
|
||||
|
||||
|
||||
def _smtp_infrastructure_dependencies(
|
||||
session: Session,
|
||||
) -> tuple[InfrastructureDependency, ...]:
|
||||
endpoints = tuple(
|
||||
session.execute(
|
||||
select(MailServerEndpoint)
|
||||
.where(MailServerEndpoint.protocol == "smtp")
|
||||
.order_by(MailServerEndpoint.id)
|
||||
).scalars()
|
||||
)
|
||||
binding_counts = {
|
||||
str(server_id): int(count)
|
||||
for server_id, count in session.execute(
|
||||
select(
|
||||
MailServerCredentialBinding.server_id,
|
||||
func.count(MailServerCredentialBinding.id),
|
||||
).group_by(MailServerCredentialBinding.server_id)
|
||||
)
|
||||
}
|
||||
dependencies: list[InfrastructureDependency] = []
|
||||
endpoint_profile_ids: set[str] = set()
|
||||
for endpoint in endpoints:
|
||||
endpoint_profile_ids.add(endpoint.profile_id)
|
||||
dependencies.append(
|
||||
InfrastructureDependency(
|
||||
capability_id="mail.smtp",
|
||||
module_id="mail",
|
||||
dependency_type="smtp_endpoint",
|
||||
dependency_ref=mail_server_ref(endpoint.id) or f"mail:{endpoint.id}",
|
||||
state="active" if endpoint.is_active else "inactive",
|
||||
scope=str(endpoint.scope_type or "tenant"),
|
||||
summary=(
|
||||
"A persisted Mail SMTP endpoint is bound to the deployment relay."
|
||||
),
|
||||
metrics={
|
||||
"credential_binding_count": binding_counts.get(endpoint.id, 0),
|
||||
"default_endpoint": int(bool(endpoint.is_default)),
|
||||
},
|
||||
required_action=(
|
||||
"Rebind, migrate, or explicitly retire this SMTP endpoint and its credential-envelope references before changing the relay capability."
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
legacy_profiles = tuple(
|
||||
session.execute(
|
||||
select(MailServerProfile)
|
||||
.where(MailServerProfile.smtp_config.is_not(None))
|
||||
.order_by(MailServerProfile.id)
|
||||
).scalars()
|
||||
)
|
||||
for profile in legacy_profiles:
|
||||
if profile.id in endpoint_profile_ids or not dict(profile.smtp_config or {}):
|
||||
continue
|
||||
dependencies.append(
|
||||
InfrastructureDependency(
|
||||
capability_id="mail.smtp",
|
||||
module_id="mail",
|
||||
dependency_type="legacy_smtp_profile",
|
||||
dependency_ref=f"mail-profile:{profile.id}",
|
||||
state="active" if profile.is_active else "inactive",
|
||||
scope=str(profile.scope_type or "tenant"),
|
||||
summary=(
|
||||
"A persisted legacy Mail profile still contains SMTP transport configuration."
|
||||
),
|
||||
metrics={"credential_binding_count": 0},
|
||||
required_action=(
|
||||
"Migrate or explicitly retire this legacy profile before changing the relay capability."
|
||||
),
|
||||
)
|
||||
)
|
||||
return tuple(dependencies)
|
||||
|
||||
|
||||
def _preflight_smtp_profile(
|
||||
session: Session,
|
||||
|
||||
@@ -48,7 +48,10 @@ from govoplan_core.core.provider_governance import (
|
||||
from govoplan_core.core.search import SearchSourceProviderRegistration
|
||||
from govoplan_core.core.views import ViewSurface
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_mail.backend.configuration_provider import MAIL_CONFIGURATION_CAPABILITY
|
||||
from govoplan_mail.backend.configuration_provider import (
|
||||
MAIL_CONFIGURATION_CAPABILITY,
|
||||
MAIL_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
|
||||
)
|
||||
from govoplan_mail.backend.documentation import (
|
||||
documentation_configuration_states,
|
||||
documentation_topics,
|
||||
@@ -447,7 +450,7 @@ POP3_PROVIDER = ExternalProviderDeclaration(
|
||||
manifest = ModuleManifest(
|
||||
id="mail",
|
||||
name="Mail",
|
||||
version="0.1.25",
|
||||
version="0.1.26",
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
optional_dependencies=("campaigns", "addresses", "calendar", "postbox", "search"),
|
||||
provides_interfaces=(
|
||||
@@ -642,6 +645,7 @@ manifest = ModuleManifest(
|
||||
),
|
||||
capability_factories={
|
||||
MAIL_CONFIGURATION_CAPABILITY: _configuration_provider,
|
||||
MAIL_INFRASTRUCTURE_DEPENDENCY_CAPABILITY: _configuration_provider,
|
||||
"mail.campaign_delivery": lambda context: __import__("govoplan_mail.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context),
|
||||
CAPABILITY_MAIL_DELIVERY_OUTBOX: lambda context: __import__(
|
||||
"govoplan_mail.backend.capabilities",
|
||||
@@ -878,7 +882,9 @@ manifest = ModuleManifest(
|
||||
"The Mail configuration provider reads the validated mail.smtp deployment capability and derives authoritative endpoint metadata. "
|
||||
"Missing non-secret transport fields are collected by preflight; authentication is represented only by an existing credential-envelope id. "
|
||||
"Tenant scope is the default, system scope requires system authority, conflicting profiles are preserved unless update is explicitly reviewed, "
|
||||
"and an unchanged second apply is a no-op. SMTP reachability remains a separate Mail profile test."
|
||||
"and an unchanged second apply is a no-op. Before the host deployer changes or removes mail.smtp, Mail inventories every persisted SMTP endpoint, "
|
||||
"legacy SMTP profile, and credential-binding count through the non-secret Ops dependency report. Missing, stale, or incomplete inventory blocks apply; "
|
||||
"SMTP reachability remains a separate Mail profile test."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin",),
|
||||
@@ -910,7 +916,9 @@ manifest = ModuleManifest(
|
||||
"Der Mail-Konfigurationsprovider liest die validierte Bereitstellungsfähigkeit mail.smtp und übernimmt deren maßgebliche Endpunktdaten. "
|
||||
"Fehlende nicht geheime Transportangaben werden im Preflight abgefragt; Authentifizierung wird ausschließlich durch die ID eines vorhandenen Zugangsdaten-Umschlags referenziert. "
|
||||
"Mandantenbezug ist der Standard, Systembezug erfordert Systemberechtigung, abweichende vorhandene Profile bleiben ohne ausdrücklich geprüfte Aktualisierung unverändert, "
|
||||
"und eine unveränderte zweite Anwendung bleibt wirkungslos. Die SMTP-Erreichbarkeit wird weiterhin separat im Mail-Profil getestet."
|
||||
"und eine unveränderte zweite Anwendung bleibt wirkungslos. Bevor der Host-Deployer mail.smtp ändert oder entfernt, inventarisiert Mail alle gespeicherten SMTP-Endpunkte, "
|
||||
"älteren SMTP-Profile und die Anzahl ihrer Zugangsdatenbindungen im nicht geheimen Ops-Abhängigkeitsbericht. Ein fehlendes, veraltetes oder unvollständiges Inventar blockiert die Anwendung; "
|
||||
"die SMTP-Erreichbarkeit wird weiterhin separat im Mail-Profil getestet."
|
||||
),
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user