feat: send suspension emails and enhanced audit logs for MFA non-compliance
This commit is contained in:
@@ -23,9 +23,10 @@ from gatehouse_app.extensions import db
|
|||||||
from gatehouse_app.models.security.mfa_policy_compliance import MfaPolicyCompliance
|
from gatehouse_app.models.security.mfa_policy_compliance import MfaPolicyCompliance
|
||||||
from gatehouse_app.models.security.organization_security_policy import OrganizationSecurityPolicy
|
from gatehouse_app.models.security.organization_security_policy import OrganizationSecurityPolicy
|
||||||
from gatehouse_app.models.user.user import User
|
from gatehouse_app.models.user.user import User
|
||||||
|
from gatehouse_app.models.organization.organization_member import OrganizationMember
|
||||||
from gatehouse_app.services.mfa_policy_service import MfaPolicyService
|
from gatehouse_app.services.mfa_policy_service import MfaPolicyService
|
||||||
from gatehouse_app.services.notification_service import NotificationService
|
from gatehouse_app.services.notification_service import NotificationService
|
||||||
from gatehouse_app.utils.constants import MfaComplianceStatus
|
from gatehouse_app.utils.constants import MfaComplianceStatus, OrganizationRole
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -35,9 +36,11 @@ def process_mfa_compliance(now: Optional[datetime] = None) -> Dict[str, Any]:
|
|||||||
|
|
||||||
This scheduled job performs the following operations:
|
This scheduled job performs the following operations:
|
||||||
1. Transitions users from PAST_DUE to SUSPENDED status
|
1. Transitions users from PAST_DUE to SUSPENDED status
|
||||||
2. Identifies users approaching deadline (within notify_days_before)
|
2. Sends suspension notification to suspended users
|
||||||
3. Sends deadline reminder notifications
|
3. Sends suspension notification to org admins
|
||||||
4. Updates notification tracking metadata
|
4. Identifies users approaching deadline (within notify_days_before)
|
||||||
|
5. Sends deadline reminder notifications
|
||||||
|
6. Updates notification tracking metadata
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
now: Current time, defaults to now (UTC)
|
now: Current time, defaults to now (UTC)
|
||||||
@@ -45,7 +48,9 @@ def process_mfa_compliance(now: Optional[datetime] = None) -> Dict[str, Any]:
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with job execution statistics:
|
Dictionary with job execution statistics:
|
||||||
- suspended_count: Number of users transitioned to suspended
|
- suspended_count: Number of users transitioned to suspended
|
||||||
- notified_count: Number of notifications sent
|
- user_notified_count: Number of suspension emails sent to users
|
||||||
|
- admin_notified_count: Number of suspension emails sent to admins
|
||||||
|
- notified_count: Number of deadline reminder notifications sent
|
||||||
- processed_count: Total compliance records processed
|
- processed_count: Total compliance records processed
|
||||||
"""
|
"""
|
||||||
if now is None:
|
if now is None:
|
||||||
@@ -55,6 +60,8 @@ def process_mfa_compliance(now: Optional[datetime] = None) -> Dict[str, Any]:
|
|||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
"suspended_count": 0,
|
"suspended_count": 0,
|
||||||
|
"user_notified_count": 0,
|
||||||
|
"admin_notified_count": 0,
|
||||||
"notified_count": 0,
|
"notified_count": 0,
|
||||||
"processed_count": 0,
|
"processed_count": 0,
|
||||||
"errors": [],
|
"errors": [],
|
||||||
@@ -62,16 +69,67 @@ def process_mfa_compliance(now: Optional[datetime] = None) -> Dict[str, Any]:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Step 1: Transition past-due users to suspended
|
# Step 1: Transition past-due users to suspended
|
||||||
suspended_count = MfaPolicyService.transition_to_suspended_if_past_due(now)
|
suspended_records = MfaPolicyService.transition_to_suspended_if_past_due(now)
|
||||||
stats["suspended_count"] = suspended_count
|
stats["suspended_count"] = len(suspended_records)
|
||||||
logger.info(f"Transitioned {suspended_count} users to suspended status")
|
logger.info(f"Transitioned {len(suspended_records)} users to suspended status")
|
||||||
|
|
||||||
# Step 2: Send notifications to users approaching deadline
|
# Step 2: Send notifications for each suspended user
|
||||||
|
for entry in suspended_records:
|
||||||
|
try:
|
||||||
|
user = entry["user"]
|
||||||
|
compliance = entry["compliance"]
|
||||||
|
org_policy = entry["org_policy"]
|
||||||
|
|
||||||
|
# 2a: Send suspension notification to the user
|
||||||
|
user_notified = NotificationService.send_mfa_suspended_notification(
|
||||||
|
user=user,
|
||||||
|
compliance=compliance,
|
||||||
|
org_policy=org_policy,
|
||||||
|
)
|
||||||
|
if user_notified:
|
||||||
|
stats["user_notified_count"] += 1
|
||||||
|
logger.info(f"Sent suspension notice to user {user.email}")
|
||||||
|
|
||||||
|
# 2b: Send suspension notification to org admins
|
||||||
|
if org_policy:
|
||||||
|
admin_members = OrganizationMember.query.filter(
|
||||||
|
OrganizationMember.organization_id == compliance.organization_id,
|
||||||
|
OrganizationMember.role.in_([OrganizationRole.OWNER, OrganizationRole.ADMIN]),
|
||||||
|
OrganizationMember.deleted_at == None,
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for member in admin_members:
|
||||||
|
admin_user = User.query.get(member.user_id)
|
||||||
|
if not admin_user or not admin_user.email:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip notifying the suspended user themselves
|
||||||
|
if admin_user.id == user.id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
admin_notified = NotificationService.send_mfa_suspended_admin_notification(
|
||||||
|
admin_user=admin_user,
|
||||||
|
suspended_user=user,
|
||||||
|
compliance=compliance,
|
||||||
|
org_policy=org_policy,
|
||||||
|
)
|
||||||
|
if admin_notified:
|
||||||
|
stats["admin_notified_count"] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Error sending suspension notifications for compliance record "
|
||||||
|
f"{entry.get('compliance', {}).id if entry.get('compliance') else 'unknown'}: {e}"
|
||||||
|
)
|
||||||
|
stats["errors"].append(str(e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Step 3: Send notifications to users approaching deadline
|
||||||
notified_count = _send_deadline_reminders(now)
|
notified_count = _send_deadline_reminders(now)
|
||||||
stats["notified_count"] = notified_count
|
stats["notified_count"] = notified_count
|
||||||
logger.info(f"Sent {notified_count} deadline reminder notifications")
|
logger.info(f"Sent {notified_count} deadline reminder notifications")
|
||||||
|
|
||||||
# Step 3: Process any pending compliance evaluations
|
# Step 4: Process any pending compliance evaluations
|
||||||
processed_count = _evaluate_pending_compliance(now)
|
processed_count = _evaluate_pending_compliance(now)
|
||||||
stats["processed_count"] = processed_count
|
stats["processed_count"] = processed_count
|
||||||
logger.info(f"Processed {processed_count} compliance records")
|
logger.info(f"Processed {processed_count} compliance records")
|
||||||
@@ -82,7 +140,10 @@ def process_mfa_compliance(now: Optional[datetime] = None) -> Dict[str, Any]:
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"MFA compliance job completed: suspended={stats['suspended_count']}, "
|
f"MFA compliance job completed: suspended={stats['suspended_count']}, "
|
||||||
f"notified={stats['notified_count']}, processed={stats['processed_count']}"
|
f"user_notified={stats['user_notified_count']}, "
|
||||||
|
f"admin_notified={stats['admin_notified_count']}, "
|
||||||
|
f"deadline_reminders={stats['notified_count']}, "
|
||||||
|
f"processed={stats['processed_count']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|||||||
@@ -424,6 +424,70 @@ def build_mfa_suspension_html(
|
|||||||
return get_base_html(content, "Account Access Restricted - MFA Enrollment Required", "Your account has been suspended due to missing MFA")
|
return get_base_html(content, "Account Access Restricted - MFA Enrollment Required", "Your account has been suspended due to missing MFA")
|
||||||
|
|
||||||
|
|
||||||
|
def build_mfa_suspension_admin_html(
|
||||||
|
admin_name: str,
|
||||||
|
org_name: str,
|
||||||
|
suspended_user_name: str,
|
||||||
|
suspended_user_email: str,
|
||||||
|
mfa_methods: str,
|
||||||
|
members_link: str,
|
||||||
|
deadline_date: str = "",
|
||||||
|
days_overdue: int = 0,
|
||||||
|
) -> str:
|
||||||
|
"""Build MFA suspension notification email for org admins.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
admin_name: Admin's name or email
|
||||||
|
org_name: Organization name
|
||||||
|
suspended_user_name: Suspended user's name
|
||||||
|
suspended_user_email: Suspended user's email
|
||||||
|
mfa_methods: Required MFA methods
|
||||||
|
members_link: Link to manage org members
|
||||||
|
deadline_date: The deadline that was missed
|
||||||
|
days_overdue: Days past the deadline
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTML email string
|
||||||
|
"""
|
||||||
|
content = f'''
|
||||||
|
<h2 style="margin: 0 0 20px 0; color: {DANGER_COLOR}; font-size: 20px; font-weight: 600;">User Suspended - MFA Non-Compliance</h2>
|
||||||
|
<p style="margin: 0 0 20px 0; color: {TEXT_COLOR}; font-size: 15px; line-height: 1.6;">
|
||||||
|
Dear <strong>{admin_name}</strong>,
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 20px 0; color: {TEXT_COLOR}; font-size: 15px; line-height: 1.6;">
|
||||||
|
A user in your organization <strong>{org_name}</strong> has been suspended due to MFA non-compliance.
|
||||||
|
</p>
|
||||||
|
{get_alert_box(f"A user account has been automatically suspended for failing to meet MFA requirements.", "warning", "⚙️")}
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin: 20px 0; background-color: {BACKGROUND_COLOR}; border-radius: 8px;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 20px;">
|
||||||
|
<h3 style="margin: 0 0 12px 0; color: {TEXT_COLOR}; font-size: 14px; font-weight: 600;">Suspended User Details:</h3>
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
{get_detail_row("Name", suspended_user_name)}
|
||||||
|
{get_detail_row("Email", suspended_user_email)}
|
||||||
|
{get_detail_row("Organization", org_name)}
|
||||||
|
{get_detail_row("Required MFA", mfa_methods)}
|
||||||
|
{get_detail_row("Deadline", deadline_date) if deadline_date else ""}
|
||||||
|
{get_detail_row("Days Overdue", str(days_overdue)) if days_overdue else ""}
|
||||||
|
</table>
|
||||||
|
<h3 style="margin: 16px 0 12px 0; color: {TEXT_COLOR}; font-size: 14px; font-weight: 600;">What Happened:</h3>
|
||||||
|
<p style="margin: 0; color: {TEXT_COLOR}; font-size: 14px; line-height: 1.6;">
|
||||||
|
This user did not configure the required multi-factor authentication method(s) within the
|
||||||
|
allowed grace period. Their account has been automatically suspended and they will only
|
||||||
|
be able to access a compliance enrollment screen until MFA is configured.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{get_action_button(members_link, "Manage Organization Members", PRIMARY_COLOR)}
|
||||||
|
<p style="margin: 20px 0; color: {MUTED_COLOR}; font-size: 13px;">
|
||||||
|
You are receiving this notification because you are an administrator of <strong>{org_name}</strong>.
|
||||||
|
No action is required from you unless the user needs assistance setting up MFA.
|
||||||
|
</p>
|
||||||
|
'''
|
||||||
|
return get_base_html(content, f"User Suspended - MFA Non-Compliance in {org_name}", f"A user in {org_name} has been suspended for missing MFA deadline")
|
||||||
|
|
||||||
|
|
||||||
def build_org_invite_html(
|
def build_org_invite_html(
|
||||||
inviter_name: str,
|
inviter_name: str,
|
||||||
org_name: str,
|
org_name: str,
|
||||||
|
|||||||
@@ -403,19 +403,19 @@ class MfaPolicyService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
"""Scheduled job to transition past-due users to suspended status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
now: Current time, defaults to now
|
now: Current time, defaults to now
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of users transitioned to suspended
|
List of dicts with suspended record details (user, compliance, org_policy)
|
||||||
"""
|
"""
|
||||||
if now is None:
|
if now is None:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
suspended_count = 0
|
suspended_records = []
|
||||||
|
|
||||||
# Find all compliance records that are past due
|
# Find all compliance records that are past due
|
||||||
past_due_records = MfaPolicyCompliance.query.filter(
|
past_due_records = MfaPolicyCompliance.query.filter(
|
||||||
@@ -437,21 +437,65 @@ class MfaPolicyService:
|
|||||||
|
|
||||||
# Update user status
|
# Update user status
|
||||||
user = User.query.get(record.user_id)
|
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
|
user.status = UserStatus.COMPLIANCE_SUSPENDED
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Audit log
|
# Get org policy for extended details
|
||||||
|
org_policy = OrganizationSecurityPolicy.query.filter_by(
|
||||||
|
organization_id=record.organization_id, deleted_at=None
|
||||||
|
).first()
|
||||||
|
|
||||||
|
days_overdue = (now - deadline).days if deadline else 0
|
||||||
|
|
||||||
|
# Audit log for user (with extended details)
|
||||||
AuditService.log_action(
|
AuditService.log_action(
|
||||||
action=AuditAction.MFA_POLICY_USER_SUSPENDED,
|
action=AuditAction.MFA_POLICY_USER_SUSPENDED,
|
||||||
user_id=record.user_id,
|
user_id=record.user_id,
|
||||||
organization_id=record.organization_id,
|
organization_id=record.organization_id,
|
||||||
|
resource_type="user",
|
||||||
|
resource_id=record.user_id,
|
||||||
description=f"User suspended due to MFA compliance deadline passed",
|
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",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
suspended_count += 1
|
# 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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return suspended_count
|
suspended_records.append({
|
||||||
|
"user": user,
|
||||||
|
"compliance": record,
|
||||||
|
"org_policy": org_policy,
|
||||||
|
})
|
||||||
|
|
||||||
|
return suspended_records
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_org_policy(
|
def create_org_policy(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from gatehouse_app.services.email_provider import EmailMessage, EmailProviderFac
|
|||||||
from gatehouse_app.services.email_templates import (
|
from gatehouse_app.services.email_templates import (
|
||||||
build_mfa_deadline_reminder_html,
|
build_mfa_deadline_reminder_html,
|
||||||
build_mfa_suspension_html,
|
build_mfa_suspension_html,
|
||||||
|
build_mfa_suspension_admin_html,
|
||||||
)
|
)
|
||||||
from gatehouse_app.utils.constants import AuditAction
|
from gatehouse_app.utils.constants import AuditAction
|
||||||
|
|
||||||
@@ -209,6 +210,100 @@ class NotificationService:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_mfa_suspended_admin_notification(
|
||||||
|
admin_user: User,
|
||||||
|
suspended_user: User,
|
||||||
|
compliance: MfaPolicyCompliance,
|
||||||
|
org_policy: OrganizationSecurityPolicy,
|
||||||
|
) -> bool:
|
||||||
|
"""Notify org admin that a user has been suspended for MFA non-compliance.
|
||||||
|
|
||||||
|
Sends an email to organization admins/owners when a member of their
|
||||||
|
organization has been automatically suspended for failing to meet MFA
|
||||||
|
compliance requirements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
admin_user: Admin/owner to notify
|
||||||
|
suspended_user: The user who was suspended
|
||||||
|
compliance: Suspended user's compliance record
|
||||||
|
org_policy: Organization's MFA policy
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if notification was sent successfully, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
org_name = compliance.organization_id
|
||||||
|
from gatehouse_app.models.organization.organization import Organization
|
||||||
|
org = Organization.query.get(compliance.organization_id)
|
||||||
|
if org:
|
||||||
|
org_name = org.name
|
||||||
|
|
||||||
|
from gatehouse_app.utils.constants import MfaPolicyMode
|
||||||
|
mfa_methods = "Multi-factor authentication"
|
||||||
|
mode = org_policy.mfa_policy_mode
|
||||||
|
if mode == MfaPolicyMode.REQUIRE_TOTP:
|
||||||
|
mfa_methods = "Authenticator app (TOTP)"
|
||||||
|
elif mode == MfaPolicyMode.REQUIRE_WEBAUTHN:
|
||||||
|
mfa_methods = "Passkey (WebAuthn)"
|
||||||
|
elif mode == MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN:
|
||||||
|
mfa_methods = "Authenticator app (TOTP) OR Passkey (WebAuthn)"
|
||||||
|
|
||||||
|
app_url = current_app.config.get("APP_URL", "http://localhost:8080")
|
||||||
|
members_link = f"{app_url}/organizations/{compliance.organization_id}/members"
|
||||||
|
|
||||||
|
deadline_str = compliance.deadline_at.strftime('%Y-%m-%d %H:%M UTC') if compliance.deadline_at else ''
|
||||||
|
days_overdue = 0
|
||||||
|
if compliance.deadline_at:
|
||||||
|
deadline = compliance.deadline_at
|
||||||
|
if deadline.tzinfo is None:
|
||||||
|
deadline = deadline.replace(tzinfo=timezone.utc)
|
||||||
|
from datetime import timezone as dt_tz
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
days_overdue = max(0, (now - deadline).days)
|
||||||
|
|
||||||
|
subject = f"User Suspended - MFA Non-Compliance in {org_name}"
|
||||||
|
html_body = build_mfa_suspension_admin_html(
|
||||||
|
admin_name=admin_user.full_name or admin_user.email,
|
||||||
|
org_name=org_name,
|
||||||
|
suspended_user_name=suspended_user.full_name or suspended_user.email,
|
||||||
|
suspended_user_email=suspended_user.email,
|
||||||
|
mfa_methods=mfa_methods,
|
||||||
|
members_link=members_link,
|
||||||
|
deadline_date=deadline_str,
|
||||||
|
days_overdue=days_overdue,
|
||||||
|
)
|
||||||
|
|
||||||
|
NotificationService._send_email_async(
|
||||||
|
to_address=admin_user.email,
|
||||||
|
subject=subject,
|
||||||
|
body=f"A user ({suspended_user.email}) in {org_name} has been suspended for MFA non-compliance. Manage members: {members_link}",
|
||||||
|
html_body=html_body,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Sent MFA suspension admin notification to {admin_user.email} "
|
||||||
|
f"regarding suspended user {suspended_user.email}"
|
||||||
|
)
|
||||||
|
AuditService.log_action(
|
||||||
|
action=AuditAction.MFA_SUSPENSION_ADMIN_NOTIFICATION_SENT,
|
||||||
|
user_id=suspended_user.id,
|
||||||
|
organization_id=compliance.organization_id,
|
||||||
|
description=f"Admin {admin_user.email} notified about MFA suspension of user {suspended_user.email}",
|
||||||
|
metadata={
|
||||||
|
"admin_user_id": admin_user.id,
|
||||||
|
"admin_email": admin_user.email,
|
||||||
|
"suspended_user_email": suspended_user.email,
|
||||||
|
"org_name": org_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
f"Error sending MFA suspension admin notification to {admin_user.email}: {e}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_deadline_reminder_body(
|
def _build_deadline_reminder_body(
|
||||||
user: User,
|
user: User,
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class AuditAction(str, Enum):
|
|||||||
MFA_COMPLIANCE_BYPASS_ATTEMPT = "mfa.compliance.bypass_attempt"
|
MFA_COMPLIANCE_BYPASS_ATTEMPT = "mfa.compliance.bypass_attempt"
|
||||||
MFA_NOTIFICATION_SENT = "mfa.notification.sent"
|
MFA_NOTIFICATION_SENT = "mfa.notification.sent"
|
||||||
MFA_SUSPENSION_NOTIFICATION_SENT = "mfa.suspension_notification.sent"
|
MFA_SUSPENSION_NOTIFICATION_SENT = "mfa.suspension_notification.sent"
|
||||||
|
MFA_SUSPENSION_ADMIN_NOTIFICATION_SENT = "mfa.suspension_admin_notification.sent"
|
||||||
|
|
||||||
# Organization actions
|
# Organization actions
|
||||||
ORG_CREATE = "org.create"
|
ORG_CREATE = "org.create"
|
||||||
|
|||||||
Reference in New Issue
Block a user