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:
Jason Bau
2013-09-06 01:36:52 -07:00
parent 4d656061be
commit 6a850e27bb
7 changed files with 125 additions and 58 deletions

View File

@@ -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

View File

@@ -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':