feat: provision SMTP profiles from deployment receipts
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from dataclasses import replace
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from govoplan_access.backend.db import models as access_models # noqa: F401
|
||||
from govoplan_core.core.configuration_packages import (
|
||||
ConfigurationPackageFragment,
|
||||
ConfigurationPreflightContext,
|
||||
)
|
||||
from govoplan_core.admin.models import SystemSettings
|
||||
from govoplan_core.core.infrastructure_capabilities import (
|
||||
infrastructure_capability_receipt_from_mapping,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.db.session import configure_database, reset_database
|
||||
from govoplan_core.security.credential_envelopes import CredentialEnvelope
|
||||
from govoplan_mail.backend.configuration_provider import (
|
||||
MAIL_CONFIGURATION_CAPABILITY,
|
||||
SqlMailConfigurationProvider,
|
||||
)
|
||||
from govoplan_mail.backend.db.models import (
|
||||
MailProfilePolicy,
|
||||
MailServerCredentialBinding,
|
||||
MailServerEndpoint,
|
||||
MailServerProfile,
|
||||
)
|
||||
from govoplan_mail.backend.manifest import manifest
|
||||
|
||||
|
||||
def _receipt():
|
||||
return infrastructure_capability_receipt_from_mapping(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"installation_id": "mail-provider-test",
|
||||
"profile": "evaluation",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "mail.smtp",
|
||||
"label": "SMTP delivery",
|
||||
"state": "available_unconfigured",
|
||||
"source": "installer-managed-test",
|
||||
"detail": "GreenMail is available for profile binding.",
|
||||
"endpoint": {
|
||||
"scheme": "smtp",
|
||||
"host": "test-mail",
|
||||
"port": 3025,
|
||||
},
|
||||
"secret_refs": [],
|
||||
"dependent_modules": ["mail"],
|
||||
}
|
||||
],
|
||||
"post_install_tasks": [
|
||||
{
|
||||
"id": "mail.smtp-profile",
|
||||
"resume_key": "mail-provider-test:mail.smtp-profile:v1",
|
||||
"capability_id": "mail.smtp",
|
||||
"state": "pending",
|
||||
"owner_module": "mail",
|
||||
"summary": "Create a Mail SMTP profile.",
|
||||
"required_inputs": ["credential envelope reference when required"],
|
||||
"secret_boundary": "credential-envelope-reference-only",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _external_receipt(*, credential_required: bool = False):
|
||||
return infrastructure_capability_receipt_from_mapping(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"installation_id": "mail-provider-external",
|
||||
"profile": "production",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "mail.smtp",
|
||||
"label": "SMTP delivery",
|
||||
"state": "available_unconfigured",
|
||||
"source": "operator-supplied",
|
||||
"detail": "An external relay needs reviewed Mail configuration.",
|
||||
"endpoint": {},
|
||||
"secret_refs": (
|
||||
["env:SMTP_CREDENTIAL_ENVELOPE_REF"]
|
||||
if credential_required
|
||||
else []
|
||||
),
|
||||
"dependent_modules": ["mail"],
|
||||
}
|
||||
],
|
||||
"post_install_tasks": [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MailConfigurationProviderTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.tempdir = tempfile.TemporaryDirectory(prefix="govoplan-mail-config-")
|
||||
self.addCleanup(self.tempdir.cleanup)
|
||||
database_path = Path(self.tempdir.name) / "mail.sqlite3"
|
||||
self.engine = create_engine(f"sqlite:///{database_path}")
|
||||
Base.metadata.create_all(
|
||||
self.engine,
|
||||
tables=(
|
||||
access_models.User.__table__,
|
||||
SystemSettings.__table__,
|
||||
CredentialEnvelope.__table__,
|
||||
MailServerProfile.__table__,
|
||||
MailServerEndpoint.__table__,
|
||||
MailServerCredentialBinding.__table__,
|
||||
MailProfilePolicy.__table__,
|
||||
),
|
||||
)
|
||||
configure_database(
|
||||
f"sqlite:///{database_path}",
|
||||
engine=self.engine,
|
||||
dispose_previous=True,
|
||||
)
|
||||
self.SessionLocal = sessionmaker(
|
||||
bind=self.engine,
|
||||
class_=Session,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
with self.SessionLocal() as session:
|
||||
session.add(
|
||||
MailProfilePolicy(
|
||||
id="tenant-policy",
|
||||
tenant_id="tenant-1",
|
||||
scope_type="tenant",
|
||||
scope_id="tenant-1",
|
||||
policy={},
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
CredentialEnvelope(
|
||||
id="credential-1",
|
||||
tenant_id="tenant-1",
|
||||
scope_type="tenant",
|
||||
scope_id="tenant-1",
|
||||
name="SMTP credential",
|
||||
credential_kind="username_password",
|
||||
public_data={"username": "mailer"},
|
||||
secret_data_encrypted="encrypted-outside-package",
|
||||
secret_keys=["password"],
|
||||
allowed_modules=["mail"],
|
||||
allowed_server_refs=[],
|
||||
inherit_to_lower_scopes=True,
|
||||
is_active=True,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
self.addCleanup(self._cleanup_database)
|
||||
self.provider = SqlMailConfigurationProvider()
|
||||
self.context = ConfigurationPreflightContext(
|
||||
tenant_id="tenant-1",
|
||||
operator_user_id=None,
|
||||
infrastructure_receipt=_receipt(),
|
||||
)
|
||||
|
||||
def _cleanup_database(self) -> None:
|
||||
reset_database()
|
||||
self.engine.dispose()
|
||||
|
||||
def test_provider_is_registered_and_describes_receipt_bound_fragment(self) -> None:
|
||||
self.assertIn(MAIL_CONFIGURATION_CAPABILITY, manifest.capability_factories)
|
||||
description = self.provider.describe()
|
||||
self.assertEqual(("smtp_profile",), description.fragment_types)
|
||||
|
||||
def test_apply_is_idempotent_and_binds_existing_credential_envelope(self) -> None:
|
||||
fragment = ConfigurationPackageFragment(
|
||||
module_id="mail",
|
||||
fragment_type="smtp_profile",
|
||||
fragment_id="test-smtp",
|
||||
payload={"credential_envelope_id": "credential-1"},
|
||||
)
|
||||
|
||||
first_plan = self.provider.preflight(fragment, self.context)
|
||||
self.assertEqual("create", first_plan.plan[0].action)
|
||||
self.assertFalse(
|
||||
any(item.severity == "blocker" for item in first_plan.diagnostics)
|
||||
)
|
||||
first_apply = self.provider.apply(fragment, {}, self.context)
|
||||
self.assertIn("test-smtp", first_apply.created_refs)
|
||||
|
||||
second_plan = self.provider.preflight(fragment, self.context)
|
||||
second_apply = self.provider.apply(fragment, {}, self.context)
|
||||
|
||||
self.assertEqual("skip", second_plan.plan[0].action)
|
||||
self.assertEqual({}, second_apply.created_refs)
|
||||
self.assertEqual({}, second_apply.updated_refs)
|
||||
with self.SessionLocal() as session:
|
||||
profiles = session.scalars(select(MailServerProfile)).all()
|
||||
servers = session.scalars(select(MailServerEndpoint)).all()
|
||||
bindings = session.scalars(select(MailServerCredentialBinding)).all()
|
||||
self.assertEqual(1, len(profiles))
|
||||
self.assertEqual("test-mail", profiles[0].smtp_config["host"])
|
||||
self.assertEqual("plain", profiles[0].smtp_config["security"])
|
||||
self.assertEqual(1, len(servers))
|
||||
self.assertEqual("test-mail", servers[0].config["host"])
|
||||
self.assertEqual(1, len(bindings))
|
||||
self.assertEqual("credential-1", bindings[0].credential_id)
|
||||
|
||||
def test_conflicting_existing_profile_is_preserved_by_default(self) -> None:
|
||||
fragment = ConfigurationPackageFragment(
|
||||
module_id="mail",
|
||||
fragment_type="smtp_profile",
|
||||
fragment_id="test-smtp",
|
||||
payload={},
|
||||
)
|
||||
self.provider.apply(fragment, {}, self.context)
|
||||
with self.SessionLocal() as session:
|
||||
server = session.scalar(select(MailServerEndpoint))
|
||||
assert server is not None
|
||||
server.config = {**server.config, "host": "manually-changed.example.test"}
|
||||
session.commit()
|
||||
|
||||
plan = self.provider.preflight(fragment, self.context)
|
||||
result = self.provider.apply(fragment, {}, self.context)
|
||||
|
||||
self.assertEqual("blocked", plan.plan[0].action)
|
||||
self.assertIn(
|
||||
"mail_configuration_conflict",
|
||||
{item.code for item in result.diagnostics},
|
||||
)
|
||||
with self.SessionLocal() as session:
|
||||
server = session.scalar(select(MailServerEndpoint))
|
||||
assert server is not None
|
||||
self.assertEqual("manually-changed.example.test", server.config["host"])
|
||||
|
||||
def test_explicit_conflict_update_reconciles_then_becomes_noop(self) -> None:
|
||||
fragment = ConfigurationPackageFragment(
|
||||
module_id="mail",
|
||||
fragment_type="smtp_profile",
|
||||
fragment_id="test-smtp",
|
||||
payload={},
|
||||
)
|
||||
self.provider.apply(fragment, {}, self.context)
|
||||
with self.SessionLocal() as session:
|
||||
server = session.scalar(select(MailServerEndpoint))
|
||||
assert server is not None
|
||||
server.config = {**server.config, "host": "manually-changed.example.test"}
|
||||
session.commit()
|
||||
update_fragment = replace(
|
||||
fragment,
|
||||
payload={"on_conflict": "update"},
|
||||
)
|
||||
|
||||
plan = self.provider.preflight(update_fragment, self.context)
|
||||
result = self.provider.apply(update_fragment, {}, self.context)
|
||||
settled = self.provider.preflight(update_fragment, self.context)
|
||||
|
||||
self.assertEqual("update", plan.plan[0].action)
|
||||
self.assertIn("test-smtp", result.updated_refs)
|
||||
self.assertEqual("skip", settled.plan[0].action)
|
||||
with self.SessionLocal() as session:
|
||||
server = session.scalar(select(MailServerEndpoint))
|
||||
assert server is not None
|
||||
self.assertEqual("test-mail", server.config["host"])
|
||||
|
||||
def test_inline_credentials_are_rejected(self) -> None:
|
||||
fragment = ConfigurationPackageFragment(
|
||||
module_id="mail",
|
||||
fragment_type="smtp_profile",
|
||||
payload={
|
||||
"smtp": {
|
||||
"host": "test-mail",
|
||||
"port": 3025,
|
||||
"security": "plain",
|
||||
"password": "must-not-cross-boundary",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
plan = self.provider.preflight(fragment, self.context)
|
||||
|
||||
self.assertEqual("blocked", plan.plan[0].action)
|
||||
self.assertIn(
|
||||
"mail_configuration_secret_forbidden",
|
||||
{item.code for item in plan.diagnostics},
|
||||
)
|
||||
|
||||
def test_external_relay_collects_missing_non_secret_inputs(self) -> None:
|
||||
fragment = ConfigurationPackageFragment(
|
||||
module_id="mail",
|
||||
fragment_type="smtp_profile",
|
||||
fragment_id="external-smtp",
|
||||
payload={},
|
||||
)
|
||||
missing = self.provider.preflight(
|
||||
fragment,
|
||||
replace(self.context, infrastructure_receipt=_external_receipt()),
|
||||
)
|
||||
ready = self.provider.preflight(
|
||||
fragment,
|
||||
replace(
|
||||
self.context,
|
||||
infrastructure_receipt=_external_receipt(),
|
||||
supplied_data={
|
||||
"mail.smtp.external-smtp.host": "smtp.example.test",
|
||||
"mail.smtp.external-smtp.port": 587,
|
||||
"mail.smtp.external-smtp.security": "starttls",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", missing.plan[0].action)
|
||||
self.assertEqual(
|
||||
{
|
||||
"mail.smtp.external-smtp.host",
|
||||
"mail.smtp.external-smtp.port",
|
||||
"mail.smtp.external-smtp.security",
|
||||
},
|
||||
{item.key for item in missing.required_data if item.required},
|
||||
)
|
||||
self.assertEqual("create", ready.plan[0].action)
|
||||
|
||||
def test_receipt_secret_reference_requires_credential_envelope_reference(
|
||||
self,
|
||||
) -> None:
|
||||
fragment = ConfigurationPackageFragment(
|
||||
module_id="mail",
|
||||
fragment_type="smtp_profile",
|
||||
fragment_id="authenticated-smtp",
|
||||
payload={
|
||||
"smtp": {
|
||||
"host": "smtp.example.test",
|
||||
"port": 587,
|
||||
"security": "starttls",
|
||||
}
|
||||
},
|
||||
)
|
||||
context = replace(
|
||||
self.context,
|
||||
infrastructure_receipt=_external_receipt(credential_required=True),
|
||||
)
|
||||
|
||||
missing = self.provider.preflight(fragment, context)
|
||||
ready = self.provider.preflight(
|
||||
ConfigurationPackageFragment(
|
||||
module_id="mail",
|
||||
fragment_type="smtp_profile",
|
||||
fragment_id="authenticated-smtp",
|
||||
payload={
|
||||
**fragment.payload,
|
||||
"credential_envelope_id": "credential-1",
|
||||
},
|
||||
),
|
||||
context,
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", missing.plan[0].action)
|
||||
self.assertIn(
|
||||
"mail.smtp.authenticated-smtp.credential_envelope_id",
|
||||
{item.key for item in missing.required_data if item.required},
|
||||
)
|
||||
self.assertEqual("create", ready.plan[0].action)
|
||||
|
||||
def test_system_profile_requires_system_configuration_authority(self) -> None:
|
||||
fragment = ConfigurationPackageFragment(
|
||||
module_id="mail",
|
||||
fragment_type="smtp_profile",
|
||||
payload={"profile": {"scope_type": "system"}},
|
||||
)
|
||||
|
||||
blocked = self.provider.preflight(fragment, self.context)
|
||||
ready = self.provider.preflight(
|
||||
fragment,
|
||||
replace(
|
||||
self.context,
|
||||
operator_scopes=frozenset({"system:settings:write"}),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", blocked.plan[0].action)
|
||||
self.assertIn(
|
||||
"system_configuration_authority_required",
|
||||
{item.code for item in blocked.diagnostics},
|
||||
)
|
||||
self.assertEqual("create", ready.plan[0].action)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user