633 lines
24 KiB
Python
633 lines
24 KiB
Python
"""Workspace onboarding templates and apply helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.domain import Domain
|
|
from app.models.mail_source import MailSource
|
|
from app.models.setting import Setting
|
|
from app.models.workspace import Workspace
|
|
from app.services.workspace_audit import record_workspace_audit_log, sanitize_audit_details
|
|
from app.services.workspaces import normalize_workspace_slug, workspace_domain_query
|
|
from app.utils.domain_validator import validate_domain_config
|
|
|
|
ONBOARDING_SCHEMA_VERSION = "dmarq.workspace_onboarding.v1"
|
|
|
|
SAFE_NOTIFICATION_DEFAULTS = {
|
|
"notifications.apprise_enabled",
|
|
"notifications.min_send_interval_minutes",
|
|
"notifications.redact_pii_enabled",
|
|
"notifications.alert_new_sources_enabled",
|
|
"notifications.alert_compliance_drop_enabled",
|
|
"notifications.alert_compliance_drop_points",
|
|
"notifications.alert_failure_threshold_enabled",
|
|
"notifications.alert_failure_threshold_count",
|
|
"notifications.alert_missing_reports_enabled",
|
|
"notifications.alert_missing_reports_days",
|
|
"notifications.summary_daily_enabled",
|
|
"notifications.summary_weekly_enabled",
|
|
"notifications.summary_send_hour_utc",
|
|
"notifications.summary_weekday_utc",
|
|
}
|
|
|
|
NOTIFICATION_SETTING_META = {
|
|
"notifications.apprise_enabled": ("Send notifications through Apprise", "boolean"),
|
|
"notifications.min_send_interval_minutes": ("Minimum minutes between notifications", "integer"),
|
|
"notifications.redact_pii_enabled": ("Redact PII in outbound notifications", "boolean"),
|
|
"notifications.alert_new_sources_enabled": ("Alert when new senders appear", "boolean"),
|
|
"notifications.alert_compliance_drop_enabled": ("Alert on compliance drops", "boolean"),
|
|
"notifications.alert_compliance_drop_points": ("Compliance drop threshold", "integer"),
|
|
"notifications.alert_failure_threshold_enabled": ("Alert on high failure volume", "boolean"),
|
|
"notifications.alert_failure_threshold_count": ("Daily failure-count threshold", "integer"),
|
|
"notifications.alert_missing_reports_enabled": ("Alert when reports stop arriving", "boolean"),
|
|
"notifications.alert_missing_reports_days": ("Days without reports before alerting", "integer"),
|
|
"notifications.summary_daily_enabled": ("Send daily DMARC summaries", "boolean"),
|
|
"notifications.summary_weekly_enabled": ("Send weekly DMARC summaries", "boolean"),
|
|
"notifications.summary_send_hour_utc": ("UTC hour for scheduled summaries", "integer"),
|
|
"notifications.summary_weekday_utc": ("UTC weekday for weekly summaries", "integer"),
|
|
}
|
|
|
|
WORKSPACE_ONBOARDING_TEMPLATES: List[Dict[str, Any]] = [
|
|
{
|
|
"id": "standard_monitoring",
|
|
"name": "Standard DMARC Monitoring",
|
|
"description": (
|
|
"Create one monitored domain, one disabled IMAP mail source, "
|
|
"and safe notification defaults."
|
|
),
|
|
"variables": [
|
|
{"name": "domain", "required": True, "example": "example.com"},
|
|
{"name": "workspace_name", "required": False, "example": "Example Client"},
|
|
{"name": "report_mailbox", "required": False, "example": "dmarc@example.com"},
|
|
{"name": "imap_server", "required": False, "example": "imap.example.com"},
|
|
{"name": "imap_username", "required": False, "example": "dmarc@example.com"},
|
|
{"name": "imap_password", "required": False, "secret": True},
|
|
],
|
|
"domains": [
|
|
{
|
|
"name": "{domain}",
|
|
"description": "Primary DMARC domain for {workspace_name}",
|
|
"dkim_selectors": ["google", "selector1"],
|
|
}
|
|
],
|
|
"mail_sources": [
|
|
{
|
|
"name": "{workspace_name} DMARC inbox",
|
|
"method": "IMAP",
|
|
"server": "{imap_server}",
|
|
"port": 993,
|
|
"username": "{imap_username}",
|
|
"password": "{imap_password}",
|
|
"folder": "INBOX",
|
|
"use_ssl": True,
|
|
"polling_interval": 60,
|
|
"enabled": False,
|
|
}
|
|
],
|
|
"notification_defaults": {
|
|
"notifications.apprise_enabled": "false",
|
|
"notifications.redact_pii_enabled": "true",
|
|
"notifications.alert_new_sources_enabled": "true",
|
|
"notifications.alert_compliance_drop_enabled": "true",
|
|
"notifications.alert_compliance_drop_points": "10",
|
|
"notifications.alert_failure_threshold_enabled": "true",
|
|
"notifications.alert_failure_threshold_count": "100",
|
|
"notifications.alert_missing_reports_enabled": "true",
|
|
"notifications.alert_missing_reports_days": "2",
|
|
"notifications.summary_weekly_enabled": "true",
|
|
"notifications.summary_send_hour_utc": "8",
|
|
"notifications.summary_weekday_utc": "0",
|
|
},
|
|
"checklist": [
|
|
{
|
|
"id": "verify-domain-dns",
|
|
"category": "domains",
|
|
"title": "Verify DMARC, SPF, and DKIM DNS posture",
|
|
"description": (
|
|
"Open the domain DNS view and confirm DMARC, SPF, and each "
|
|
"configured DKIM selector resolve."
|
|
),
|
|
},
|
|
{
|
|
"id": "connect-mail-source",
|
|
"category": "mail_sources",
|
|
"title": "Connect the DMARC report inbox",
|
|
"description": (
|
|
"Add credentials or complete OAuth, run a connection test, "
|
|
"then enable polling."
|
|
),
|
|
},
|
|
{
|
|
"id": "run-initial-import",
|
|
"category": "mail_sources",
|
|
"title": "Run an initial DMARC import",
|
|
"description": (
|
|
"Trigger a manual import for the mail source and confirm " "reports are parsed."
|
|
),
|
|
},
|
|
{
|
|
"id": "configure-notification-target",
|
|
"category": "notifications",
|
|
"title": "Configure and test notification delivery",
|
|
"description": (
|
|
"Add Apprise targets, send a test notification, and confirm "
|
|
"alert thresholds match the client."
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"id": "dns_only_assessment",
|
|
"name": "DNS Posture Assessment",
|
|
"description": (
|
|
"Create monitored domains and a validation checklist without " "mailbox ingestion."
|
|
),
|
|
"variables": [
|
|
{"name": "domain", "required": True, "example": "example.com"},
|
|
{"name": "workspace_name", "required": False, "example": "Example Client"},
|
|
],
|
|
"domains": [
|
|
{
|
|
"name": "{domain}",
|
|
"description": "DNS posture assessment for {workspace_name}",
|
|
"dkim_selectors": [],
|
|
}
|
|
],
|
|
"mail_sources": [],
|
|
"notification_defaults": {
|
|
"notifications.apprise_enabled": "false",
|
|
"notifications.alert_missing_reports_enabled": "false",
|
|
"notifications.summary_weekly_enabled": "false",
|
|
},
|
|
"checklist": [
|
|
{
|
|
"id": "verify-domain-dns",
|
|
"category": "domains",
|
|
"title": "Verify published authentication records",
|
|
"description": (
|
|
"Review DMARC, SPF, DKIM, MTA-STS, and BIMI posture before "
|
|
"recommending changes."
|
|
),
|
|
},
|
|
{
|
|
"id": "document-report-inbox",
|
|
"category": "mail_sources",
|
|
"title": "Document the future DMARC report inbox",
|
|
"description": (
|
|
"Confirm where aggregate reports are delivered before " "enabling ingestion."
|
|
),
|
|
},
|
|
],
|
|
},
|
|
]
|
|
|
|
|
|
class _SafeFormatDict(dict):
|
|
def __missing__(self, key):
|
|
return "{" + key + "}"
|
|
|
|
|
|
def _render_value(value: Any, variables: Dict[str, Any]) -> Any:
|
|
if isinstance(value, str):
|
|
return value.format_map(
|
|
_SafeFormatDict({k: "" if v is None else v for k, v in variables.items()})
|
|
)
|
|
if isinstance(value, list):
|
|
return [_render_value(item, variables) for item in value]
|
|
if isinstance(value, dict):
|
|
return {key: _render_value(item, variables) for key, item in value.items()}
|
|
return value
|
|
|
|
|
|
def _clean_optional_strings(value: Any) -> Any:
|
|
if isinstance(value, dict):
|
|
return {key: _clean_optional_strings(item) for key, item in value.items()}
|
|
if isinstance(value, list):
|
|
return [_clean_optional_strings(item) for item in value]
|
|
if isinstance(value, str):
|
|
stripped = value.strip()
|
|
return stripped or None
|
|
return value
|
|
|
|
|
|
def _template_by_id(template_id: str) -> Dict[str, Any]:
|
|
for template in WORKSPACE_ONBOARDING_TEMPLATES:
|
|
if template["id"] == template_id:
|
|
return copy.deepcopy(template)
|
|
raise ValueError(f"Unknown onboarding template: {template_id}")
|
|
|
|
|
|
def list_onboarding_templates() -> List[Dict[str, Any]]:
|
|
"""Return static workspace onboarding templates."""
|
|
return copy.deepcopy(WORKSPACE_ONBOARDING_TEMPLATES)
|
|
|
|
|
|
def public_onboarding_plan(plan: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Return an API-safe plan with secret-like values redacted."""
|
|
return sanitize_audit_details(plan)
|
|
|
|
|
|
def _workspace_context(
|
|
workspace: Dict[str, Any],
|
|
variables: Optional[Dict[str, Any]],
|
|
) -> Tuple[str, str, Dict[str, Any]]:
|
|
context = {str(key): value for key, value in (variables or {}).items()}
|
|
workspace_name = str(workspace.get("name") or context.get("workspace_name") or "").strip()
|
|
slug = normalize_workspace_slug(str(workspace.get("slug") or workspace_name))
|
|
context.setdefault("workspace_name", workspace_name or slug)
|
|
context.setdefault("domain", "")
|
|
context.setdefault("report_mailbox", "")
|
|
context.setdefault("imap_server", "")
|
|
context.setdefault("imap_username", context.get("report_mailbox", ""))
|
|
context.setdefault("imap_password", "")
|
|
return slug, workspace_name, context
|
|
|
|
|
|
def _required_variable_errors(template: Dict[str, Any], variables: Dict[str, Any]) -> List[str]:
|
|
errors = []
|
|
for variable in template.get("variables", []):
|
|
name = variable["name"]
|
|
if variable.get("required") and not str(variables.get(name) or "").strip():
|
|
errors.append(f"template variable is required: {name}")
|
|
return errors
|
|
|
|
|
|
def _render_sections(
|
|
template: Dict[str, Any],
|
|
variables: Dict[str, Any],
|
|
*,
|
|
domains: Optional[List[Dict[str, Any]]],
|
|
mail_sources: Optional[List[Dict[str, Any]]],
|
|
notification_defaults: Optional[Dict[str, Any]],
|
|
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], Dict[str, Any]]:
|
|
rendered_domains = _clean_optional_strings(
|
|
_render_value(domains if domains is not None else template.get("domains", []), variables)
|
|
)
|
|
rendered_sources = _clean_optional_strings(
|
|
_render_value(
|
|
mail_sources if mail_sources is not None else template.get("mail_sources", []),
|
|
variables,
|
|
)
|
|
)
|
|
rendered_notifications = _clean_optional_strings(
|
|
_render_value(
|
|
(
|
|
notification_defaults
|
|
if notification_defaults is not None
|
|
else template.get("notification_defaults", {})
|
|
),
|
|
variables,
|
|
)
|
|
)
|
|
return rendered_domains, rendered_sources, rendered_notifications
|
|
|
|
|
|
def _validated_domains(
|
|
rendered_domains: Iterable[Dict[str, Any]],
|
|
) -> Tuple[List[Dict[str, Any]], List[str]]:
|
|
errors = []
|
|
validated = []
|
|
for item in rendered_domains:
|
|
name = str(item.get("name") or "").strip().strip(".").lower()
|
|
validation = validate_domain_config(
|
|
{"name": name, "description": item.get("description") or ""}
|
|
)
|
|
if not validation["valid"]:
|
|
errors.append(f"invalid domain {name or '<empty>'}: {validation['errors']}")
|
|
continue
|
|
selectors = [
|
|
str(selector).strip()
|
|
for selector in item.get("dkim_selectors") or []
|
|
if str(selector).strip()
|
|
]
|
|
validated.append(
|
|
{
|
|
"name": name,
|
|
"description": item.get("description"),
|
|
"dkim_selectors": selectors,
|
|
}
|
|
)
|
|
return validated, errors
|
|
|
|
|
|
def _validated_mail_sources(
|
|
rendered_sources: Iterable[Dict[str, Any]],
|
|
) -> Tuple[List[Dict[str, Any]], List[str]]:
|
|
errors = []
|
|
validated = []
|
|
for item in rendered_sources:
|
|
name = str(item.get("name") or "").strip()
|
|
method = str(item.get("method") or "IMAP").strip().upper()
|
|
if not name:
|
|
errors.append("mail source name is required")
|
|
continue
|
|
if method not in {"IMAP", "POP3", "GMAIL_API", "M365_GRAPH"}:
|
|
errors.append(f"unsupported mail source method: {method}")
|
|
continue
|
|
validated.append(
|
|
{
|
|
"name": name,
|
|
"method": method,
|
|
"server": item.get("server"),
|
|
"port": int(item.get("port") or 993),
|
|
"username": item.get("username"),
|
|
"password": item.get("password"),
|
|
"use_ssl": bool(item.get("use_ssl", True)),
|
|
"folder": item.get("folder") or "INBOX",
|
|
"polling_interval": int(item.get("polling_interval") or 60),
|
|
"enabled": bool(item.get("enabled", False)),
|
|
"gmail_client_id": item.get("gmail_client_id"),
|
|
"gmail_client_secret": item.get("gmail_client_secret"),
|
|
"m365_tenant_id": item.get("m365_tenant_id") or "common",
|
|
"m365_client_id": item.get("m365_client_id"),
|
|
"m365_client_secret": item.get("m365_client_secret"),
|
|
"m365_mailbox": item.get("m365_mailbox"),
|
|
"m365_folder_id": item.get("m365_folder_id"),
|
|
}
|
|
)
|
|
return validated, errors
|
|
|
|
|
|
def _validated_notification_defaults(
|
|
rendered_notifications: Dict[str, Any],
|
|
) -> Tuple[Dict[str, str], List[str]]:
|
|
errors = []
|
|
safe_notifications = {}
|
|
for key, value in (rendered_notifications or {}).items():
|
|
if key not in SAFE_NOTIFICATION_DEFAULTS:
|
|
errors.append(f"unsupported notification default: {key}")
|
|
continue
|
|
safe_notifications[key] = "" if value is None else str(value)
|
|
return safe_notifications, errors
|
|
|
|
|
|
def _validated_sections(
|
|
rendered_domains: Iterable[Dict[str, Any]],
|
|
rendered_sources: Iterable[Dict[str, Any]],
|
|
rendered_notifications: Dict[str, Any],
|
|
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], Dict[str, str], List[str]]:
|
|
domains, domain_errors = _validated_domains(rendered_domains)
|
|
sources, source_errors = _validated_mail_sources(rendered_sources)
|
|
notifications, notification_errors = _validated_notification_defaults(rendered_notifications)
|
|
return domains, sources, notifications, domain_errors + source_errors + notification_errors
|
|
|
|
|
|
def build_onboarding_plan( # pylint: disable=too-many-locals
|
|
*,
|
|
template_id: str,
|
|
workspace: Dict[str, Any],
|
|
variables: Optional[Dict[str, Any]] = None,
|
|
domains: Optional[List[Dict[str, Any]]] = None,
|
|
mail_sources: Optional[List[Dict[str, Any]]] = None,
|
|
notification_defaults: Optional[Dict[str, Any]] = None,
|
|
overwrite_existing: bool = False,
|
|
) -> Dict[str, Any]:
|
|
"""Render and validate a workspace onboarding plan."""
|
|
template = _template_by_id(template_id)
|
|
slug, workspace_name, variables = _workspace_context(workspace, variables)
|
|
errors: List[str] = []
|
|
if not slug:
|
|
errors.append("workspace.slug or workspace.name is required")
|
|
if not workspace_name:
|
|
errors.append("workspace.name is required")
|
|
errors.extend(_required_variable_errors(template, variables))
|
|
rendered_domains, rendered_sources, rendered_notifications = _render_sections(
|
|
template,
|
|
variables,
|
|
domains=domains,
|
|
mail_sources=mail_sources,
|
|
notification_defaults=notification_defaults,
|
|
)
|
|
validated_domains, validated_sources, safe_notifications, section_errors = _validated_sections(
|
|
rendered_domains, rendered_sources, rendered_notifications
|
|
)
|
|
errors.extend(section_errors)
|
|
|
|
return {
|
|
"schema_version": ONBOARDING_SCHEMA_VERSION,
|
|
"template_id": template_id,
|
|
"template_name": template["name"],
|
|
"workspace": {
|
|
"slug": slug,
|
|
"name": workspace_name,
|
|
"description": workspace.get("description"),
|
|
},
|
|
"domains": validated_domains,
|
|
"mail_sources": validated_sources,
|
|
"notification_defaults": safe_notifications,
|
|
"checklist": copy.deepcopy(template.get("checklist", [])),
|
|
"overwrite_existing": overwrite_existing,
|
|
"errors": errors,
|
|
}
|
|
|
|
|
|
def _ensure_workspace(db: Session, workspace_plan: Dict[str, Any]) -> Tuple[Workspace, str]:
|
|
workspace = db.query(Workspace).filter(Workspace.slug == workspace_plan["slug"]).first()
|
|
if workspace:
|
|
return workspace, "existing"
|
|
workspace = Workspace(
|
|
slug=workspace_plan["slug"],
|
|
name=workspace_plan["name"],
|
|
description=workspace_plan.get("description"),
|
|
active=True,
|
|
)
|
|
db.add(workspace)
|
|
db.flush()
|
|
return workspace, "created"
|
|
|
|
|
|
def _apply_domains(
|
|
db: Session,
|
|
*,
|
|
workspace: Workspace,
|
|
domain_plans: Iterable[Dict[str, Any]],
|
|
) -> List[Dict[str, Any]]:
|
|
results: List[Dict[str, Any]] = []
|
|
for item in domain_plans:
|
|
existing = workspace_domain_query(db, workspace).filter(Domain.name == item["name"]).first()
|
|
if existing:
|
|
results.append({"name": item["name"], "status": "existing", "id": existing.id})
|
|
continue
|
|
owned_elsewhere = (
|
|
db.query(Domain)
|
|
.filter(Domain.name == item["name"], Domain.workspace_id != workspace.id)
|
|
.first()
|
|
)
|
|
if owned_elsewhere:
|
|
results.append(
|
|
{
|
|
"name": item["name"],
|
|
"status": "conflict",
|
|
"message": "Domain is already owned by another workspace",
|
|
}
|
|
)
|
|
continue
|
|
domain = Domain(
|
|
workspace_id=workspace.id,
|
|
name=item["name"],
|
|
description=item.get("description"),
|
|
dkim_selectors=",".join(item.get("dkim_selectors") or []) or None,
|
|
active=True,
|
|
verified=False,
|
|
)
|
|
db.add(domain)
|
|
db.flush()
|
|
results.append({"name": domain.name, "status": "created", "id": domain.id})
|
|
return results
|
|
|
|
|
|
def _apply_mail_sources(
|
|
db: Session,
|
|
*,
|
|
workspace: Workspace,
|
|
source_plans: Iterable[Dict[str, Any]],
|
|
) -> List[Dict[str, Any]]:
|
|
results: List[Dict[str, Any]] = []
|
|
for item in source_plans:
|
|
existing = (
|
|
db.query(MailSource)
|
|
.filter(MailSource.workspace_id == workspace.id, MailSource.name == item["name"])
|
|
.first()
|
|
)
|
|
if existing:
|
|
results.append({"name": item["name"], "status": "existing", "id": existing.id})
|
|
continue
|
|
source = MailSource(
|
|
workspace_id=workspace.id,
|
|
name=item["name"],
|
|
method=item["method"],
|
|
server=item.get("server"),
|
|
port=item.get("port") or 993,
|
|
username=item.get("username"),
|
|
password=item.get("password"),
|
|
use_ssl=item.get("use_ssl", True),
|
|
folder=item.get("folder") or "INBOX",
|
|
polling_interval=item.get("polling_interval") or 60,
|
|
enabled=item.get("enabled", False),
|
|
gmail_client_id=item.get("gmail_client_id"),
|
|
gmail_client_secret=item.get("gmail_client_secret"),
|
|
m365_tenant_id=item.get("m365_tenant_id") or "common",
|
|
m365_client_id=item.get("m365_client_id"),
|
|
m365_client_secret=item.get("m365_client_secret"),
|
|
m365_mailbox=item.get("m365_mailbox"),
|
|
m365_folder_id=item.get("m365_folder_id"),
|
|
)
|
|
db.add(source)
|
|
db.flush()
|
|
results.append({"name": source.name, "status": "created", "id": source.id})
|
|
return results
|
|
|
|
|
|
def _apply_notification_defaults(
|
|
db: Session,
|
|
*,
|
|
defaults: Dict[str, str],
|
|
overwrite_existing: bool,
|
|
) -> List[Dict[str, Any]]:
|
|
results: List[Dict[str, Any]] = []
|
|
for key, value in defaults.items():
|
|
row = db.query(Setting).filter(Setting.key == key).first()
|
|
if row and not overwrite_existing:
|
|
results.append({"key": key, "status": "existing"})
|
|
continue
|
|
if row:
|
|
row.value = value
|
|
results.append({"key": key, "status": "updated"})
|
|
continue
|
|
description, value_type = NOTIFICATION_SETTING_META.get(
|
|
key, ("Workspace onboarding default", "string")
|
|
)
|
|
db.add(
|
|
Setting(
|
|
key=key,
|
|
value=value,
|
|
description=description,
|
|
value_type=value_type,
|
|
category="notifications",
|
|
)
|
|
)
|
|
results.append({"key": key, "status": "created"})
|
|
return results
|
|
|
|
|
|
def apply_onboarding_plan(
|
|
db: Session,
|
|
*,
|
|
plan: Dict[str, Any],
|
|
auth_context: Optional[Dict[str, Any]],
|
|
request=None,
|
|
) -> Dict[str, Any]:
|
|
"""Apply a validated onboarding plan and return an API-safe result."""
|
|
if plan.get("errors"):
|
|
raise ValueError("Cannot apply an invalid onboarding plan")
|
|
try:
|
|
workspace, workspace_status = _ensure_workspace(db, plan["workspace"])
|
|
domain_results = _apply_domains(db, workspace=workspace, domain_plans=plan["domains"])
|
|
source_results = _apply_mail_sources(
|
|
db,
|
|
workspace=workspace,
|
|
source_plans=plan["mail_sources"],
|
|
)
|
|
notification_results = _apply_notification_defaults(
|
|
db,
|
|
defaults=plan["notification_defaults"],
|
|
overwrite_existing=bool(plan.get("overwrite_existing")),
|
|
)
|
|
conflicts = [item for item in domain_results if item["status"] == "conflict"]
|
|
if conflicts:
|
|
db.rollback()
|
|
return {
|
|
"applied": False,
|
|
"workspace": plan["workspace"],
|
|
"errors": [item["message"] for item in conflicts],
|
|
"results": {"domains": domain_results},
|
|
}
|
|
record_workspace_audit_log(
|
|
db,
|
|
workspace=workspace,
|
|
action="workspace.onboarding_applied",
|
|
entity_type="workspace",
|
|
entity_id=workspace.id,
|
|
entity_name=workspace.slug,
|
|
details={
|
|
"template_id": plan["template_id"],
|
|
"workspace_status": workspace_status,
|
|
"domains": domain_results,
|
|
"mail_sources": source_results,
|
|
"notification_defaults": notification_results,
|
|
"checklist_ids": [item["id"] for item in plan.get("checklist", [])],
|
|
},
|
|
auth_context=auth_context,
|
|
request=request,
|
|
)
|
|
db.commit()
|
|
except IntegrityError:
|
|
db.rollback()
|
|
raise
|
|
|
|
db.refresh(workspace)
|
|
return public_onboarding_plan(
|
|
{
|
|
"applied": True,
|
|
"schema_version": plan["schema_version"],
|
|
"template_id": plan["template_id"],
|
|
"workspace": {
|
|
"id": workspace.id,
|
|
"slug": workspace.slug,
|
|
"name": workspace.name,
|
|
"status": workspace_status,
|
|
},
|
|
"results": {
|
|
"domains": domain_results,
|
|
"mail_sources": source_results,
|
|
"notification_defaults": notification_results,
|
|
},
|
|
"checklist": plan.get("checklist", []),
|
|
}
|
|
)
|