mozreview: Override login page to use Bugzilla auth delegation (bug 993233). r=smacleod draft
authorMark Cote <mcote@mozilla.com>
Tue, 25 Aug 2015 19:01:55 -0400
changeset 5156 0fcc2b0cf145ce54fbc56996d7fde1004025c36f
parent 5143 3a0c2899bae3ef856423153a4e7cbd0b9bcb02db
child 5157 7d7971ad9957dd3cab5326ab28bf702f0ec7ee39
push id160
push usermcote@mozilla.com
push dateTue, 25 Aug 2015 23:02:54 +0000
reviewerssmacleod
bugs993233
mozreview: Override login page to use Bugzilla auth delegation (bug 993233). r=smacleod Passing a "full" query-string parameter with any value, e.g. "full=true", in the login URL results in the full login page being displayed. This exists primarily to allow the admin user to log in, which does not have a corresponding Bugzilla account. Also override the "Log Out" link to not redirect to the login page, since if you still have an open Bugzilla session, you'll be automatically logged back into Review Board. Selenium tests have been updated, but not all appear to be passing on tip right now, so there are still failures.
pylib/mozreview/mozreview/extension.py
pylib/mozreview/mozreview/static/mozreview/css/common.less
pylib/mozreview/mozreview/static/mozreview/js/logout.js
pylib/mozreview/mozreview/templates/mozreview/after-login-form.html
pylib/mozreview/mozreview/templates/mozreview/base-extrahead-login-form.html
pylib/mozreview/mozreview/templates/mozreview/before-login-form.html
pylib/mozreview/mozreview/templatetags/bugzilla.py
pylib/mozreview/mozreview/tests/test_login.py
pylib/mozreview/mozreview/urls.py
testing/vcttesting/unittest.py
--- a/pylib/mozreview/mozreview/extension.py
+++ b/pylib/mozreview/mozreview/extension.py
@@ -64,25 +64,31 @@ class MozReviewExtension(Extension):
         'ldap_url': '',
         'ldap_user': '',
         'ldap_password': '',
     }
 
     is_configurable = True
 
     css_bundles = {
+        'default': {
+            'source_filenames': ['mozreview/css/common.less'],
+        },
         'review': {
             'source_filenames': ['mozreview/css/review.less',
                                  'mozreview/css/commits.less'],
         },
         'viewdiff': {
             'source_filenames': ['mozreview/css/viewdiff.less'],
         },
     }
     js_bundles = {
+        'default': {
+            'source_filenames': ['mozreview/js/logout.js'],
+        },
         'reviews': {
             # TODO: Everything will break if init_rr.js is not first in this
             # list.
             'source_filenames': ['mozreview/js/init_rr.js',
                                  'mozreview/js/commits.js',
                                  'mozreview/js/review.js',
                                  'mozreview/js/try.js',
                                  'mozreview/js/ui.mozreviewautocomplete.js',]
@@ -151,16 +157,23 @@ class MozReviewExtension(Extension):
         # bundle.
         TemplateHook(self, 'base-css', 'mozreview/review-stylings-css.html',
                      apply_to=review_request_url_names)
         TemplateHook(self, 'base-css', 'mozreview/viewdiff-stylings-css.html',
                      apply_to=diffviewer_url_names)
         TemplateHook(self, 'base-scripts-post',
                      'mozreview/review-scripts-js.html',
                      apply_to=review_request_url_names)
+        TemplateHook(self, 'base-extrahead',
+                     'mozreview/base-extrahead-login-form.html',
+                     apply_to=['login'])
+        TemplateHook(self, 'before-login-form',
+                     'mozreview/before-login-form.html', apply_to=['login'])
+        TemplateHook(self, 'after-login-form',
+                     'mozreview/after-login-form.html', apply_to=['login'])
 
         ReviewRequestFieldsHook(self, 'main', [CommitsListField])
         # This forces the Commits field to be the top item.
         main_fieldset.field_classes.insert(0,
                                            main_fieldset.field_classes.pop())
 
         # The above hack forced Commits at the top, but the rest of these
         # fields are fine below the Description.
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/static/mozreview/css/common.less
@@ -0,0 +1,3 @@
+#accountnav li {
+  visibility: hidden;
+}
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/static/mozreview/js/logout.js
@@ -0,0 +1,4 @@
+$(document).ready(function() {
+  $("a[href='/account/logout/']").attr("href", "/mozreview/logout/");
+  $("#accountnav li").css("visibility", "visible");
+});
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/templates/mozreview/after-login-form.html
@@ -0,0 +1,17 @@
+{% load bugzilla %}
+{% get_full as full %}
+
+{% if user_authenticated or not full %}
+  end of standard login form -->
+
+  <div class="auth-header">
+    {% if request.user.is_authenticated %}
+      <p>You must <a href="{% url 'logout' %}">log out</a> before
+         logging back in.</p>
+    {% else %}
+      <p>You are being redirected to
+        <a href="{% bugzilla_auth_uri %}">Bugzilla</a> to log in...
+      </p>
+    {% endif %}
+  </div>
+{% endif %}
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/templates/mozreview/base-extrahead-login-form.html
@@ -0,0 +1,6 @@
+{% load bugzilla %}
+{% get_full as full %}
+
+{% if not full and not request.user.is_authenticated %}
+  <meta http-equiv="refresh" content="0;URL={% bugzilla_auth_uri %}">
+{% endif %}
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/templates/mozreview/before-login-form.html
@@ -0,0 +1,6 @@
+{% load bugzilla %}
+{% get_full as full %}
+
+{% if request.user.is_authenticated or not full %}
+  <!-- disable login form
+{% endif %}
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/templatetags/bugzilla.py
@@ -0,0 +1,41 @@
+import posixpath
+
+from urllib import urlencode
+from urlparse import urljoin, urlparse, urlunparse
+
+from django import template
+from django.core.urlresolvers import reverse
+
+from djblets.siteconfig.models import SiteConfiguration
+
+
+register = template.Library()
+
+
+@register.simple_tag(takes_context=True)
+def bugzilla_auth_uri(context):
+    # TODO: We only store the XML-RPC URL in our settings, but we need
+    # the auth URL.  Ideally we'd store just the root Bugzilla URL
+    # and modify it where appropriate, but we'll be switching to REST at
+    # some point so we might as well fix it then.
+    request = context['request']
+    redirect = request.GET.get('next')
+    callback_uri = request.build_absolute_uri(reverse('bmo-auth-callback'))
+
+    if redirect:
+        callback_uri += '?%s' % urlencode({'redirect': redirect})
+
+    siteconfig = SiteConfiguration.objects.get_current()
+    xmlrpc_url = siteconfig.get('auth_bz_xmlrpc_url')
+    u = urlparse(xmlrpc_url)
+    bugzilla_root = posixpath.dirname(u.path).rstrip('/') + '/'
+    query_dict = {'description': 'mozreview', 'callback': callback_uri}
+
+    return urlunparse((u.scheme, u.netloc, urljoin(bugzilla_root, 'auth.cgi'),
+                       '', urlencode(query_dict), ''))
+
+
+@register.assignment_tag(takes_context=True)
+def get_full(context):
+    request = context['request']
+    return bool(request.GET.get('full'))
--- a/pylib/mozreview/mozreview/tests/test_login.py
+++ b/pylib/mozreview/mozreview/tests/test_login.py
@@ -3,29 +3,30 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, unicode_literals
 
 from vcttesting.unittest import MozReviewWebDriverTest
 
 
 class LoginTest(MozReviewWebDriverTest):
-    def assertInvalidLogin(self):
-        self.verify_rburl('account/login/')
-        el = self.browser.find_elements_by_class_name('errorlist')
-        self.assertEqual(len(el), 1)
-        li = el[0].find_elements_by_tag_name('li')
-        self.assertEqual(len(li), 1)
-        self.assertIn('Please enter a correct username and password',
-            li[0].text)
-
+    def assertInvalidLogin(self, verify_error_msg=True):
+        self.verify_bzurl('auth.cgi')
+        if verify_error_msg:
+            el = self.browser.find_element_by_id('error_msg')
+            self.assertIn('The username or password you entered is not valid',
+                          el.text)
 
     def test_invalid_login(self):
         self.reviewboard_login('nobody', 'invalid', verify=False)
-        self.assertInvalidLogin()
+        # The Bugzilla login form uses type="email" in the login field.
+        # Since this means we can't submit the form with the username
+        # 'nobody' (not a valid email address), we won't get the standard
+        # invalid-login error message.
+        self.assertInvalidLogin(verify_error_msg=False)
 
     def test_username_login_disallowed(self):
         self.bugzilla().create_user('baduser@example.com', 'password1',
                                     'Some User [:user1]')
 
         self.reviewboard_login('user1', 'password1', verify=False)
         self.assertInvalidLogin()
 
--- a/pylib/mozreview/mozreview/urls.py
+++ b/pylib/mozreview/mozreview/urls.py
@@ -1,8 +1,12 @@
 from __future__ import unicode_literals
 
 from django.conf.urls import patterns, url
+from django.contrib.auth import views as auth_views
 
 
-urlpatterns = patterns('mozreview.views',
+urlpatterns = patterns(
+    'mozreview.views',
+
     url(r'^bmo_auth_callback/$', 'bmo_auth_callback', name='bmo-auth-callback'),
+    url(r'^logout/$', auth_views.logout, {'next_page': '/r/'}),
 )
--- a/testing/vcttesting/unittest.py
+++ b/testing/vcttesting/unittest.py
@@ -3,17 +3,19 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import os
 import shutil
 import subprocess
 import tempfile
+import time
 import unittest
+from urllib import urlencode
 
 from selenium import webdriver
 import selenium.webdriver.support.expected_conditions as EC
 from selenium.webdriver.common.by import By
 from selenium.webdriver.common.keys import Keys
 from selenium.webdriver.remote.switch_to import SwitchTo
 from selenium.webdriver.support.wait import WebDriverWait
 
@@ -140,28 +142,55 @@ class MozReviewWebDriverTest(MozReviewTe
     @property
     def switch_to(self):
         return SwitchTo(self.browser)
 
     def load_rburl(self, path):
         """Load the specified Review Board URL."""
         self.browser.get('%s%s' % (self.rburl, path))
 
+    def load_bzurl(self, path):
+        """Load the specified Bugzilla URL."""
+        self.browser.get('%s%s' % (self.bzurl, path))
+
     def verify_rburl(self, path):
         """Verify the current URL is the specified Review Board URL."""
         current = self.browser.current_url
         self.assertEqual(current, '%s%s' % (self.rburl, path))
 
+    def verify_bzurl(self, path):
+        """Verify the current URL is the specified Bugzilla URL."""
+        current = self.browser.current_url
+        self.assertEqual(current, '%s%s' % (self.bzurl, path))
+
     def reviewboard_login(self, username, password, verify=True):
         """Log into Review Board with the specified credentials."""
-        self.load_rburl('account/login')
+        # Ensure that we're logged out of both Review Board and Bugzilla;
+        # otherwise we will be automatically logged back in.
+        self.load_rburl('mozreview/logout/')
+        self.load_bzurl('index.cgi?logout=1')
+        self.load_rburl('account/login/')
 
-        input_username = self.browser.find_element_by_id('id_username')
+        # Wait for redirect to Bugzilla login.
+        bz_auth_url_path = 'auth.cgi?%s' % urlencode({
+            'callback': '%smozreview/bmo_auth_callback/' % self.rburl,
+            'description': 'mozreview'
+        })
+        bz_auth_url = '%s%s' % (self.bzurl, bz_auth_url_path)
+
+        for _ in xrange(0, 5):
+            if self.browser.current_url == bz_auth_url:
+                break
+            time.sleep(1)
+
+        self.verify_bzurl(bz_auth_url_path)
+
+        input_username = self.browser.find_element_by_id('Bugzilla_login')
         input_username.send_keys(username)
-        input_password = self.browser.find_element_by_id('id_password')
+        input_password = self.browser.find_element_by_id('Bugzilla_password')
         input_password.send_keys(password)
 
         input_password.submit()
 
         if verify:
             WebDriverWait(self.browser, 10).until(
                 EC.title_is(u'My Dashboard | Review Board'))