Feat(Fix): Multi-Tenant Zerotier Org Setups

Imports Network From Zerotier
Async Emails
Migration guardrails
Admin to see all approvals states
This commit is contained in:
2026-03-29 23:14:20 +05:45
parent 05eb092228
commit 2b6f7e15af
21 changed files with 974 additions and 239 deletions
+78 -97
View File
@@ -17,6 +17,7 @@ from datetime import datetime, timezone
from typing import Optional, Dict, Any
import logging
import json
import threading
from gatehouse_app.extensions import db
from gatehouse_app.models.security.mfa_policy_compliance import MfaPolicyCompliance
@@ -78,29 +79,22 @@ class NotificationService:
)
# Send the notification
success = NotificationService._send_email(
NotificationService._send_email_async(
to_address=user.email,
subject=subject,
body=body,
)
if success:
logger.info(
f"Sent MFA deadline reminder to {user.email} "
f"({days_until_deadline} days remaining)"
)
AuditService.log_action(
action=AuditAction.MFA_POLICY_USER_COMPLIANT,
user_id=user.id,
organization_id=compliance.organization_id,
description=f"MFA deadline reminder sent. Days remaining: {days_until_deadline}",
)
else:
logger.warning(
f"Failed to send MFA deadline reminder to {user.email}"
)
return success
logger.info(
f"Sent MFA deadline reminder to {user.email} "
f"({days_until_deadline} days remaining)"
)
AuditService.log_action(
action=AuditAction.MFA_POLICY_USER_COMPLIANT,
user_id=user.id,
organization_id=compliance.organization_id,
description=f"MFA deadline reminder sent. Days remaining: {days_until_deadline}",
)
return True
except Exception as e:
logger.exception(f"Error sending MFA deadline reminder to {user.email}: {e}")
@@ -136,27 +130,19 @@ class NotificationService:
)
# Send the notification
success = NotificationService._send_email(
NotificationService._send_email_async(
to_address=user.email,
subject=subject,
body=body,
)
if success:
logger.info(f"Sent MFA suspension notification to {user.email}")
# Audit log
AuditService.log_action(
action=AuditAction.MFA_POLICY_USER_SUSPENDED,
user_id=user.id,
organization_id=compliance.organization_id,
description="MFA compliance suspension notification sent",
)
else:
logger.warning(
f"Failed to send MFA suspension notification to {user.email}"
)
return success
logger.info(f"Sent MFA suspension notification to {user.email}")
AuditService.log_action(
action=AuditAction.MFA_POLICY_USER_SUSPENDED,
user_id=user.id,
organization_id=compliance.organization_id,
description="MFA compliance suspension notification sent",
)
return True
except Exception as e:
logger.exception(
@@ -285,89 +271,84 @@ Gatehouse Security Team
return body
@staticmethod
def _send_email(
def _send_email_async(
to_address: str,
subject: str,
body: str,
html_body: Optional[str] = None,
) -> bool:
"""Send an email via SMTP.
) -> None:
"""Send an email on a daemon thread so the calling request returns immediately.
Returns True if the email was sent successfully, False otherwise.
If EMAIL_ENABLED is False, logs the email body instead (simulation mode).
If EMAIL_ENABLED is False, logs instead of sending.
All SMTP exceptions are caught and logged — this method never raises.
The Flask app context is pushed inside the thread so current_app works correctly.
"""
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from flask import current_app
email_enabled = current_app.config.get(NotificationService.EMAIL_ENABLED_KEY, False)
app = current_app._get_current_object() # capture real app before leaving request context
if not email_enabled:
logger.info(
f"[EMAIL DISABLED] Would have sent to: {to_address} | Subject: {subject}\n"
f"Body: {body[:500]}"
)
return False
def _send():
with app.app_context():
email_enabled = app.config.get(NotificationService.EMAIL_ENABLED_KEY, False)
if not email_enabled:
logger.info(
f"[EMAIL DISABLED] Would have sent to: {to_address} | Subject: {subject}\n"
f"Body: {body[:500]}"
)
return
smtp_host = current_app.config.get(NotificationService.SMTP_HOST_KEY, "")
smtp_port_raw = current_app.config.get(NotificationService.SMTP_PORT_KEY, 587)
smtp_username = current_app.config.get(NotificationService.SMTP_USERNAME_KEY)
smtp_password = current_app.config.get(NotificationService.SMTP_PASSWORD_KEY)
from_address = current_app.config.get(
NotificationService.FROM_ADDRESS_KEY, ""
)
smtp_host = app.config.get(NotificationService.SMTP_HOST_KEY, "")
smtp_port_raw = app.config.get(NotificationService.SMTP_PORT_KEY, 587)
smtp_username = app.config.get(NotificationService.SMTP_USERNAME_KEY)
smtp_password = app.config.get(NotificationService.SMTP_PASSWORD_KEY)
from_address = app.config.get(NotificationService.FROM_ADDRESS_KEY, "")
# Guard: refuse to attempt a connection when critical config is missing.
# This surfaces a clear log message instead of a confusing socket error.
missing = [k for k, v in [
("SMTP_HOST", smtp_host),
("FROM_ADDRESS", from_address),
] if not v]
if missing:
logger.error(
f"[EMAIL] Cannot send — missing config: {', '.join(missing)}. "
f"Would have sent to: {to_address} | Subject: {subject}"
)
return False
missing = [k for k, v in [("SMTP_HOST", smtp_host), ("FROM_ADDRESS", from_address)] if not v]
if missing:
logger.error(
f"[EMAIL] Cannot send — missing config: {', '.join(missing)}. "
f"Would have sent to: {to_address} | Subject: {subject}"
)
return
try:
smtp_port = int(smtp_port_raw)
except (TypeError, ValueError):
logger.error(f"[EMAIL] Invalid SMTP_PORT value: {smtp_port_raw!r}")
return False
try:
smtp_port = int(smtp_port_raw)
except (TypeError, ValueError):
logger.error(f"[EMAIL] Invalid SMTP_PORT value: {smtp_port_raw!r}")
return
smtp_use_tls = current_app.config.get(
NotificationService.SMTP_USE_TLS_KEY,
smtp_port not in (25, 1025),
)
smtp_use_tls = app.config.get(
NotificationService.SMTP_USE_TLS_KEY,
smtp_port not in (25, 1025),
)
try:
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = from_address
msg["To"] = to_address
msg.attach(MIMEText(body, "plain"))
if html_body:
msg.attach(MIMEText(html_body, "html"))
try:
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = from_address
msg["To"] = to_address
msg.attach(MIMEText(body, "plain"))
if html_body:
msg.attach(MIMEText(html_body, "html"))
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.ehlo()
if smtp_use_tls:
server.starttls()
server.ehlo()
if smtp_username and smtp_password:
server.login(smtp_username, smtp_password)
server.send_message(msg)
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.ehlo()
if smtp_use_tls:
server.starttls()
server.ehlo()
if smtp_username and smtp_password:
server.login(smtp_username, smtp_password)
server.send_message(msg)
logger.info(f"[EMAIL] Sent to {to_address} | Subject: {subject}")
return True
logger.info(f"[EMAIL] Sent to {to_address} | Subject: {subject}")
except Exception as e:
logger.error(f"[EMAIL] Failed to send to {to_address}: {e}")
return False
except Exception as e:
logger.error(f"[EMAIL] Failed to send to {to_address}: {e}")
threading.Thread(target=_send, daemon=True).start()
@staticmethod
def get_notification_stats(user_id: str) -> Dict[str, Any]: