From 9ac01b51458b1d325f7f4232b635d2331287203f Mon Sep 17 00:00:00 2001 From: Ionesio Junior Date: Wed, 21 Feb 2024 21:54:09 -0300 Subject: [PATCH 1/4] - ADD support to email templates - Small fix at init_notifier - ADD Suspicious Activity Notification for non authorized register attempts - Update notifier.send methods --- .../service/notification/email_templates.py | 195 ++++++++++++++++++ .../notification/notification_service.py | 3 +- .../service/notification/notifications.py | 3 + .../src/syft/service/notifier/notifier.py | 18 +- .../syft/service/notifier/notifier_service.py | 8 +- .../src/syft/service/notifier/smtp_client.py | 2 +- .../syft/service/request/request_service.py | 2 + .../src/syft/service/user/user_service.py | 38 +++- 8 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 packages/syft/src/syft/service/notification/email_templates.py diff --git a/packages/syft/src/syft/service/notification/email_templates.py b/packages/syft/src/syft/service/notification/email_templates.py new file mode 100644 index 00000000000..85727817554 --- /dev/null +++ b/packages/syft/src/syft/service/notification/email_templates.py @@ -0,0 +1,195 @@ +from ...abstract_node import AbstractNode +from ..context import AuthedServiceContext + +class EmailTemplate: + pass + +class SuspiciousActivityEmailTemplate(EmailTemplate): + + @staticmethod + def email_title(notification: "Notification", context: AuthedServiceContext) -> str: + return f"Domain {context.node.name}: Suspicious Activity Detected!" + + @staticmethod + def email_body(notification: "Notification", context: AuthedServiceContext) -> str: + user_service = context.node.get_service("userservice") + user = notification.linked_obj.resolve_with_context( + context=context + ).ok() + + head = """ + + Suspicious Activity Alert + + + """ + + body = f""" + +
+

Suspicious Activity Detected

+

Hello Admin,

+

We have detected some suspicious activities in your domain node. Please review the following details:

+
+

Date and Time of Activity: {notification.created_at}

+

Type of Activity: Unauthorized Register Attempt

+

User : {user.name} {user.email}

+
+

We recommend you to take the following actions:

+ +

Stay Safe,
Your {context.node.name} Team

+
+ + + """ + return f"""{head} {body}""" + +class RequestEmailTemplate(EmailTemplate): + + @staticmethod + def email_title(notification: "Notification", context: AuthedServiceContext) -> str: + return f"Domain {context.node.name}: New Request!" + + @staticmethod + def email_body(notification: "Notification", context: AuthedServiceContext) -> str: + request_obj = notification.linked_obj.resolve_with_context( + context=context + ).ok() + + head = """ + + Access Request Notification + + """ + + body = f""" + +
+
+ Request Notification +
+
+

Hello,

+

A new request has been submitted and requires your attention. Please review the details below:

+ +
+
Request Details
+
+ +

ID: {request_obj.id}

+

Submitted By: {request_obj.requesting_user_name} {request_obj.requesting_user_email or ""}

+

Date: {request_obj.request_time}

+

Changes: {",".join([change.__class__.__name__ for change in request_obj.changes])}

+
+
+

To review and respond to this request, please click the button below:

+ Review Request +

If you did not expect this request or have concerns about it, please contact our support team immediately.

+
+ +
+ + """ + return f"""{head} {body}""" diff --git a/packages/syft/src/syft/service/notification/notification_service.py b/packages/syft/src/syft/service/notification/notification_service.py index 0437d867443..8430e351b35 100644 --- a/packages/syft/src/syft/service/notification/notification_service.py +++ b/packages/syft/src/syft/service/notification/notification_service.py @@ -40,7 +40,6 @@ def send( self, context: AuthedServiceContext, notification: CreateNotification ) -> Union[Notification, SyftError]: """Send a new notification""" - new_notification = notification.to(Notification, context=context) # Add read permissions to person receiving this message @@ -55,7 +54,7 @@ def send( ) notifier_service = context.node.get_service("notifierservice") - res = notifier_service.dispatch_notification(context.node, new_notification) + res = notifier_service.dispatch_notification(context, new_notification) if isinstance(res, SyftError): return res diff --git a/packages/syft/src/syft/service/notification/notifications.py b/packages/syft/src/syft/service/notification/notifications.py index a903a8e87b2..56750033e1c 100644 --- a/packages/syft/src/syft/service/notification/notifications.py +++ b/packages/syft/src/syft/service/notification/notifications.py @@ -2,6 +2,7 @@ from enum import Enum from typing import List from typing import Optional +from typing import Type # relative from ...client.api import APIRegistry @@ -24,6 +25,7 @@ from ...util import options from ...util.colors import SURFACE from ..notifier.notifier_enums import NOTIFIERS +from .email_templates import EmailTemplate @serializable() @@ -139,6 +141,7 @@ class Notification(SyftObject): status: NotificationStatus = NotificationStatus.UNREAD linked_obj: Optional[LinkedObject] notifier_types: Optional[List[NOTIFIERS]] = [] + email_template: Optional[Type[EmailTemplate]] = None replies: Optional[List[ReplyNotification]] = [] __attr_searchable__ = [ diff --git a/packages/syft/src/syft/service/notifier/notifier.py b/packages/syft/src/syft/service/notifier/notifier.py index 3fe30ed8d4b..af56041392c 100644 --- a/packages/syft/src/syft/service/notifier/notifier.py +++ b/packages/syft/src/syft/service/notifier/notifier.py @@ -13,7 +13,7 @@ from result import Result # relative -from ...abstract_node import AbstractNode +from ..context import AuthedServiceContext from ...node.credentials import SyftVerifyKey from ...serde.serializable import serializable from ...types.syft_object import SYFT_OBJECT_VERSION_1 @@ -76,18 +76,22 @@ def check_credentials( password=password, ) - def send(self, node: AbstractNode, notification: Notification) -> Result[Ok, Err]: + def send(self, context: AuthedServiceContext, notification: Notification) -> Result[Ok, Err]: try: - user_service = node.get_service("userservice") + user_service = context.node.get_service("userservice") sender_email = user_service.get_by_verify_key( notification.from_user_verify_key ).email receiver_email = user_service.get_by_verify_key( notification.to_user_verify_key ).email + print(f"sender_email: {sender_email}") + print(f"receiver_email: {receiver_email}") - subject = notification.subject - body = "Testing email notification!" + subject = notification.email_template.email_title(notification, context=context) + body = notification.email_template.email_body(notification, context=context) + # subject = notification.subject + # body = "Testing email notification!" if isinstance(receiver_email, str): receiver_email = [receiver_email] @@ -165,13 +169,13 @@ def validate_email_credentials( def send_notifications( self, - node: AbstractNode, + context: AuthedServiceContext, notification: Notification, ) -> Result[Ok, Err]: notifier_objs: List = self.select_notifiers(notification) for notifier in notifier_objs: - result = notifier.send(node, notification) + result = notifier.send(context, notification) if result.err(): return result diff --git a/packages/syft/src/syft/service/notifier/notifier_service.py b/packages/syft/src/syft/service/notifier/notifier_service.py index 7c92d1451b7..35599d87147 100644 --- a/packages/syft/src/syft/service/notifier/notifier_service.py +++ b/packages/syft/src/syft/service/notifier/notifier_service.py @@ -275,9 +275,9 @@ def init_notifier( # This is not a public API. # This method is used by other services to dispatch notifications internally def dispatch_notification( - self, node: AbstractNode, notification: Notification - ) -> Union[SyftSuccess, SyftError]: - admin_key = node.get_service("userservice").admin_verify_key() + self, context: AuthedServiceContext, notification: Notification + ) -> Union[SyftError]: + admin_key = context.node.get_service("userservice").admin_verify_key() notifier = self.stash.get(admin_key) if notifier.is_err(): return SyftError( @@ -288,7 +288,7 @@ def dispatch_notification( notifier: NotifierSettings = notifier.ok() # If notifier is active if notifier.active: - resp = notifier.send_notifications(node=node, notification=notification) + resp = notifier.send_notifications(context=context, notification=notification) if resp.is_err(): return SyftError(message=resp.err()) diff --git a/packages/syft/src/syft/service/notifier/smtp_client.py b/packages/syft/src/syft/service/notifier/smtp_client.py index dfa7be2e2a8..1f4df6531e5 100644 --- a/packages/syft/src/syft/service/notifier/smtp_client.py +++ b/packages/syft/src/syft/service/notifier/smtp_client.py @@ -35,7 +35,7 @@ def send(self, sender: str, receiver: list[str], subject: str, body: str) -> Non msg["From"] = sender msg["To"] = ", ".join(receiver) msg["Subject"] = subject - msg.attach(MIMEText(body, "plain")) + msg.attach(MIMEText(body, "html")) with smtplib.SMTP( self.server, self.port, timeout=self.SOCKET_TIMEOUT diff --git a/packages/syft/src/syft/service/request/request_service.py b/packages/syft/src/syft/service/request/request_service.py index 7dfeeafa5be..3d523cc1069 100644 --- a/packages/syft/src/syft/service/request/request_service.py +++ b/packages/syft/src/syft/service/request/request_service.py @@ -22,6 +22,7 @@ from ..notification.notification_service import CreateNotification from ..notification.notification_service import NotificationService from ..notification.notifications import Notification +from ..notification.email_templates import RequestEmailTemplate from ..notifier.notifier_enums import NOTIFIERS from ..response import SyftError from ..response import SyftSuccess @@ -89,6 +90,7 @@ def submit( to_user_verify_key=root_verify_key, linked_obj=link, notifier_types=[NOTIFIERS.EMAIL], + email_template=RequestEmailTemplate, ) method = context.node.get_service_method(NotificationService.send) result = method(context=context, notification=message) diff --git a/packages/syft/src/syft/service/user/user_service.py b/packages/syft/src/syft/service/user/user_service.py index 8bdb28bc275..93119facd93 100644 --- a/packages/syft/src/syft/service/user/user_service.py +++ b/packages/syft/src/syft/service/user/user_service.py @@ -4,6 +4,10 @@ from typing import Tuple from typing import Union +# third party +from result import Err +from result import Ok + # relative from ...abstract_node import NodeType from ...exceptions.user import UserAlreadyExistsException @@ -29,6 +33,7 @@ from ..settings.settings_stash import SettingsStash from .user import User from .user import UserCreate +from ...store.linked_obj import LinkedObject from .user import UserPrivateKey from .user import UserSearch from .user import UserUpdate @@ -41,7 +46,11 @@ from .user_roles import ServiceRole from .user_roles import ServiceRoleCapability from .user_stash import UserStash - +from ..notification.notification_service import CreateNotification +from ..notification.notification_service import NotificationService +from ..notification.email_templates import SuspiciousActivityEmailTemplate +from ..notification.notifications import Notification +from ..notifier.notifier_enums import NOTIFIERS @instrument @serializable() @@ -425,6 +434,33 @@ def register( ) if not can_user_register: + root_key = self.admin_verify_key() + root_context = AuthedServiceContext( + node=context.node, credentials=root_key + ) + link = None + if new_user.created_by: + user = self.stash.get_by_signing_key( + credentials=root_context.credentials, + signing_key=new_user.created_by + ).ok() + link = LinkedObject.with_context( + user, + context=root_context + ) + message = CreateNotification( + subject="Not allowed register attempt", + from_user_verify_key=root_key, + to_user_verify_key=root_key, + linked_obj=link, + notifier_types=[NOTIFIERS.EMAIL], + email_template=SuspiciousActivityEmailTemplate, + ) + + method = context.node.get_service_method(NotificationService.send) + result = method(context=root_context, notification=message) + if not isinstance(result, Notification): + return Err(result) return SyftError( message=f"You don't have permission to create an account " f"on the domain: {context.node.name}. Please contact the Domain Owner." From bcc1a4f57ff07d02ef337bbe33f49a8dbc8be55a Mon Sep 17 00:00:00 2001 From: Ionesio Junior Date: Thu, 22 Feb 2024 16:45:39 -0300 Subject: [PATCH 2/4] Replace Suspicious Activity -> OnBoard email --- .../service/notification/email_templates.py | 93 ++++++++++--------- .../src/syft/service/notifier/notifier.py | 14 +-- .../syft/service/notifier/notifier_service.py | 4 +- .../syft/service/request/request_service.py | 2 +- .../src/syft/service/user/user_service.py | 60 +++++------- 5 files changed, 87 insertions(+), 86 deletions(-) diff --git a/packages/syft/src/syft/service/notification/email_templates.py b/packages/syft/src/syft/service/notification/email_templates.py index 85727817554..ab601494c03 100644 --- a/packages/syft/src/syft/service/notification/email_templates.py +++ b/packages/syft/src/syft/service/notification/email_templates.py @@ -1,25 +1,29 @@ -from ...abstract_node import AbstractNode +# relative from ..context import AuthedServiceContext + class EmailTemplate: pass -class SuspiciousActivityEmailTemplate(EmailTemplate): +class OnBoardEmailTemplate(EmailTemplate): @staticmethod def email_title(notification: "Notification", context: AuthedServiceContext) -> str: - return f"Domain {context.node.name}: Suspicious Activity Detected!" - + return f"Welcome to {context.node.name} node!" + @staticmethod def email_body(notification: "Notification", context: AuthedServiceContext) -> str: user_service = context.node.get_service("userservice") - user = notification.linked_obj.resolve_with_context( - context=context - ).ok() + admin_name = user_service.get_by_verify_key( + user_service.admin_verify_key() + ).name - head = """ + head = ( + f""" - Suspicious Activity Alert + Welcome to {context.node.name} + """ + + """ """ + ) body = f"""
-

Suspicious Activity Detected

-

Hello Admin,

-

We have detected some suspicious activities in your domain node. Please review the following details:

-
-

Date and Time of Activity: {notification.created_at}

-

Type of Activity: Unauthorized Register Attempt

-

User : {user.name} {user.email}

+

Welcome to {context.node.name} node!

+

Hello,

+

We're thrilled to have you on board and excited to help you get started with our powerful features:

+ +
+

Remote Data Science

+

Access and analyze data from anywhere, using our comprehensive suite of data science tools.

+
+ +
+

Remote Code Execution

+

Execute code remotely on private data, ensuring flexibility and efficiency in your research.

+
+ + + +

Explore these features and much more within your account. If you have any questions or need assistance, don't hesitate to reach out.

+ +

Cheers,

+

{admin_name}

+ + -

We recommend you to take the following actions:

-
    -
  • Identify the person who tried it.
  • -
  • Investigate his/her reasons.
  • -
  • Contact support if you notice any unfamiliar activity.
  • -
-

Stay Safe,
Your {context.node.name} Team

-
- """ return f"""{head} {body}""" -class RequestEmailTemplate(EmailTemplate): +class RequestEmailTemplate(EmailTemplate): @staticmethod def email_title(notification: "Notification", context: AuthedServiceContext) -> str: return f"Domain {context.node.name}: New Request!" - + @staticmethod def email_body(notification: "Notification", context: AuthedServiceContext) -> str: - request_obj = notification.linked_obj.resolve_with_context( - context=context - ).ok() + request_obj = notification.linked_obj.resolve_with_context(context=context).ok() head = """ @@ -180,7 +189,7 @@ def email_body(notification: "Notification", context: AuthedServiceContext) -> s

Date: {request_obj.request_time}

Changes: {",".join([change.__class__.__name__ for change in request_obj.changes])}

- +

To review and respond to this request, please click the button below:

Review Request

If you did not expect this request or have concerns about it, please contact our support team immediately.

diff --git a/packages/syft/src/syft/service/notifier/notifier.py b/packages/syft/src/syft/service/notifier/notifier.py index af56041392c..d334ac43dd8 100644 --- a/packages/syft/src/syft/service/notifier/notifier.py +++ b/packages/syft/src/syft/service/notifier/notifier.py @@ -13,11 +13,11 @@ from result import Result # relative -from ..context import AuthedServiceContext from ...node.credentials import SyftVerifyKey from ...serde.serializable import serializable from ...types.syft_object import SYFT_OBJECT_VERSION_1 from ...types.syft_object import SyftObject +from ..context import AuthedServiceContext from ..notification.notifications import Notification from ..response import SyftError from ..response import SyftSuccess @@ -76,7 +76,9 @@ def check_credentials( password=password, ) - def send(self, context: AuthedServiceContext, notification: Notification) -> Result[Ok, Err]: + def send( + self, context: AuthedServiceContext, notification: Notification + ) -> Result[Ok, Err]: try: user_service = context.node.get_service("userservice") sender_email = user_service.get_by_verify_key( @@ -85,13 +87,11 @@ def send(self, context: AuthedServiceContext, notification: Notification) -> Res receiver_email = user_service.get_by_verify_key( notification.to_user_verify_key ).email - print(f"sender_email: {sender_email}") - print(f"receiver_email: {receiver_email}") - subject = notification.email_template.email_title(notification, context=context) + subject = notification.email_template.email_title( + notification, context=context + ) body = notification.email_template.email_body(notification, context=context) - # subject = notification.subject - # body = "Testing email notification!" if isinstance(receiver_email, str): receiver_email = [receiver_email] diff --git a/packages/syft/src/syft/service/notifier/notifier_service.py b/packages/syft/src/syft/service/notifier/notifier_service.py index 35599d87147..1e5b3f3d49d 100644 --- a/packages/syft/src/syft/service/notifier/notifier_service.py +++ b/packages/syft/src/syft/service/notifier/notifier_service.py @@ -288,7 +288,9 @@ def dispatch_notification( notifier: NotifierSettings = notifier.ok() # If notifier is active if notifier.active: - resp = notifier.send_notifications(context=context, notification=notification) + resp = notifier.send_notifications( + context=context, notification=notification + ) if resp.is_err(): return SyftError(message=resp.err()) diff --git a/packages/syft/src/syft/service/request/request_service.py b/packages/syft/src/syft/service/request/request_service.py index 3d523cc1069..30e209dcb27 100644 --- a/packages/syft/src/syft/service/request/request_service.py +++ b/packages/syft/src/syft/service/request/request_service.py @@ -19,10 +19,10 @@ from ..action.action_permissions import ActionPermission from ..code.user_code import UserCode from ..context import AuthedServiceContext +from ..notification.email_templates import RequestEmailTemplate from ..notification.notification_service import CreateNotification from ..notification.notification_service import NotificationService from ..notification.notifications import Notification -from ..notification.email_templates import RequestEmailTemplate from ..notifier.notifier_enums import NOTIFIERS from ..response import SyftError from ..response import SyftSuccess diff --git a/packages/syft/src/syft/service/user/user_service.py b/packages/syft/src/syft/service/user/user_service.py index 93119facd93..778a29f8890 100644 --- a/packages/syft/src/syft/service/user/user_service.py +++ b/packages/syft/src/syft/service/user/user_service.py @@ -5,8 +5,6 @@ from typing import Union # third party -from result import Err -from result import Ok # relative from ...abstract_node import NodeType @@ -16,6 +14,7 @@ from ...node.credentials import UserLoginCredentials from ...serde.serializable import serializable from ...store.document_store import DocumentStore +from ...store.linked_obj import LinkedObject from ...types.syft_metaclass import Empty from ...types.uid import UID from ...util.telemetry import instrument @@ -24,6 +23,10 @@ from ..context import AuthedServiceContext from ..context import NodeServiceContext from ..context import UnauthedServiceContext +from ..notification.email_templates import OnBoardEmailTemplate +from ..notification.notification_service import CreateNotification +from ..notification.notification_service import NotificationService +from ..notifier.notifier_enums import NOTIFIERS from ..response import SyftError from ..response import SyftSuccess from ..service import AbstractService @@ -33,7 +36,6 @@ from ..settings.settings_stash import SettingsStash from .user import User from .user import UserCreate -from ...store.linked_obj import LinkedObject from .user import UserPrivateKey from .user import UserSearch from .user import UserUpdate @@ -46,11 +48,7 @@ from .user_roles import ServiceRole from .user_roles import ServiceRoleCapability from .user_stash import UserStash -from ..notification.notification_service import CreateNotification -from ..notification.notification_service import NotificationService -from ..notification.email_templates import SuspiciousActivityEmailTemplate -from ..notification.notifications import Notification -from ..notifier.notifier_enums import NOTIFIERS + @instrument @serializable() @@ -434,33 +432,6 @@ def register( ) if not can_user_register: - root_key = self.admin_verify_key() - root_context = AuthedServiceContext( - node=context.node, credentials=root_key - ) - link = None - if new_user.created_by: - user = self.stash.get_by_signing_key( - credentials=root_context.credentials, - signing_key=new_user.created_by - ).ok() - link = LinkedObject.with_context( - user, - context=root_context - ) - message = CreateNotification( - subject="Not allowed register attempt", - from_user_verify_key=root_key, - to_user_verify_key=root_key, - linked_obj=link, - notifier_types=[NOTIFIERS.EMAIL], - email_template=SuspiciousActivityEmailTemplate, - ) - - method = context.node.get_service_method(NotificationService.send) - result = method(context=root_context, notification=message) - if not isinstance(result, Notification): - return Err(result) return SyftError( message=f"You don't have permission to create an account " f"on the domain: {context.node.name}. Please contact the Domain Owner." @@ -489,6 +460,25 @@ def register( user = result.ok() success_message = f"User '{user.name}' successfully registered!" + + # Notification Step + root_key = self.admin_verify_key() + root_context = AuthedServiceContext(node=context.node, credentials=root_key) + link = None + if new_user.created_by: + link = LinkedObject.with_context(user, context=root_context) + message = CreateNotification( + subject="Not allowed register attempt", + from_user_verify_key=root_key, + to_user_verify_key=user.verify_key, + linked_obj=link, + notifier_types=[NOTIFIERS.EMAIL], + email_template=OnBoardEmailTemplate, + ) + + method = context.node.get_service_method(NotificationService.send) + result = method(context=root_context, notification=message) + if request_user_role in DATA_OWNER_ROLE_LEVEL: success_message += " To see users, run `[your_client].users`" From a5eefb1fe03e152a963d67e789cbc414ea05c2cc Mon Sep 17 00:00:00 2001 From: Ionesio Junior Date: Thu, 22 Feb 2024 16:52:54 -0300 Subject: [PATCH 3/4] Fix lint --- packages/syft/src/syft/service/user/user_service.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/syft/src/syft/service/user/user_service.py b/packages/syft/src/syft/service/user/user_service.py index 778a29f8890..89a12f83861 100644 --- a/packages/syft/src/syft/service/user/user_service.py +++ b/packages/syft/src/syft/service/user/user_service.py @@ -4,8 +4,6 @@ from typing import Tuple from typing import Union -# third party - # relative from ...abstract_node import NodeType from ...exceptions.user import UserAlreadyExistsException From dd57951a03be1560927d99dca1b8dcdd21919c80 Mon Sep 17 00:00:00 2001 From: Ionesio Junior Date: Fri, 23 Feb 2024 11:59:47 -0300 Subject: [PATCH 4/4] Fix linting --- .../service/notification/email_templates.py | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/syft/src/syft/service/notification/email_templates.py b/packages/syft/src/syft/service/notification/email_templates.py index ab601494c03..0816ffdc799 100644 --- a/packages/syft/src/syft/service/notification/email_templates.py +++ b/packages/syft/src/syft/service/notification/email_templates.py @@ -1,6 +1,13 @@ +# stdlib +from typing import TYPE_CHECKING + # relative from ..context import AuthedServiceContext +if TYPE_CHECKING: + # relative + from .notifications import Notification + class EmailTemplate: pass @@ -61,7 +68,8 @@ def email_body(notification: "Notification", context: AuthedServiceContext) -> s

Welcome to {context.node.name} node!

Hello,

-

We're thrilled to have you on board and excited to help you get started with our powerful features:

+

We're thrilled to have you on board and + excited to help you get started with our powerful features:

Remote Data Science

@@ -75,7 +83,8 @@ def email_body(notification: "Notification", context: AuthedServiceContext) -> s -

Explore these features and much more within your account. If you have any questions or need assistance, don't hesitate to reach out.

+

Explore these features and much more within your account. + If you have any questions or need assistance, don't hesitate to reach out.

Cheers,

{admin_name}

@@ -178,21 +187,27 @@ def email_body(notification: "Notification", context: AuthedServiceContext) -> s

Hello,

-

A new request has been submitted and requires your attention. Please review the details below:

+

A new request has been submitted and requires your attention. + Please review the details below:

Request Details

ID: {request_obj.id}

-

Submitted By: {request_obj.requesting_user_name} {request_obj.requesting_user_email or ""}

+

+ Submitted By: + {request_obj.requesting_user_name} {request_obj.requesting_user_email or ""} +

Date: {request_obj.request_time}

-

Changes: {",".join([change.__class__.__name__ for change in request_obj.changes])}

+

+ Changes: + {",".join([change.__class__.__name__ for change in request_obj.changes])} +

-

To review and respond to this request, please click the button below:

- Review Request -

If you did not expect this request or have concerns about it, please contact our support team immediately.

+

If you did not expect this request or have concerns about it, + please contact our support team immediately.