feat: send suspension emails and enhanced audit logs for MFA non-compliance

This commit is contained in:
2026-05-29 05:27:49 +00:00
parent 13767d3fa1
commit f869f6c06d
5 changed files with 289 additions and 24 deletions
+57 -13
View File
@@ -403,19 +403,19 @@ class MfaPolicyService:
)
@staticmethod
def transition_to_suspended_if_past_due(now: Optional[datetime] = None) -> int:
def transition_to_suspended_if_past_due(now: Optional[datetime] = None) -> List[Dict[str, Any]]:
"""Scheduled job to transition past-due users to suspended status.
Args:
now: Current time, defaults to now
Returns:
Number of users transitioned to suspended
List of dicts with suspended record details (user, compliance, org_policy)
"""
if now is None:
now = datetime.now(timezone.utc)
suspended_count = 0
suspended_records = []
# Find all compliance records that are past due
past_due_records = MfaPolicyCompliance.query.filter(
@@ -437,21 +437,65 @@ class MfaPolicyService:
# Update user status
user = User.query.get(record.user_id)
if user and user.status != UserStatus.COMPLIANCE_SUSPENDED:
if not user:
continue
if user.status != UserStatus.COMPLIANCE_SUSPENDED:
user.status = UserStatus.COMPLIANCE_SUSPENDED
db.session.commit()
# Audit log
AuditService.log_action(
action=AuditAction.MFA_POLICY_USER_SUSPENDED,
user_id=record.user_id,
organization_id=record.organization_id,
description=f"User suspended due to MFA compliance deadline passed",
)
# Get org policy for extended details
org_policy = OrganizationSecurityPolicy.query.filter_by(
organization_id=record.organization_id, deleted_at=None
).first()
suspended_count += 1
days_overdue = (now - deadline).days if deadline else 0
return suspended_count
# Audit log for user (with extended details)
AuditService.log_action(
action=AuditAction.MFA_POLICY_USER_SUSPENDED,
user_id=record.user_id,
organization_id=record.organization_id,
resource_type="user",
resource_id=record.user_id,
description=f"User suspended due to MFA compliance deadline passed",
metadata={
"deadline_at": record.deadline_at.isoformat() if record.deadline_at else None,
"suspended_at": now.isoformat(),
"days_overdue": days_overdue,
"user_email": user.email,
"policy_mode": org_policy.mfa_policy_mode.value if org_policy else None,
"grace_period_days": org_policy.mfa_grace_period_days if org_policy else None,
"policy_version": org_policy.policy_version if org_policy else None,
"reason": "MFA compliance deadline passed without required enrollment",
},
)
# Audit log for org (org-scoped entry for admin visibility)
AuditService.log_action(
action=AuditAction.MFA_POLICY_USER_SUSPENDED,
user_id=None,
organization_id=record.organization_id,
resource_type="user",
resource_id=record.user_id,
description=f"Organization member {user.email} suspended due to MFA non-compliance",
metadata={
"suspended_user_id": record.user_id,
"suspended_user_email": user.email,
"deadline_at": record.deadline_at.isoformat() if record.deadline_at else None,
"suspended_at": now.isoformat(),
"days_overdue": days_overdue,
"policy_mode": org_policy.mfa_policy_mode.value if org_policy else None,
},
)
suspended_records.append({
"user": user,
"compliance": record,
"org_policy": org_policy,
})
return suspended_records
@staticmethod
def create_org_policy(