Commit 948c07c4 by Jason Bau

Revamped + Enhanced Shibboleth support

* If a shib users type in their email on the regular login page,
  redirects them to /shib-login/
* Modify student.views.accounts_login to handle redirects
  generated by @login_required for courses that use shib for
  access control.
  Redirect those logins to /shib-login/?next=
parent 499d272a
"""
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, "<title>Register for")
self.assertContains(response,
("<title>Preferences for {platform_name}</title>"
.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 = '<input id="name" type="text" name="name"'
if sn_empty and given_name_empty:
if sn_empty and given_name_empty and displayname_empty:
self.assertContains(response, fullname_input_HTML)
else:
self.assertNotContains(response, fullname_input_HTML)
......@@ -228,7 +236,7 @@ class ShibSPTest(ModuleStoreTestCase):
# clean up b/c we don't want existing ExternalAuthMap for the next run
client.session['ExternalAuthMap'].delete()
@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_formSubmit(self):
"""
Tests user creation after the registration form that pops is submitted. If there is no shib
......@@ -292,17 +300,25 @@ class ShibSPTest(ModuleStoreTestCase):
profile = UserProfile.objects.get(user=user)
sn_empty = not identity.get('sn')
given_name_empty = not identity.get('givenName')
displayname_empty = not identity.get('displayName')
if displayname_empty:
if sn_empty and given_name_empty:
self.assertEqual(profile.name, postvars['name'])
else:
self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name)
self.assertNotIn(u';', profile.name)
else:
self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name)
self.assertEqual(profile.name, identity.get('displayName'))
# clean up for next loop
request2.session['ExternalAuthMap'].delete()
UserProfile.objects.filter(user=user).delete()
Registration.objects.filter(user=user).delete()
user.delete()
@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_course_specificLoginAndReg(self):
"""
Tests that the correct course specific login and registration urls work for shib
......@@ -374,7 +390,7 @@ class ShibSPTest(ModuleStoreTestCase):
'?course_id=DNE/DNE/DNE' +
'&enrollment_action=enroll')
@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_enrollment_limit_by_domain(self):
"""
Tests that the enrollmentDomain setting is properly limiting enrollment to those who have
......@@ -438,10 +454,12 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertEqual(response.status_code, 400)
self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
@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_enrollment(self):
"""
A functionality test that a student with an existing shib login can auto-enroll in a class with GET params
A functionality test that a student with an existing shib login
can auto-enroll in a class with GET or POST params. Also tests the direction functionality of
the 'next' GET/POST param
"""
student = UserFactory.create()
extauth = ExternalAuthMap(external_id='testuser@stanford.edu',
......@@ -465,13 +483,25 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
self.client.logout()
request_kwargs = {'path': '/shib-login/',
'data': {'enrollment_action': 'enroll', 'course_id': course.id},
'data': {'enrollment_action': 'enroll', 'course_id': course.id, 'next': '/testredirect'},
'follow': False,
'REMOTE_USER': 'testuser@stanford.edu',
'Shib-Identity-Provider': 'https://idp.stanford.edu/'}
response = self.client.get(**request_kwargs)
# successful login is a redirect to "/"
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], 'http://testserver/')
self.assertEqual(response['location'], 'http://testserver/testredirect')
# now there is enrollment
self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
# Clean up and try again with POST (doesn't happen with real production shib, doing this for test coverage)
self.client.logout()
CourseEnrollment.unenroll(student, course.id)
self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
response = self.client.post(**request_kwargs)
# successful login is a redirect to "/"
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], 'http://testserver/testredirect')
# now there is enrollment
self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
......@@ -11,7 +11,7 @@ from external_auth.models import ExternalAuthMap
from external_auth.djangostore import DjangoOpenIDStore
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login, logout
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.core.validators import validate_email
......@@ -44,7 +44,7 @@ from openid.server.trustroot import TrustRoot
from openid.extensions import ax, sreg
from ratelimitbackend.exceptions import RateLimitException
import student.views as student_views
import student.views
# Required for Pearson
from courseware.views import get_module_for_descriptor, jump_to
from courseware.model_data import FieldDataCache
......@@ -136,6 +136,7 @@ def _external_login_or_signup(request,
fullname,
retfun=None):
"""Generic external auth login or signup"""
logout(request)
# see if we have a map from this external_id to an edX username
try:
......@@ -160,13 +161,13 @@ def _external_login_or_signup(request,
if uses_shibboleth:
# if we are using shib, try to link accounts using email
try:
link_user = User.objects.get(email=eamap.external_email)
link_user = User.objects.get(email=eamap.external_id)
if not ExternalAuthMap.objects.filter(user=link_user).exists():
# if there's no pre-existing linked eamap, we link the user
eamap.user = link_user
eamap.save()
internal_user = link_user
log.info('SHIB: Linking existing account for %s', eamap.external_email)
log.info('SHIB: Linking existing account for %s', eamap.external_id)
# now pass through to log in
else:
# otherwise, there must have been an error, b/c we've already linked a user with these external
......@@ -215,9 +216,9 @@ def _external_login_or_signup(request,
# testing request.method for extra paranoia
if uses_shibboleth and request.method == 'GET':
enroll_request = _make_shib_enrollment_request(request)
student_views.try_change_enrollment(enroll_request)
student.views.try_change_enrollment(enroll_request)
else:
student_views.try_change_enrollment(request)
student.views.try_change_enrollment(request)
AUDIT_LOG.info("Login success - %s (%s)", user.username, user.email)
if retfun is None:
return redirect('/')
......@@ -239,11 +240,13 @@ def _signup(request, eamap):
# save this for use by student.views.create_account
request.session['ExternalAuthMap'] = eamap
# default conjoin name, no spaces
# default conjoin name, no spaces, flattened to ascii b/c django can't handle unicode usernames, sadly
# but this only affects username, not fullname
username = eamap.external_name.replace(' ', '')
context = {'has_extauth_info': True,
'show_signup_immediately': True,
'extauth_domain': eamap.external_domain,
'extauth_id': eamap.external_id,
'extauth_email': eamap.external_email,
'extauth_username': username,
......@@ -270,7 +273,7 @@ def _signup(request, eamap):
log.info('EXTAUTH: Doing signup for %s', eamap.external_id)
return student_views.register_user(request, extra_context=context)
return student.views.register_user(request, extra_context=context)
# -----------------------------------------------------------------------------
......@@ -368,11 +371,11 @@ def ssl_login(request):
if not cert:
# no certificate information - go onward to main index
return student_views.index(request)
return student.views.index(request)
(_user, email, fullname) = _ssl_dn_extract_info(cert)
retfun = functools.partial(student_views.index, request)
retfun = functools.partial(student.views.index, request)
return _external_login_or_signup(
request,
external_id=email,
......@@ -435,24 +438,32 @@ def shib_login(request):
else:
# If we get here, the user has authenticated properly
shib = {attr: request.META.get(attr, '')
for attr in ['REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider']}
for attr in ['REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider', 'displayName']}
# Clean up first name, last name, and email address
# TODO: Make this less hardcoded re: format, but split will work
# even if ";" is not present, since we are accessing 1st element
shib['sn'] = shib['sn'].split(";")[0].strip().capitalize().decode('utf-8')
shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize().decode('utf-8')
shib['sn'] = shib['sn'].split(";")[0].strip().capitalize()
shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize()
# TODO: should we be logging creds here, at info level?
log.info("SHIB creds returned: %r", shib)
fullname = shib['displayName'] if shib['displayName'] else u'%s %s' % (shib['givenName'], shib['sn'])
next = request.REQUEST.get('next')
retfun = None
if next:
retfun = functools.partial(redirect, next)
return _external_login_or_signup(
request,
external_id=shib['REMOTE_USER'],
external_domain=SHIBBOLETH_DOMAIN_PREFIX + shib['Shib-Identity-Provider'],
credentials=shib,
email=shib['mail'],
fullname=u'%s %s' % (shib['givenName'], shib['sn']),
fullname=fullname,
retfun=retfun
)
......@@ -486,20 +497,18 @@ def course_specific_login(request, course_id):
Dispatcher function for selecting the specific login method
required by the course
"""
query_string = request.META.get("QUERY_STRING", '')
try:
course = course_from_id(course_id)
except ItemNotFoundError:
# couldn't find the course, will just return vanilla signin page
return redirect_with_querystring('signin_user', query_string)
return redirect_with_querystring('signin_user', request.GET)
# now the dispatching conditionals. Only shib for now
if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX):
return redirect_with_querystring('shib-login', query_string)
return redirect_with_querystring('shib-login', request.GET)
# Default fallthrough to normal signin page
return redirect_with_querystring('signin_user', query_string)
return redirect_with_querystring('signin_user', request.GET)
def course_specific_register(request, course_id):
......@@ -507,29 +516,27 @@ def course_specific_register(request, course_id):
Dispatcher function for selecting the specific registration method
required by the course
"""
query_string = request.META.get("QUERY_STRING", '')
try:
course = course_from_id(course_id)
except ItemNotFoundError:
# couldn't find the course, will just return vanilla registration page
return redirect_with_querystring('register_user', query_string)
return redirect_with_querystring('register_user', request.GET)
# now the dispatching conditionals. Only shib for now
if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX):
# shib-login takes care of both registration and login flows
return redirect_with_querystring('shib-login', query_string)
return redirect_with_querystring('shib-login', request.GET)
# Default fallthrough to normal registration page
return redirect_with_querystring('register_user', query_string)
return redirect_with_querystring('register_user', request.GET)
def redirect_with_querystring(view_name, query_string):
def redirect_with_querystring(view_name, querydict_get):
"""
Helper function to add query string to redirect views
Helper function to carry over get parameters across redirects
"""
if query_string:
return redirect("%s?%s" % (reverse(view_name), query_string))
if querydict_get:
return redirect("%s?%s" % (reverse(view_name), querydict_get.urlencode(safe='/')))
return redirect(view_name)
......
......@@ -2,14 +2,26 @@
Tests for student activation and login
'''
import json
import unittest
from mock import patch
from django.test import TestCase
from django.test.client import Client
from django.test.utils import override_settings
from django.conf import settings
from django.core.cache import cache
from django.core.urlresolvers import reverse, NoReverseMatch
from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory
from student.views import parse_course_id_from_string, get_course_enrollment_domain
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import editable_modulestore
from external_auth.models import ExternalAuthMap
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
class LoginTest(TestCase):
'''
......@@ -164,3 +176,92 @@ class LoginTest(TestCase):
format_string = args[0]
for log_string in log_strings:
self.assertIn(log_string, format_string)
class UtilFnTest(TestCase):
def test_parse_course_id_from_string(self):
COURSE_ID = u'org/num/run'
COURSE_URL = u'/courses/{}/otherstuff'.format(COURSE_ID)
NON_COURSE_URL = u'/blahblah'
self.assertEqual(parse_course_id_from_string(COURSE_URL), COURSE_ID)
self.assertIsNone(parse_course_id_from_string(NON_COURSE_URL))
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class ExternalAuthTest(ModuleStoreTestCase):
"""
Tests how login_user() interacts with ExternalAuth, in particular Shib
"""
def setUp(self):
self.store = editable_modulestore()
self.course = CourseFactory.create(org='Stanford', number='456', display_name='NO SHIB')
self.shib_course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only')
self.shib_course.enrollment_domain = 'shib:https://idp.stanford.edu/'
metadata = own_metadata(self.shib_course)
metadata['enrollment_domain'] = self.shib_course.enrollment_domain
self.store.update_metadata(self.shib_course.location.url(), metadata)
self.user_w_map = UserFactory.create(email='withmap@stanford.edu')
self.extauth = ExternalAuthMap(external_id='withmap@stanford.edu',
external_email='withmap@stanford.edu',
external_domain='shib:https://idp.stanford.edu/',
external_credentials="",
user=self.user_w_map)
self.user_w_map.save()
self.extauth.save()
self.user_wo_map = UserFactory.create(email='womap@gmail.com')
self.user_wo_map.save()
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
def test_login_page_redirect(self):
"""
Tests that when a shib user types their email address into the login page, they get redirected
to the shib login.
"""
response = self.client.post(reverse('login'), {'email':self.user_w_map.email, 'password':''})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, json.dumps({'success': False, 'redirect':reverse('shib-login')}))
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
def test_get_course_enrollment_domain(self):
"""
Tests the get_course_enrollment_domain utility function
"""
self.assertIsNone(get_course_enrollment_domain("I/DONT/EXIST"))
self.assertIsNone(get_course_enrollment_domain(self.course.id))
self.assertEqual(self.shib_course.enrollment_domain, get_course_enrollment_domain(self.shib_course.id))
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
def test_login_required_dashboard(self):
"""
Tests redirects to when @login_required to dashboard, which should always be the normal login,
since there is no course context
"""
response = self.client.get(reverse('dashboard'))
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], 'http://testserver/accounts/login?next=/dashboard')
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
def test_externalauth_login_required_course_context(self):
"""
Tests the redirects when visiting course-specific URL with @login_required.
Should vary by course depending on its enrollment_domain
"""
TARGET_URL = reverse('courseware', args=[self.course.id])
noshib_response = self.client.get(TARGET_URL, follow=True)
self.assertEqual(noshib_response.redirect_chain[-1],
('http://testserver/accounts/login?next={url}'.format(url=TARGET_URL), 302))
self.assertContains(noshib_response, ("<title>Log into your {platform_name} Account</title>"
.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)
......@@ -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<course_id>[^/]+/[^/]+/[^/]+)', 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':
......
......@@ -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();
......
<%! 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"><title>${_("Preferences for {platform_name}").format(platform_name=settings.PLATFORM_NAME)}</title></%block>
<%block name="js_extra">
<script type="text/javascript">
$(function() {
var view_name = 'view-register';
// adding js class for styling with accessibility in mind
$('body').addClass('js').addClass(view_name);
// new window/tab opening
$('a[rel="external"], a[class="new-vp"]')
.click( function() {
window.open( $(this).attr('href') );
return false;
});
// form field label styling on focus
$("form :input").focus(function() {
$("label[for='" + this.id + "']").parent().addClass("is-focused");
}).blur(function() {
$("label").parent().removeClass("is-focused");
});
});
(function() {
toggleSubmitButton(true);
$('#register-form').on('submit', function() {
toggleSubmitButton(false);
});
$('#register-form').on('ajax:error', function() {
toggleSubmitButton(true);
});
$('#register-form').on('ajax:success', function(event, json, xhr) {
if(json.success) {
location.href="${reverse('dashboard')}";
} else {
toggleSubmitButton(true);
$('.status.message.submission-error').addClass('is-shown').focus();
$('.status.message.submission-error .message-copy').html(json.value).stop().css("display", "block");
$(".field-error").removeClass('field-error');
$("[data-field='"+json.field+"']").addClass('field-error')
}
});
})(this);
function toggleSubmitButton(enable) {
var $submitButton = $('form .form-actions #submit');
if(enable) {
$submitButton.
removeClass('is-disabled').
removeProp('disabled').
html("${_('Update my {platform_name} Account').format(platform_name=settings.PLATFORM_NAME)}");
}
else {
$submitButton.
addClass('is-disabled').
prop('disabled', true).
html(gettext('Processing your account information &hellip;'));
}
}
</script>
</%block>
<section class="introduction">
<header>
<h1 class="sr">${_("Welcome {username}! Please set your preferences below").format(username=extauth_id,
platform_name=settings.PLATFORM_NAME)}</h1>
</header>
</section>
<%block name="login_button"></%block>
<section class="register container">
<section role="main" class="content">
<form role="form" id="register-form" method="post" data-remote="true" action="/create_account" novalidate>
<!-- status messages -->
<div role="alert" class="status message">
<h3 class="message-title">${_("We're sorry, {platform_name} enrollment is not available in your region").format(platform_name=settings.PLATFORM_NAME)}</h3>
<p class="message-copy">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
</div>
<div role="alert" class="status message submission-error" tabindex="-1">
<h3 class="message-title">${_("The following errors occured while processing your registration:")} </h3>
<ul class="message-copy"> </ul>
</div>
<p class="instructions">
${_('Required fields are noted by <strong class="indicator">bold text and an asterisk (*)</strong>.')}
</p>
<fieldset class="group group-form group-form-requiredinformation">
<legend class="sr">${_('Required Information')}</legend>
<div class="message">
<p class="message-copy">${_("Enter a public username:")}</p>
</div>
<ol class="list-input">
<li class="field required text" id="field-username">
<label for="username">${_('Public Username')}</label>
<input id="username" type="text" name="username" value="${extauth_username}" placeholder="${_('example: JaneDoe')}" required aria-required="true" />
<span class="tip tip-input">${_('Will be shown in any discussions or forums you participate in')}</span>
</li>
% if ask_for_email:
<li class="field required text" id="field-email">
<label for="email">${_("E-mail")}</label>
<input class="" id="email" type="email" name="email" value="" placeholder="${_('example: username@domain.com')}" />
</li>
% endif
% if ask_for_fullname:
<li class="field required text" id="field-name">
<label for="name">${_('Full Name')}</label>
<input id="name" type="text" name="name" value="" placeholder="$_('example: Jane Doe')}" />
</li>
% endif
</ol>
</fieldset>
<fieldset class="group group-form group-form-accountacknowledgements">
<legend class="sr">${_("Account Acknowledgements")}</legend>
<ol class="list-input">
<li class="field-group">
% if ask_for_tos :
<div class="field required checkbox" id="field-tos">
<input id="tos-yes" type="checkbox" name="terms_of_service" value="true" required aria-required="true" />
<label for="tos-yes">${_('I agree to the {link_start}Terms of Service{link_end}').format(
link_start='<a href="{url}" class="new-vp">'.format(url=marketing_link('TOS')),
link_end='</a>')}</label>
</div>
% endif
<div class="field required checkbox" id="field-honorcode">
<input id="honorcode-yes" type="checkbox" name="honor_code" value="true" />
<%
## TODO: provide a better way to override these links
if self.stanford_theme_enabled():
honor_code_path = marketing_link('TOS') + "#honor"
else:
honor_code_path = marketing_link('HONOR')
%>
<label for="honorcode-yes">${_('I agree to the {link_start}Honor Code{link_end}').format(
link_start='<a href="{url}" class="new-vp">'.format(url=honor_code_path),
link_end='</a>')}</label>
</div>
</li>
</ol>
</fieldset>
% if course_id and enrollment_action:
<input type="hidden" name="enrollment_action" value="${enrollment_action | h}" />
<input type="hidden" name="course_id" value="${course_id | h}" />
% endif
<div class="form-actions">
<button name="submit" type="submit" id="submit" class="action action-primary action-update">${_('Submit')} <span class="orn-plus">+</span> ${_('Update My Account')}</button>
</div>
</form>
</section>
</section>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment