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.
--- 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'))