diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py
index 0355730256..b1a03711ce 100644
--- a/common/djangoapps/external_auth/tests/test_shib.py
+++ b/common/djangoapps/external_auth/tests/test_shib.py
@@ -1,3 +1,4 @@
+
"""
Tests for Shibboleth Authentication
@jbau
@@ -32,11 +33,12 @@ TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT,
# b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing
# For the sake of python convention we'll make all of these variable names ALL_CAPS
-IDP = 'https://idp.stanford.edu/'
-REMOTE_USER = 'test_user@stanford.edu'
-MAILS = [None, '', 'test_user@stanford.edu']
-GIVENNAMES = [None, '', 'Jason', 'jas\xc3\xb6n; John; bob'] # At Stanford, the givenNames can be a list delimited by ';'
-SNS = [None, '', 'Bau', '\xe5\x8c\x85; smith'] # At Stanford, the sns can be a list delimited by ';'
+IDP = u'https://idp.stanford.edu/'
+REMOTE_USER = u'test_user@stanford.edu'
+MAILS = [None, u'', u'test_user@stanford.edu']
+DISPLAYNAMES = [None, u'', u'Jason \u5305']
+GIVENNAMES = [None, u'', u'jas\xf6n; John; bob'] # At Stanford, the givenNames can be a list delimited by ';'
+SNS = [None, u'', u'\u5305; smith'] # At Stanford, the sns can be a list delimited by ';'
def gen_all_identities():
@@ -46,10 +48,12 @@ def gen_all_identities():
could potentially pass to django via request.META, i.e.
setting (or not) request.META['givenName'], etc.
"""
- def _build_identity_dict(mail, given_name, surname):
+ def _build_identity_dict(mail, display_name, given_name, surname):
""" Helper function to return a dict of test identity """
meta_dict = {'Shib-Identity-Provider': IDP,
'REMOTE_USER': REMOTE_USER}
+ if display_name is not None:
+ meta_dict['displayName'] = display_name
if mail is not None:
meta_dict['mail'] = mail
if given_name is not None:
@@ -61,7 +65,8 @@ def gen_all_identities():
for mail in MAILS:
for given_name in GIVENNAMES:
for surname in SNS:
- yield _build_identity_dict(mail, given_name, surname)
+ for display_name in DISPLAYNAMES:
+ yield _build_identity_dict(mail, display_name, given_name, surname)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
@@ -75,7 +80,7 @@ class ShibSPTest(ModuleStoreTestCase):
def setUp(self):
self.store = editable_modulestore()
- @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True)
+ @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
def test_exception_shib_login(self):
"""
Tests that we get the error page when there is no REMOTE_USER
@@ -101,7 +106,7 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertIn(u'logged in via Shibboleth', args[0])
self.assertEquals(remote_user, args[1])
- @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True)
+ @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
def test_shib_login(self):
"""
Tests that:
@@ -195,11 +200,13 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertEquals(len(audit_log_calls), 0)
else:
self.assertEqual(response.status_code, 200)
- self.assertContains(response, "
Register for")
+ self.assertContains(response,
+ ("Preferences for {platform_name}"
+ .format(platform_name=settings.PLATFORM_NAME)))
# no audit logging calls
self.assertEquals(len(audit_log_calls), 0)
- @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True)
+ @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
def test_registration_form(self):
"""
Tests the registration form showing up with the proper parameters.
@@ -219,8 +226,9 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertNotContains(response, mail_input_HTML)
sn_empty = not identity.get('sn')
given_name_empty = not identity.get('givenName')
+ displayname_empty = not identity.get('displayName')
fullname_input_HTML = 'Log into your {platform_name} Account"
+ .format(platform_name=settings.PLATFORM_NAME)))
+ self.assertEqual(noshib_response.status_code, 200)
+
+ TARGET_URL_SHIB = reverse('courseware', args=[self.shib_course.id])
+ shib_response = self.client.get(**{'path': TARGET_URL_SHIB,
+ 'follow': True,
+ 'REMOTE_USER':self.extauth.external_id,
+ 'Shib-Identity-Provider':'https://idp.stanford.edu/'})
+ # Test that the shib-login redirect page with ?next= and the desired page are part of the redirect chain
+ # The 'courseware' page actually causes a redirect itself, so it's not the end of the chain and we
+ # won't test its contents
+ self.assertEqual(shib_response.redirect_chain[-3],
+ ('http://testserver/shib-login/?next={url}'.format(url=TARGET_URL_SHIB), 302))
+ self.assertEqual(shib_response.redirect_chain[-2],
+ ('http://testserver{url}'.format(url=TARGET_URL_SHIB), 302))
+ self.assertEqual(shib_response.status_code, 200)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 6e19c9ad5f..c351518335 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -24,7 +24,8 @@ from django.core.urlresolvers import reverse
from django.core.validators import validate_email, validate_slug, ValidationError
from django.core.exceptions import ObjectDoesNotExist
from django.db import IntegrityError, transaction
-from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, Http404
+from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbidden,
+ HttpResponseNotAllowed, Http404, HttpResponseRedirect)
from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie
from django.utils.http import cookie_date
@@ -57,6 +58,7 @@ from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access
from external_auth.models import ExternalAuthMap
+import external_auth.views
from bulk_email.models import Optout
@@ -70,6 +72,8 @@ AUDIT_LOG = logging.getLogger("audit")
Article = namedtuple('Article', 'title url author image deck publication publish_date')
+SHIB_DOMAIN_PREFIX = 'shib'
+
def csrf_token(context):
"""A csrf token that can be included in a form."""
@@ -95,7 +99,7 @@ def index(request, extra_context={}, user=None):
# The course selection work is done in courseware.courses.
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
# do explicit check, because domain=None is valid
- if domain == False:
+ if domain is False:
domain = request.META.get('HTTP_HOST')
courses = get_courses(None, domain=domain)
@@ -255,6 +259,8 @@ def register_user(request, extra_context=None):
if extra_context is not None:
context.update(extra_context)
+ if context.get("extauth_domain", '').startswith(SHIB_DOMAIN_PREFIX):
+ return render_to_response('register-shib.html', context)
return render_to_response('register.html', context)
@@ -407,11 +413,35 @@ def change_enrollment(request):
else:
return HttpResponseBadRequest(_("Enrollment action is invalid"))
+
+def parse_course_id_from_string(input_str):
+ m_obj = re.match(r'^/courses/(?P[^/]+/[^/]+/[^/]+)', input_str)
+ if m_obj:
+ return m_obj.group('course_id')
+ return None
+
+
+def get_course_enrollment_domain(course_id):
+ try:
+ course = course_from_id(course_id)
+ return course.enrollment_domain
+ except ItemNotFoundError:
+ return None
+
+
@ensure_csrf_cookie
-def accounts_login(request, error=""):
+def accounts_login(request):
if settings.MITX_FEATURES.get('AUTH_USE_CAS'):
return redirect(reverse('cas-login'))
- return render_to_response('login.html', {'error': error})
+ # see if the "next" parameter has been set, whether it has a course context, and if so, whether
+ # there is a course-specific place to redirect
+ next = request.GET.get('next')
+ if next:
+ course_id = parse_course_id_from_string(next)
+ if course_id and get_course_enrollment_domain(course_id):
+ return external_auth.views.course_specific_login(request, course_id)
+ return render_to_response('login.html')
+
# Need different levels of logging
@ensure_csrf_cookie
@@ -429,6 +459,17 @@ def login_user(request, error=""):
AUDIT_LOG.warning(u"Login failed - Unknown user email: {0}".format(email))
user = None
+ # check if the user has a linked shibboleth account, if so, redirect the user to shib-login
+ # This behavior is pretty much like what gmail does for shibboleth. Try entering some @stanford.edu
+ # address into the Gmail login.
+ if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and user:
+ try:
+ eamap = ExternalAuthMap.objects.get(user=user)
+ if eamap.external_domain.startswith(SHIB_DOMAIN_PREFIX):
+ return HttpResponse(json.dumps({'success': False, 'redirect': reverse('shib-login')}))
+ except (ExternalAuthMap.DoesNotExist, ExternalAuthMap.MultipleObjectsReturned):
+ pass
+
# if the user doesn't exist, we want to set the username to an invalid
# username so that authentication is guaranteed to fail and we can take
# advantage of the ratelimited backend
@@ -630,9 +671,9 @@ def create_account(request, post_override=None):
return HttpResponse(json.dumps(js))
# Can't have terms of service for certain SHIB users, like at Stanford
- tos_not_required = settings.MITX_FEATURES.get("AUTH_USE_SHIB") \
- and settings.MITX_FEATURES.get('SHIB_DISABLE_TOS') \
- and DoExternalAuth and ("shib" in eamap.external_domain)
+ tos_not_required = (settings.MITX_FEATURES.get("AUTH_USE_SHIB") and
+ settings.MITX_FEATURES.get('SHIB_DISABLE_TOS') and
+ DoExternalAuth and eamap.external_domain.startswith(SHIB_DOMAIN_PREFIX))
if not tos_not_required:
if post_vars.get('terms_of_service', 'false') != u'true':
diff --git a/lms/templates/login.html b/lms/templates/login.html
index b737255a0d..9a06fc4717 100644
--- a/lms/templates/login.html
+++ b/lms/templates/login.html
@@ -53,6 +53,9 @@
} else {
location.href="${reverse('dashboard')}";
}
+ } else if(json.hasOwnProperty('redirect')){
+ var u=decodeURI(window.location.search);
+ location.href=json.redirect+u;
} else {
toggleSubmitButton(true);
$('.message.submission-error').addClass('is-shown').focus();
diff --git a/lms/templates/register-shib.html b/lms/templates/register-shib.html
new file mode 100644
index 0000000000..ba6f9a27bb
--- /dev/null
+++ b/lms/templates/register-shib.html
@@ -0,0 +1,195 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+<%inherit file="main.html" />
+
+<%namespace name='static' file='static_content.html'/>
+<%namespace file='main.html' import="login_query"/>
+
+<%! from django.core.urlresolvers import reverse %>
+<%! from django.utils import html %>
+<%! from django_countries.countries import COUNTRIES %>
+<%! from student.models import UserProfile %>
+<%! from datetime import date %>
+<%! import calendar %>
+
+<%block name="title">${_("Preferences for {platform_name}").format(platform_name=settings.PLATFORM_NAME)}%block>
+
+<%block name="js_extra">
+
+%block>
+
+
+
+
${_("Welcome {username}! Please set your preferences below").format(username=extauth_id,
+ platform_name=settings.PLATFORM_NAME)}