Commit 69097e7b by Carson Gee

Merge pull request #2032 from carsongee/cg/ssl-studio

More fully integrate and test ssl external auth in CMS
parents 4dd2c42a 8cbe263c
...@@ -3,13 +3,13 @@ Public views ...@@ -3,13 +3,13 @@ Public views
""" """
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.conf import settings from django.conf import settings
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut from external_auth.views import ssl_login_shortcut, ssl_get_cert_from_request
from microsite_configuration.middleware import MicrositeConfiguration from microsite_configuration.middleware import MicrositeConfiguration
__all__ = ['signup', 'login_page', 'howitworks'] __all__ = ['signup', 'login_page', 'howitworks']
...@@ -21,7 +21,14 @@ def signup(request): ...@@ -21,7 +21,14 @@ def signup(request):
Display the signup form. Display the signup form.
""" """
csrf_token = csrf(request)['csrf_token'] csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token}) if request.user.is_authenticated():
return redirect('/course')
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to course to login to process their certificate if SSL is enabled
# and registration is disabled.
return redirect(reverse('login'))
return render_to_response('register.html', {'csrf': csrf_token})
@ssl_login_shortcut @ssl_login_shortcut
...@@ -31,6 +38,12 @@ def login_page(request): ...@@ -31,6 +38,12 @@ def login_page(request):
Display the login form. Display the login form.
""" """
csrf_token = csrf(request)['csrf_token'] csrf_token = csrf(request)['csrf_token']
if (settings.FEATURES['AUTH_USE_CERTIFICATES'] and
ssl_get_cert_from_request(request)):
# SSL login doesn't require a login view, so redirect
# to course now that the user is authenticated via
# the decorator.
return redirect('/course')
return render_to_response( return render_to_response(
'login.html', 'login.html',
{ {
......
...@@ -44,7 +44,7 @@ FEATURES = { ...@@ -44,7 +44,7 @@ FEATURES = {
'ENABLE_DISCUSSION_SERVICE': False, 'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_CERTIFICATES': False,
# email address for studio staff (eg to request course creation) # email address for studio staff (eg to request course creation)
'STUDIO_REQUEST_EMAIL': '', 'STUDIO_REQUEST_EMAIL': '',
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
from .common import * from .common import *
from .dev import * from .dev import *
FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True FEATURES['AUTH_USE_CERTIFICATES'] = True
FEATURES['USE_DJANGO_PIPELINE'] = False # don't recompile scss FEATURES['USE_DJANGO_PIPELINE'] = False # don't recompile scss
......
...@@ -146,6 +146,9 @@ CACHES = { ...@@ -146,6 +146,9 @@ CACHES = {
} }
# Add external_auth to Installed apps for testing
INSTALLED_APPS += ('external_auth', )
# hide ratelimit warnings while running tests # hide ratelimit warnings while running tests
filterwarnings('ignore', message='No request passed to the backend, unable to rate-limit') filterwarnings('ignore', message='No request passed to the backend, unable to rate-limit')
......
...@@ -21,13 +21,14 @@ from edxmako.middleware import MakoMiddleware ...@@ -21,13 +21,14 @@ from edxmako.middleware import MakoMiddleware
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
import external_auth.views import external_auth.views
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.exceptions import InsufficientSpecificationError
FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy() FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy()
FEATURES_WITH_SSL_AUTH['AUTH_USE_MIT_CERTIFICATES'] = True FEATURES_WITH_SSL_AUTH['AUTH_USE_CERTIFICATES'] = True
FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP = FEATURES_WITH_SSL_AUTH.copy() FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP = FEATURES_WITH_SSL_AUTH.copy()
FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP['AUTH_USE_MIT_CERTIFICATES_IMMEDIATE_SIGNUP'] = True FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP['AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'] = True
FEATURES_WITHOUT_SSL_AUTH = settings.FEATURES.copy() FEATURES_WITHOUT_SSL_AUTH = settings.FEATURES.copy()
FEATURES_WITHOUT_SSL_AUTH['AUTH_USE_MIT_CERTIFICATES'] = False FEATURES_WITHOUT_SSL_AUTH['AUTH_USE_CERTIFICATES'] = False
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH) @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH)
...@@ -90,15 +91,10 @@ class SSLClientTest(TestCase): ...@@ -90,15 +91,10 @@ class SSLClientTest(TestCase):
User.objects.get(email=self.USER_EMAIL) User.objects.get(email=self.USER_EMAIL)
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms') @unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
@unittest.skip
def test_ssl_login_with_signup_cms(self): def test_ssl_login_with_signup_cms(self):
""" """
Validate that an SSL login creates an eamap user and Validate that an SSL login creates an eamap user and
redirects them to the signup page on CMS. redirects them to the signup page on CMS.
This currently is failing and should be resolved to passing at
some point. using skip here instead of expectFailure because
of an issue with nose.
""" """
self.client.get( self.client.get(
reverse('contentstore.views.login_page'), reverse('contentstore.views.login_page'),
...@@ -135,21 +131,19 @@ class SSLClientTest(TestCase): ...@@ -135,21 +131,19 @@ class SSLClientTest(TestCase):
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms') @unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP) @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
@unittest.skip
def test_ssl_login_without_signup_cms(self): def test_ssl_login_without_signup_cms(self):
""" """
Test IMMEDIATE_SIGNUP feature flag and ensure the user account is Test IMMEDIATE_SIGNUP feature flag and ensure the user account is
automatically created on CMS. automatically created on CMS, and that we are redirected
to courses.
This currently is failing and should be resolved to passing at
some point. using skip here instead of expectFailure because
of an issue with nose.
""" """
self.client.get( response = self.client.get(
reverse('contentstore.views.login_page'), reverse('contentstore.views.login_page'),
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL) SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
) )
self.assertEqual(response.status_code, 302)
self.assertIn('/course', response['location'])
# Assert our user exists in both eamap and Users, and that we are logged in # Assert our user exists in both eamap and Users, and that we are logged in
try: try:
...@@ -191,6 +185,27 @@ class SSLClientTest(TestCase): ...@@ -191,6 +185,27 @@ class SSLClientTest(TestCase):
self.assertIn(reverse('dashboard'), response['location']) self.assertIn(reverse('dashboard'), response['location'])
self.assertIn('_auth_user_id', self.client.session) self.assertIn('_auth_user_id', self.client.session)
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
def test_cms_registration_page_bypass(self):
"""
This tests to make sure when immediate signup is on that
the user doesn't get presented with the registration page.
"""
# Expect a NotImplementError from course page as we don't have anything else built
with self.assertRaisesRegexp(InsufficientSpecificationError,
'Must provide one of url, version_guid, package_id'):
self.client.get(
reverse('signup'), follow=True,
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
# assert that we are logged in
self.assertIn('_auth_user_id', self.client.session)
# Now that we are logged in, make sure we don't see the registration page
with self.assertRaisesRegexp(InsufficientSpecificationError,
'Must provide one of url, version_guid, package_id'):
self.client.get(reverse('signup'), follow=True)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP) @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
def test_signin_page_bypass(self): def test_signin_page_bypass(self):
...@@ -212,6 +227,7 @@ class SSLClientTest(TestCase): ...@@ -212,6 +227,7 @@ class SSLClientTest(TestCase):
self.assertIn(reverse('dashboard'), response['location']) self.assertIn(reverse('dashboard'), response['location'])
self.assertIn('_auth_user_id', self.client.session) self.assertIn('_auth_user_id', self.client.session)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP) @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
def test_ssl_bad_eamap(self): def test_ssl_bad_eamap(self):
......
...@@ -253,7 +253,7 @@ def _signup(request, eamap, retfun=None): ...@@ -253,7 +253,7 @@ def _signup(request, eamap, retfun=None):
# save this for use by student.views.create_account # save this for use by student.views.create_account
request.session['ExternalAuthMap'] = eamap request.session['ExternalAuthMap'] = eamap
if settings.FEATURES.get('AUTH_USE_MIT_CERTIFICATES_IMMEDIATE_SIGNUP', ''): if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP', ''):
# do signin immediately, by calling create_account, instead of asking # do signin immediately, by calling create_account, instead of asking
# student to fill in form. MIT students already have information filed. # student to fill in form. MIT students already have information filed.
username = eamap.external_email.split('@', 1)[0] username = eamap.external_email.split('@', 1)[0]
...@@ -362,7 +362,7 @@ def ssl_login_shortcut(fn): ...@@ -362,7 +362,7 @@ def ssl_login_shortcut(fn):
call. call.
""" """
if not settings.FEATURES['AUTH_USE_MIT_CERTIFICATES']: if not settings.FEATURES['AUTH_USE_CERTIFICATES']:
return fn(*args, **kwargs) return fn(*args, **kwargs)
request = args[0] request = args[0]
...@@ -394,7 +394,7 @@ def ssl_login_shortcut(fn): ...@@ -394,7 +394,7 @@ def ssl_login_shortcut(fn):
def ssl_login(request): def ssl_login(request):
""" """
This is called by branding.views.index when This is called by branding.views.index when
FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True FEATURES['AUTH_USE_CERTIFICATES'] = True
Used for MIT user authentication. This presumes the web server Used for MIT user authentication. This presumes the web server
(nginx) has been configured to require specific client (nginx) has been configured to require specific client
...@@ -408,7 +408,7 @@ def ssl_login(request): ...@@ -408,7 +408,7 @@ def ssl_login(request):
Else continues on with student.views.index, and no authentication. Else continues on with student.views.index, and no authentication.
""" """
# Just to make sure we're calling this only at MIT: # Just to make sure we're calling this only at MIT:
if not settings.FEATURES['AUTH_USE_MIT_CERTIFICATES']: if not settings.FEATURES['AUTH_USE_CERTIFICATES']:
return HttpResponseForbidden() return HttpResponseForbidden()
cert = ssl_get_cert_from_request(request) cert = ssl_get_cert_from_request(request)
......
...@@ -330,7 +330,7 @@ def signin_user(request): ...@@ -330,7 +330,7 @@ def signin_user(request):
""" """
This view will display the non-modal login form This view will display the non-modal login form
""" """
if (settings.FEATURES['AUTH_USE_MIT_CERTIFICATES'] and if (settings.FEATURES['AUTH_USE_CERTIFICATES'] and
external_auth.views.ssl_get_cert_from_request(request)): external_auth.views.ssl_get_cert_from_request(request)):
# SSL login doesn't require a view, so redirect # SSL login doesn't require a view, so redirect
# branding and allow that to process the login if it # branding and allow that to process the login if it
...@@ -357,7 +357,7 @@ def register_user(request, extra_context=None): ...@@ -357,7 +357,7 @@ def register_user(request, extra_context=None):
""" """
if request.user.is_authenticated(): if request.user.is_authenticated():
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
if settings.FEATURES.get('AUTH_USE_MIT_CERTIFICATES_IMMEDIATE_SIGNUP'): if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to branding to process their certificate if SSL is enabled # Redirect to branding to process their certificate if SSL is enabled
# and registration is disabled. # and registration is disabled.
return redirect(reverse('root')) return redirect(reverse('root'))
...@@ -645,7 +645,7 @@ def accounts_login(request): ...@@ -645,7 +645,7 @@ def accounts_login(request):
""" """
if settings.FEATURES.get('AUTH_USE_CAS'): if settings.FEATURES.get('AUTH_USE_CAS'):
return redirect(reverse('cas-login')) return redirect(reverse('cas-login'))
if settings.FEATURES['AUTH_USE_MIT_CERTIFICATES']: if settings.FEATURES['AUTH_USE_CERTIFICATES']:
# SSL login doesn't require a view, so redirect # SSL login doesn't require a view, so redirect
# to branding and allow that to process the login. # to branding and allow that to process the login.
return redirect(reverse('root')) return redirect(reverse('root'))
......
...@@ -23,7 +23,7 @@ def index(request): ...@@ -23,7 +23,7 @@ def index(request):
if settings.COURSEWARE_ENABLED and request.user.is_authenticated(): if settings.COURSEWARE_ENABLED and request.user.is_authenticated():
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
if settings.FEATURES.get('AUTH_USE_MIT_CERTIFICATES'): if settings.FEATURES.get('AUTH_USE_CERTIFICATES'):
from external_auth.views import ssl_login from external_auth.views import ssl_login
return ssl_login(request) return ssl_login(request)
......
...@@ -26,7 +26,7 @@ TEST_MONGODB_LOG = { ...@@ -26,7 +26,7 @@ TEST_MONGODB_LOG = {
} }
FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy() FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy()
FEATURES_WITH_SSL_AUTH['AUTH_USE_MIT_CERTIFICATES'] = True FEATURES_WITH_SSL_AUTH['AUTH_USE_CERTIFICATES'] = True
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
......
...@@ -160,7 +160,7 @@ class Users(SysadminDashboardView): ...@@ -160,7 +160,7 @@ class Users(SysadminDashboardView):
email_domain = getattr(settings, 'SSL_AUTH_EMAIL_DOMAIN', 'MIT.EDU') email_domain = getattr(settings, 'SSL_AUTH_EMAIL_DOMAIN', 'MIT.EDU')
msg = u'' msg = u''
if settings.FEATURES['AUTH_USE_MIT_CERTIFICATES']: if settings.FEATURES['AUTH_USE_CERTIFICATES']:
if not '@' in uname: if not '@' in uname:
email = '{0}@{1}'.format(uname, email_domain) email = '{0}@{1}'.format(uname, email_domain)
else: else:
...@@ -202,7 +202,7 @@ class Users(SysadminDashboardView): ...@@ -202,7 +202,7 @@ class Users(SysadminDashboardView):
profile.name = name profile.name = name
profile.save() profile.save()
if settings.FEATURES['AUTH_USE_MIT_CERTIFICATES']: if settings.FEATURES['AUTH_USE_CERTIFICATES']:
credential_string = getattr(settings, 'SSL_AUTH_DN_FORMAT_STRING', credential_string = getattr(settings, 'SSL_AUTH_DN_FORMAT_STRING',
'/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}') '/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}')
credentials = credential_string.format(name, email) credentials = credential_string.format(name, email)
......
...@@ -37,7 +37,7 @@ TEST_MONGODB_LOG = { ...@@ -37,7 +37,7 @@ TEST_MONGODB_LOG = {
} }
FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy() FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy()
FEATURES_WITH_SSL_AUTH['AUTH_USE_MIT_CERTIFICATES'] = True FEATURES_WITH_SSL_AUTH['AUTH_USE_CERTIFICATES'] = True
class SysadminBaseTestCase(ModuleStoreTestCase): class SysadminBaseTestCase(ModuleStoreTestCase):
......
...@@ -8,7 +8,7 @@ Settings for the LMS that runs alongside the CMS on AWS ...@@ -8,7 +8,7 @@ Settings for the LMS that runs alongside the CMS on AWS
from ..dev import * from ..dev import *
FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False FEATURES['AUTH_USE_CERTIFICATES'] = False
SUBDOMAIN_BRANDING['edge'] = 'edge' SUBDOMAIN_BRANDING['edge'] = 'edge'
SUBDOMAIN_BRANDING['preview.edge'] = 'edge' SUBDOMAIN_BRANDING['preview.edge'] = 'edge'
......
...@@ -98,7 +98,7 @@ FEATURES = { ...@@ -98,7 +98,7 @@ FEATURES = {
# extrernal access methods # extrernal access methods
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False, 'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False, 'AUTH_USE_OPENID': False,
'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_CERTIFICATES': False,
'AUTH_USE_OPENID_PROVIDER': False, 'AUTH_USE_OPENID_PROVIDER': False,
# Even though external_auth is in common, shib assumes the LMS views / urls, so it should only be enabled # Even though external_auth is in common, shib assumes the LMS views / urls, so it should only be enabled
# in LMS # in LMS
......
...@@ -202,7 +202,7 @@ OPENID_PROVIDER_TRUSTED_ROOTS = ['*'] ...@@ -202,7 +202,7 @@ OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
######################## MIT Certificates SSL Auth ############################ ######################## MIT Certificates SSL Auth ############################
FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False FEATURES['AUTH_USE_CERTIFICATES'] = False
################################# CELERY ###################################### ################################# CELERY ######################################
......
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