Address @brianhw review comments
* Fix open redirect vulnerability * Add Logging To AUDIT_LOG : Note I had to change existing tests that mocked AUDIT_LOG with this * Use external_auth.views.SHIBBOLETH_DOMAIN_PREFIX in student.views * Add a bunch of documentation * PEP8 / Pylint
This commit is contained in:
@@ -12,7 +12,7 @@ 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 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
|
||||
@@ -166,27 +166,34 @@ class LoginTest(TestCase):
|
||||
|
||||
def _assert_audit_log(self, mock_audit_log, level, log_strings):
|
||||
"""
|
||||
Check that the audit log has received the expected call.
|
||||
Check that the audit log has received the expected call as its last call.
|
||||
"""
|
||||
method_calls = mock_audit_log.method_calls
|
||||
self.assertEquals(len(method_calls), 1)
|
||||
name, args, _kwargs = method_calls[0]
|
||||
name, args, _kwargs = method_calls[-1]
|
||||
self.assertEquals(name, level)
|
||||
self.assertEquals(len(args), 1)
|
||||
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))
|
||||
"""
|
||||
Tests for utility functions in student.views
|
||||
"""
|
||||
def test__parse_course_id_from_string(self):
|
||||
"""
|
||||
Tests the _parse_course_id_from_string util function
|
||||
"""
|
||||
COURSE_ID = u'org/num/run' # pylint: disable=C0103
|
||||
COURSE_URL = u'/courses/{}/otherstuff'.format(COURSE_ID) # pylint: disable=C0103
|
||||
NON_COURSE_URL = u'/blahblah' # pylint: disable=C0103
|
||||
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):
|
||||
class ExternalAuthShibTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests how login_user() interacts with ExternalAuth, in particular Shib
|
||||
"""
|
||||
@@ -215,18 +222,18 @@ class ExternalAuthTest(ModuleStoreTestCase):
|
||||
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':''})
|
||||
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')}))
|
||||
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):
|
||||
def test__get_course_enrollment_domain(self):
|
||||
"""
|
||||
Tests the get_course_enrollment_domain utility function
|
||||
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))
|
||||
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):
|
||||
@@ -244,7 +251,7 @@ class ExternalAuthTest(ModuleStoreTestCase):
|
||||
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])
|
||||
TARGET_URL = reverse('courseware', args=[self.course.id]) # pylint: disable=C0103
|
||||
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))
|
||||
@@ -252,11 +259,11 @@ class ExternalAuthTest(ModuleStoreTestCase):
|
||||
.format(platform_name=settings.PLATFORM_NAME)))
|
||||
self.assertEqual(noshib_response.status_code, 200)
|
||||
|
||||
TARGET_URL_SHIB = reverse('courseware', args=[self.shib_course.id])
|
||||
TARGET_URL_SHIB = reverse('courseware', args=[self.shib_course.id]) # pylint: disable=C0103
|
||||
shib_response = self.client.get(**{'path': TARGET_URL_SHIB,
|
||||
'follow': True,
|
||||
'REMOTE_USER':self.extauth.external_id,
|
||||
'Shib-Identity-Provider':'https://idp.stanford.edu/'})
|
||||
'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
|
||||
|
||||
@@ -25,7 +25,7 @@ from django.core.validators import validate_email, validate_slug, ValidationErro
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbidden,
|
||||
HttpResponseNotAllowed, Http404, HttpResponseRedirect)
|
||||
HttpResponseNotAllowed, Http404)
|
||||
from django.shortcuts import redirect
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.utils.http import cookie_date
|
||||
@@ -72,8 +72,6 @@ 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."""
|
||||
@@ -259,7 +257,7 @@ 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):
|
||||
if context.get("extauth_domain", '').startswith(external_auth.views.SHIBBOLETH_DOMAIN_PREFIX):
|
||||
return render_to_response('register-shib.html', context)
|
||||
return render_to_response('register.html', context)
|
||||
|
||||
@@ -414,14 +412,24 @@ def change_enrollment(request):
|
||||
return HttpResponseBadRequest(_("Enrollment action is invalid"))
|
||||
|
||||
|
||||
def parse_course_id_from_string(input_str):
|
||||
def _parse_course_id_from_string(input_str):
|
||||
"""
|
||||
Helper function to determine if input_str (typically the queryparam 'next') contains a course_id.
|
||||
@param input_str:
|
||||
@return: the course_id if found, None if not
|
||||
"""
|
||||
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):
|
||||
def _get_course_enrollment_domain(course_id):
|
||||
"""
|
||||
Helper function to get the enrollment domain set for a course with id course_id
|
||||
@param course_id:
|
||||
@return:
|
||||
"""
|
||||
try:
|
||||
course = course_from_id(course_id)
|
||||
return course.enrollment_domain
|
||||
@@ -435,10 +443,10 @@ def accounts_login(request):
|
||||
return redirect(reverse('cas-login'))
|
||||
# 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):
|
||||
redirect_to = request.GET.get('next')
|
||||
if redirect_to:
|
||||
course_id = _parse_course_id_from_string(redirect_to)
|
||||
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')
|
||||
|
||||
@@ -465,11 +473,11 @@ def login_user(request, error=""):
|
||||
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):
|
||||
if eamap.external_domain.startswith(external_auth.views.SHIBBOLETH_DOMAIN_PREFIX):
|
||||
return HttpResponse(json.dumps({'success': False, 'redirect': reverse('shib-login')}))
|
||||
except ExternalAuthMap.DoesNotExist:
|
||||
# This is actually the common case, logging in user without external linked login
|
||||
log.info("User %s w/o external auth attempting login", user)
|
||||
AUDIT_LOG.info("User %s w/o external auth attempting login", user)
|
||||
|
||||
# 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
|
||||
@@ -674,7 +682,8 @@ def create_account(request, post_override=None):
|
||||
# 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 eamap.external_domain.startswith(SHIB_DOMAIN_PREFIX))
|
||||
DoExternalAuth and
|
||||
eamap.external_domain.startswith(external_auth.views.SHIBBOLETH_DOMAIN_PREFIX))
|
||||
|
||||
if not tos_not_required:
|
||||
if post_vars.get('terms_of_service', 'false') != u'true':
|
||||
|
||||
Reference in New Issue
Block a user