diff --git a/fixbackend/cloud_accounts/service_impl.py b/fixbackend/cloud_accounts/service_impl.py index 847caa9e..0a8a7bb0 100644 --- a/fixbackend/cloud_accounts/service_impl.py +++ b/fixbackend/cloud_accounts/service_impl.py @@ -65,7 +65,7 @@ WorkspaceId, ) from fixbackend.logging_context import set_cloud_account_id, set_fix_cloud_account_id, set_workspace_id -from fixbackend.notification.email.email_messages import SecurityScanFinished +from fixbackend.notification.email import email_messages as email from fixbackend.notification.notification_service import NotificationService from fixbackend.sqs import SQSRawListener from fixbackend.utils import uid @@ -319,7 +319,7 @@ def compute_failed_scan_count(acc: CloudAccount) -> int: await self.analytics_event_sender.send(AEFirstWorkspaceCollectFinished(user_id, event.tenant_id)) # inform workspace users about the first successful collect await self.notification_service.send_message_to_workspace( - workspace_id=event.tenant_id, message=SecurityScanFinished() + workspace_id=event.tenant_id, message=email.SecurityScanFinished() ) if first_account_collect: await self.analytics_event_sender.send(AEFirstAccountCollectFinished(user_id, event.tenant_id)) @@ -351,6 +351,10 @@ def compute_failed_scan_count(acc: CloudAccount) -> int: case AwsAccountDegraded.kind: degraded_event = AwsAccountDegraded.from_json(message) + await self.notification_service.send_message_to_workspace( + workspace_id=degraded_event.tenant_id, + message=email.AccountDegraded(cloud_account_id=degraded_event.aws_account_id), + ) await send_pub_sub_message(degraded_event) case _: diff --git a/fixbackend/notification/email/email_messages.py b/fixbackend/notification/email/email_messages.py index 2bffc8d5..98287bec 100644 --- a/fixbackend/notification/email/email_messages.py +++ b/fixbackend/notification/email/email_messages.py @@ -19,6 +19,8 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined from functools import lru_cache +from fixbackend.ids import CloudAccountId + @lru_cache(maxsize=1) def get_env() -> Environment: @@ -136,8 +138,24 @@ def html(self) -> str: "security_scan_finished.html", title=self.subject(), fix_console_url="https://app.global.fixcloud.io/", - foo="foo", ) -EmailMessage = Union[Signup, Invite, VerifyEmail, SecurityScanFinished, PasswordReset] +@frozen(kw_only=True) +class AccountDegraded: + cloud_account_id: CloudAccountId + + def subject(self) -> str: + return f"FIX: Account {self.cloud_account_id} Degraded" + + def text(self) -> str: + return f"Account {self.cloud_account_id} can't be collected and now in degraded state. Please check that the account exists." + + def html(self) -> str: + return render( + "account_degraded.html", + message=self, + ) + + +EmailMessage = Union[Signup, Invite, VerifyEmail, SecurityScanFinished, PasswordReset, AccountDegraded] diff --git a/fixbackend/notification/email/templates/account_degraded.html b/fixbackend/notification/email/templates/account_degraded.html new file mode 100644 index 00000000..ea9e3fb1 --- /dev/null +++ b/fixbackend/notification/email/templates/account_degraded.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block content %} + + +
+

+ Account {{ message.cloud_account_id }} degraded! +

+ +

+ One of your accounts can't be collected and is degraded now! Please check the account {{ + message.cloud_account_id }} for issues. +

+ +
+ +{% endblock content %} \ No newline at end of file diff --git a/fixbackend/notification/notification_service.py b/fixbackend/notification/notification_service.py index 2c95cb21..ac379260 100644 --- a/fixbackend/notification/notification_service.py +++ b/fixbackend/notification/notification_service.py @@ -139,9 +139,12 @@ async def send_message_to_workspace(self, *, workspace_id: WorkspaceId, message: emails = [user.email for user in await self.user_repository.get_by_ids(workspace.all_users())] for email in emails: - await self.email_sender.send_email( - to=email, subject=message.subject(), text=message.text(), html=message.html() - ) + try: + await self.email_sender.send_email( + to=email, subject=message.subject(), text=message.text(), html=message.html() + ) + except Exception as e: + log.error(f"Failed to send message to workspace {workspace_id}: {e}") async def list_notification_provider_configs(self, workspace_id: WorkspaceId) -> Dict[str, Json]: configs = await self.provider_config_repo.all_messaging_configs_for_workspace(workspace_id) diff --git a/tests/fixbackend/notification/notification_service_test.py b/tests/fixbackend/notification/notification_service_test.py index 3eb04a1a..1b0544fa 100644 --- a/tests/fixbackend/notification/notification_service_test.py +++ b/tests/fixbackend/notification/notification_service_test.py @@ -33,7 +33,7 @@ NodeId, BenchmarkName, ) -from fixbackend.notification.email.email_messages import SecurityScanFinished +from fixbackend.notification.email.email_messages import AccountDegraded, SecurityScanFinished from fixbackend.notification.model import ( WorkspaceAlert, AlertingSetting, @@ -274,3 +274,17 @@ async def test_send_test_alert( # ensure that an alert was created assert len(inventory_requests) == 1 assert str(inventory_requests[0].url) == "https://discord.com/webhook_example" + + +@pytest.mark.asyncio +async def test_send_degraded_message( + notification_service: NotificationService, + workspace: Workspace, + email_sender: InMemoryEmailSender, +) -> None: + message = AccountDegraded(cloud_account_id=CloudAccountId("12345")) + await notification_service.send_message_to_workspace(workspace_id=workspace.id, message=message) + + assert len(email_sender.call_args) == 1 + assert email_sender.call_args[0].subject == "FIX: Account 12345 Degraded" + assert "Account 12345 degraded!" in (email_sender.call_args[0].html or "")