Browse Source

feat(plugins): Add failure handling for invalid identities when fetching commits

David Cramer 7 years ago
parent
commit
3723e7cf78

+ 3 - 1
src/sentry/exceptions.py

@@ -48,7 +48,9 @@ class PluginIdentityRequired(PluginError):
 
 
 class InvalidIdentity(Exception):
-    pass
+    def __init__(self, message='', identity=None):
+        super(InvalidIdentity, self).__init__(message)
+        self.identity = identity
 
 
 class HookValidationError(Exception):

+ 33 - 1
src/sentry/tasks/commits.py

@@ -2,14 +2,44 @@ from __future__ import absolute_import
 
 import logging
 
+from django.core.urlresolvers import reverse
+
 from sentry.exceptions import InvalidIdentity, PluginError
 from sentry.models import Deploy, Release, ReleaseHeadCommit, Repository, User
 from sentry.plugins import bindings
 from sentry.tasks.base import instrumented_task, retry
+from sentry.utils.email import MessageBuilder
+from sentry.utils.http import absolute_uri
 
 logger = logging.getLogger(__name__)
 
 
+def generate_invalid_identity_email(identity, commit_failure=False):
+    new_context = {
+        'identity': identity,
+        'auth_url': absolute_uri(reverse('socialauth_associate', args=[identity.provider])),
+        'commit_failure': commit_failure,
+    }
+
+    return MessageBuilder(
+        subject='Action Required',
+        context=new_context,
+        template='sentry/emails/identity-invalid.txt',
+        html_template='sentry/emails/identity-invalid.html',
+    )
+
+# we're future proofing this function a bit so it could be used with other code
+
+
+def handle_invalid_identity(identity, commit_failure=False):
+    # email the user
+    msg = generate_invalid_identity_email(identity, commit_failure)
+    msg.send_async(to=[identity.user.email])
+
+    # now remove the identity, as its invalid
+    identity.delete()
+
+
 @instrumented_task(
     name='sentry.tasks.commits.fetch_commits',
     queue='commits',
@@ -77,7 +107,7 @@ def fetch_commits(release_id, user_id, refs, prev_release_id=None, **kwargs):
             repo_commits = provider.compare_commits(repo, start_sha, end_sha, actor=user)
         except NotImplementedError:
             pass
-        except (PluginError, InvalidIdentity):
+        except (PluginError, InvalidIdentity) as exc:
             logger.exception(
                 'fetch_commits.error',
                 exc_info=True,
@@ -89,6 +119,8 @@ def fetch_commits(release_id, user_id, refs, prev_release_id=None, **kwargs):
                     'start_sha': start_sha,
                 }
             )
+            if isinstance(exc, InvalidIdentity) and getattr(exc, 'identity', None):
+                handle_invalid_identity(identity=exc.identity, commit_failure=True)
         else:
             logger.info(
                 'fetch_commits.complete',

+ 1 - 0
src/sentry/templates/sentry/debug/mail/preview.html

@@ -25,6 +25,7 @@
       <optgroup label="Account">
         <option value="mail/confirm-email/">Confirm Email</option>
         <option value="mail/recover-account/">Reset Password</option>
+        <option value="mail/invalid-identity/">Invalid Identity</option>
       </optgroup>
       <optgroup label="Membership">
         <option value="mail/request-access/">Access Requested</option>

+ 14 - 0
src/sentry/templates/sentry/emails/identity-invalid.html

@@ -0,0 +1,14 @@
+{% extends "sentry/emails/base.html" %}
+
+{% block main %}
+  <h3>Action Required</h3>
+  <p>An identity with a third party service provider ({{ identity.provider }}) failed to authenticate and has been removed.</p>
+  {% if commit_failure %} }
+    <p>You will need to <a href="{{ auth_url }}">associate your account with {{ identity.provider }}</a> to continue automatically fetching commits for your releases.</p>
+  {% else %}
+    <p>You will need to <a href="{{ auth_url }}">associate your account with {{ identity.provider }}</a> to continue using the integration.</p>
+  {% endif %}
+  <p><a href="{{ auth_url }}" class="btn">Associate Identity</a></p>
+{% endblock %}
+
+{% block footer %}{% endblock %}

+ 8 - 0
src/sentry/templates/sentry/emails/identity-invalid.txt

@@ -0,0 +1,8 @@
+Action Required
+---------------
+
+An identity with a third party service provider ({{ identity.provider }}) failed to authenticate and has been removed.
+
+You will need to associate your account with {{ identity.provider }} to continue using the integration.
+
+{{ auth_url }}

+ 2 - 0
src/sentry/web/debug_urls.py

@@ -10,6 +10,7 @@ from sentry.web.frontend.debug.debug_assigned_email import (
 )
 from sentry.web.frontend.debug.debug_trigger_error import (DebugTriggerErrorView)
 from sentry.web.frontend.debug.debug_error_embed import (DebugErrorPageEmbedView)
+from sentry.web.frontend.debug.debug_invalid_identity_email import DebugInvalidIdentityEmailView
 from sentry.web.frontend.debug.debug_mfa_added_email import (DebugMfaAddedEmailView)
 from sentry.web.frontend.debug.debug_mfa_removed_email import (DebugMfaRemovedEmailView)
 from sentry.web.frontend.debug.debug_new_release_email import (DebugNewReleaseEmailView)
@@ -53,6 +54,7 @@ urlpatterns = patterns(
     url(r'^debug/mail/request-access/$', sentry.web.frontend.debug.mail.request_access),
     url(r'^debug/mail/access-approved/$', sentry.web.frontend.debug.mail.access_approved),
     url(r'^debug/mail/invitation/$', sentry.web.frontend.debug.mail.invitation),
+    url(r'^debug/mail/invalid-identity/$', DebugInvalidIdentityEmailView.as_view()),
     url(r'^debug/mail/confirm-email/$', sentry.web.frontend.debug.mail.confirm_email),
     url(r'^debug/mail/recover-account/$', sentry.web.frontend.debug.mail.recover_account),
     url(r'^debug/mail/unassigned/$', DebugUnassignedEmailView.as_view()),

+ 22 - 0
src/sentry/web/frontend/debug/debug_invalid_identity_email.py

@@ -0,0 +1,22 @@
+from __future__ import absolute_import, print_function
+
+from django.views.generic import View
+from social_auth.models import UserSocialAuth
+
+from sentry.tasks.commits import generate_invalid_identity_email
+
+from .mail import MailPreview
+
+
+class DebugInvalidIdentityEmailView(View):
+    def get(self, request):
+        identity = UserSocialAuth(user=request.user, provider='dummy')
+
+        email = generate_invalid_identity_email(
+            identity=identity,
+        )
+        return MailPreview(
+            html_template=email.html_template,
+            text_template=email.template,
+            context=email.context,
+        ).render(request)

+ 20 - 0
tests/acceptance/test_emails.py

@@ -140,6 +140,26 @@ class EmailTestCase(AcceptanceTestCase):
         self.browser.wait_until('#preview')
         self.browser.snapshot('digest email txt')
 
+    def test_invalid_identity_text(self):
+        self.browser.get(self.build_url('/debug/mail/invalid-identity/', 'txt'))
+        self.browser.wait_until('#preview')
+        self.browser.snapshot('invalid identity email txt')
+
+    def test_invalid_identity_html(self):
+        self.browser.get(self.build_url('/debug/mail/invalid-identity/', 'html'))
+        self.browser.wait_until('#preview')
+        self.browser.snapshot('invalid identity email html')
+
+    def test_invitation_text(self):
+        self.browser.get(self.build_url('/debug/mail/invitation/', 'txt'))
+        self.browser.wait_until('#preview')
+        self.browser.snapshot('invitation email txt')
+
+    def test_invitation_html(self):
+        self.browser.get(self.build_url('/debug/mail/invitation/', 'html'))
+        self.browser.wait_until('#preview')
+        self.browser.snapshot('invitation email html')
+
     def test_report_html(self):
         self.browser.get(self.build_url('/debug/mail/report/'))
         self.browser.wait_until('#preview')

+ 80 - 2
tests/sentry/tasks/test_commits.py

@@ -1,13 +1,16 @@
 from __future__ import absolute_import
 
+from django.core import mail
 from mock import patch
+from social_auth.models import UserSocialAuth
 
+from sentry.exceptions import InvalidIdentity
 from sentry.models import Commit, Deploy, Release, ReleaseHeadCommit, Repository
-from sentry.tasks.commits import fetch_commits
+from sentry.tasks.commits import fetch_commits, handle_invalid_identity
 from sentry.testutils import TestCase
 
 
-class FetchCommits(TestCase):
+class FetchCommitsTest(TestCase):
     def test_simple(self):
         self.login_as(user=self.user)
         org = self.create_organization(owner=self.user, name='baz')
@@ -79,3 +82,78 @@ class FetchCommits(TestCase):
         assert commit_list[2].key == 'b' * 40
 
         mock_notify_if_ready.assert_called_with(deploy.id, fetch_complete=True)
+
+    @patch('sentry.tasks.commits.handle_invalid_identity')
+    @patch('sentry.plugins.providers.dummy.repository.DummyRepositoryProvider.compare_commits')
+    def test_invalid_identity(self, mock_compare_commits, mock_handle_invalid_identity):
+        self.login_as(user=self.user)
+        org = self.create_organization(owner=self.user, name='baz')
+
+        repo = Repository.objects.create(
+            name='example',
+            provider='dummy',
+            organization_id=org.id,
+        )
+        release = Release.objects.create(
+            organization_id=org.id,
+            version='abcabcabc',
+        )
+
+        commit = Commit.objects.create(
+            organization_id=org.id,
+            repository_id=repo.id,
+            key='a' * 40,
+        )
+
+        ReleaseHeadCommit.objects.create(
+            organization_id=org.id,
+            repository_id=repo.id,
+            release=release,
+            commit=commit,
+        )
+
+        refs = [{
+            'repository': repo.name,
+            'commit': 'b' * 40,
+        }]
+
+        release2 = Release.objects.create(
+            organization_id=org.id,
+            version='12345678',
+        )
+
+        usa = UserSocialAuth.objects.create(
+            user=self.user,
+            provider='dummy',
+        )
+
+        mock_compare_commits.side_effect = InvalidIdentity(identity=usa)
+
+        fetch_commits(
+            release_id=release2.id,
+            user_id=self.user.id,
+            refs=refs,
+            previous_release_id=release.id,
+        )
+
+        mock_handle_invalid_identity.assert_called_once_with(
+            identity=usa,
+            commit_failure=True,
+        )
+
+
+class HandleInvalidIdentityTest(TestCase):
+    def test_simple(self):
+        usa = UserSocialAuth.objects.create(
+            user=self.user,
+            provider='dummy',
+        )
+
+        with self.tasks():
+            handle_invalid_identity(usa)
+
+        assert not UserSocialAuth.objects.filter(id=usa.id).exists()
+
+        msg = mail.outbox[-1]
+        assert msg.subject == 'Action Required'
+        assert msg.to == [self.user.email]