Browse Source

feat(2fa): Send additional security emails (#8824)

Megan Heskett 6 years ago
parent
commit
f596267391

+ 14 - 1
src/sentry/api/endpoints/user_authenticator_details.py

@@ -77,6 +77,16 @@ class UserAuthenticatorDetailsEndpoint(UserEndpoint):
         if interface.interface_id == 'recovery':
             interface.regenerate_codes()
 
+            capture_security_activity(
+                account=user,
+                type='recovery-codes-regenerated',
+                actor=request.user,
+                ip_address=request.META['REMOTE_ADDR'],
+                context={
+                    'authenticator': authenticator,
+                },
+                send_email=True
+            )
         return Response(serialize(interface))
 
     @sudo_required
@@ -109,6 +119,8 @@ class UserAuthenticatorDetailsEndpoint(UserEndpoint):
                 return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
             interface.authenticator.save()
+            device_name = interface.get_device_name(interface_device_id)
+
             capture_security_activity(
                 account=user,
                 type='mfa-removed',
@@ -116,8 +128,9 @@ class UserAuthenticatorDetailsEndpoint(UserEndpoint):
                 ip_address=request.META['REMOTE_ADDR'],
                 context={
                     'authenticator': authenticator,
+                    'device_name': device_name
                 },
-                send_email=False
+                send_email=True
             )
             return Response(status=status.HTTP_204_NO_CONTENT)
 

+ 8 - 3
src/sentry/api/endpoints/user_authenticator_enroll.py

@@ -156,6 +156,7 @@ class UserAuthenticatorEnrollEndpoint(UserEndpoint):
         except KeyError:
             pass
 
+        context = {}
         # Need to update interface with phone number before validating OTP
         if 'phone' in request.DATA:
             interface.phone_number = serializer.data['phone']
@@ -181,20 +182,24 @@ class UserAuthenticatorEnrollEndpoint(UserEndpoint):
                 serializer.data['response'],
                 serializer.data['deviceName']
             )
+            context.update({
+                'device_name': serializer.data['deviceName']
+            })
 
         try:
             interface.enroll(request.user)
         except Authenticator.AlreadyEnrolled:
             return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST)
         else:
+            context.update({
+                'authenticator': interface.authenticator
+            })
             capture_security_activity(
                 account=request.user,
                 type='mfa-added',
                 actor=request.user,
                 ip_address=request.META['REMOTE_ADDR'],
-                context={
-                    'authenticator': interface.authenticator,
-                },
+                context=context,
                 send_email=True,
             )
             request.user.clear_lost_passwords()

+ 5 - 0
src/sentry/models/authenticator.py

@@ -536,6 +536,11 @@ class U2fInterface(AuthenticatorInterface):
             return True
         return False
 
+    def get_device_name(self, key):
+        for device in self.config.get('devices') or ():
+            if device['binding']['keyHandle'] == key:
+                return device['name']
+
     def get_registered_devices(self):
         rv = []
         for device in self.config.get('devices') or ():

+ 3 - 0
src/sentry/security/emails.py

@@ -21,6 +21,9 @@ def generate_security_email(account, type, actor, ip_address, context=None, curr
     elif type == 'password-changed':
         template = 'sentry/emails/password-changed.txt'
         html_template = 'sentry/emails/password-changed.html'
+    elif type == 'recovery-codes-regenerated':
+        template = 'sentry/emails/recovery-codes-regenerated.txt'
+        html_template = 'sentry/emails/recovery-codes-regenerated.html'
     else:
         raise ValueError('unknown type: {}'.format(type))
 

+ 7 - 1
src/sentry/templates/sentry/emails/mfa-added.html

@@ -5,8 +5,14 @@
 {% endblock %}
 
 {% block security_metadata %}
+  <tr>
+    <td></td>
+    <td style="padding-bottom: 0px"><strong>{{ authenticator.interface.name }}</strong></td>
+  </tr>
+  {% if device_name %}
     <tr>
       <td></td>
-      <td><strong>{{ authenticator.interface.name }}</strong></td>
+      <td style="padding-top: 0px"><strong>{{ device_name }}</strong></td>
     </tr>
+  {% endif %}
 {% endblock %}

+ 1 - 0
src/sentry/templates/sentry/emails/mfa-added.txt

@@ -6,4 +6,5 @@ An authenticator has been added to your Sentry account.
 
 {% block security_metadata %}
 Authenticator: {{ authenticator.interface.name }}
+{% if device_name %}Device: {{ device_name }}{% endif %}
 {% endblock %}

+ 7 - 1
src/sentry/templates/sentry/emails/mfa-removed.html

@@ -5,8 +5,14 @@
 {% endblock %}
 
 {% block security_metadata %}
+  <tr>
+    <td></td>
+    <td style="padding-bottom: 0px"><strong>{{ authenticator.interface.name }}</strong></td>
+  </tr>
+  {% if device_name %}
     <tr>
       <td></td>
-      <td><strong>{{ authenticator.interface.name }}</strong></td>
+      <td style="padding-top: 0px"><strong>{{ device_name }}</strong></td>
     </tr>
+  {% endif %}
 {% endblock %}

+ 1 - 0
src/sentry/templates/sentry/emails/mfa-removed.txt

@@ -6,4 +6,5 @@ An authenticator has been removed from your Sentry account.
 
 {% block security_metadata %}
 Authenticator: {{ authenticator.interface.name }}
+{% if device_name %}Device: {{ device_name }}{% endif %}
 {% endblock %}

+ 5 - 0
src/sentry/templates/sentry/emails/recovery-codes-regenerated.html

@@ -0,0 +1,5 @@
+{% extends "sentry/emails/security_base.html" %}
+
+{% block security_body %}
+  <p>Recovery codes have been regenerated for your Sentry account.</p>
+{% endblock %}

+ 5 - 0
src/sentry/templates/sentry/emails/recovery-codes-regenerated.txt

@@ -0,0 +1,5 @@
+{% extends "sentry/emails/security_base.txt" %}
+
+{% block security_body %}
+Recovery codes have been regenerated for your Sentry account.
+{% endblock %}

Some files were not shown because too many files changed in this diff