# -*- coding: utf-8 -*- """ Tests for Shibboleth Authentication @jbau """ import unittest from ddt import ddt, data from django.conf import settings from django.http import HttpResponseRedirect from django.test import TestCase from django.test.client import RequestFactory, Client as DjangoTestClient from django.test.utils import override_settings from django.core.urlresolvers import reverse from django.contrib.auth.models import AnonymousUser, User from django.utils.importlib import import_module from edxmako.tests import mako_middleware_process_request from external_auth.models import ExternalAuthMap from external_auth.views import ( shib_login, course_specific_login, course_specific_register, _flatten_to_ascii ) from mock import patch from urllib import urlencode from student.views import create_account, change_enrollment from student.models import UserProfile, CourseEnrollment from student.tests.factories import UserFactory from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore import ModuleStoreEnum # Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider' # attributes via request.META. We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present # 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 # These values would all returned from request.META, so they need to be str, not unicode IDP = 'https://idp.stanford.edu/' REMOTE_USER = 'test_user@stanford.edu' MAILS = [None, '', 'test_user@stanford.edu'] # unicode shouldn't be in emails, would fail django's email validator DISPLAYNAMES = [None, '', 'Jason 包'] GIVENNAMES = [None, '', 'jasön; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' SNS = [None, '', '包; smith'] # At Stanford, the sns can be a list delimited by ';' def gen_all_identities(): """ A generator for all combinations of test inputs. Each generated item is a dict that represents what a shib IDP could potentially pass to django via request.META, i.e. setting (or not) request.META['givenName'], etc. """ 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: meta_dict['givenName'] = given_name if surname is not None: meta_dict['sn'] = surname return meta_dict for mail in MAILS: for given_name in GIVENNAMES: for surname in SNS: for display_name in DISPLAYNAMES: yield _build_identity_dict(mail, display_name, given_name, surname) @ddt @override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache') class ShibSPTest(ModuleStoreTestCase): """ Tests for the Shibboleth SP, which communicates via request.META (Apache environment variables set by mod_shib) """ request_factory = RequestFactory() def setUp(self): super(ShibSPTest, self).setUp(create_user=False) self.test_user_id = ModuleStoreEnum.UserID.test @unittest.skipUnless(settings.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 or Shib-Identity-Provider in request.META """ no_remote_user_request = self.request_factory.get('/shib-login') no_remote_user_request.META.update({'Shib-Identity-Provider': IDP}) no_remote_user_request.user = AnonymousUser() mako_middleware_process_request(no_remote_user_request) no_remote_user_response = shib_login(no_remote_user_request) self.assertEqual(no_remote_user_response.status_code, 403) self.assertIn("identity server did not return your ID information", no_remote_user_response.content) no_idp_request = self.request_factory.get('/shib-login') no_idp_request.META.update({'REMOTE_USER': REMOTE_USER}) no_idp_response = shib_login(no_idp_request) self.assertEqual(no_idp_response.status_code, 403) self.assertIn("identity server did not return your ID information", no_idp_response.content) def _assert_shib_login_is_logged(self, audit_log_call, remote_user): """Asserts that shibboleth login attempt is being logged""" remote_user = _flatten_to_ascii(remote_user) # django usernames have to be ascii method_name, args, _kwargs = audit_log_call self.assertEquals(method_name, 'info') self.assertEquals(len(args), 1) self.assertIn(u'logged in via Shibboleth', args[0]) self.assertIn(remote_user, args[0]) @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_shib_login(self): """ Tests that: * shib credentials that match an existing ExternalAuthMap with a linked active user logs the user in * shib credentials that match an existing ExternalAuthMap with a linked inactive user shows error page * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user without an existing ExternalAuthMap links the two and log the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user that already has an ExternalAuthMap causes an error (403) * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear """ user_w_map = UserFactory.create(email='withmap@stanford.edu') extauth = ExternalAuthMap(external_id='withmap@stanford.edu', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=user_w_map) user_wo_map = UserFactory.create(email='womap@stanford.edu') user_w_map.save() user_wo_map.save() extauth.save() inactive_user = UserFactory.create(email='inactive@stanford.edu') inactive_user.is_active = False inactive_extauth = ExternalAuthMap(external_id='inactive@stanford.edu', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=inactive_user) inactive_user.save() inactive_extauth.save() idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/'] remote_users = ['withmap@stanford.edu', 'womap@stanford.edu', 'testuser2@someother_idp.com', 'inactive@stanford.edu'] for idp in idps: for remote_user in remote_users: request = self.request_factory.get('/shib-login') request.session = import_module(settings.SESSION_ENGINE).SessionStore() # empty session request.META.update({'Shib-Identity-Provider': idp, 'REMOTE_USER': remote_user, 'mail': remote_user}) request.user = AnonymousUser() mako_middleware_process_request(request) with patch('external_auth.views.AUDIT_LOG') as mock_audit_log: response = shib_login(request) audit_log_calls = mock_audit_log.method_calls if idp == "https://idp.stanford.edu/" and remote_user == 'withmap@stanford.edu': self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_w_map) self.assertEqual(response['Location'], '/dashboard') # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'info') self.assertEquals(len(args), 1) self.assertIn(u'Login success', args[0]) self.assertIn(remote_user, args[0]) elif idp == "https://idp.stanford.edu/" and remote_user == 'inactive@stanford.edu': self.assertEqual(response.status_code, 403) self.assertIn("Account not yet activated: please look for link in your email", response.content) # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'warning') self.assertEquals(len(args), 1) self.assertIn(u'is not active after external login', args[0]) # self.assertEquals(remote_user, args[1]) elif idp == "https://idp.stanford.edu/" and remote_user == 'womap@stanford.edu': self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map)) self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_wo_map) self.assertEqual(response['Location'], '/dashboard') # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'info') self.assertEquals(len(args), 1) self.assertIn(u'Login success', args[0]) self.assertIn(remote_user, args[0]) elif idp == "https://someother.idp.com/" and remote_user in \ ['withmap@stanford.edu', 'womap@stanford.edu', 'inactive@stanford.edu']: self.assertEqual(response.status_code, 403) self.assertIn("You have already created an account using an external login", response.content) # no audit logging calls self.assertEquals(len(audit_log_calls), 0) else: self.assertEqual(response.status_code, 200) self.assertContains(response, ("Preferences for {platform_name}" .format(platform_name=settings.PLATFORM_NAME))) # no audit logging calls self.assertEquals(len(audit_log_calls), 0) def _base_test_extauth_auto_activate_user_with_flag(self, log_user_string="inactive@stanford.edu"): """ Tests that FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] means extauth automatically linked users, activates them, and logs them in """ inactive_user = UserFactory.create(email='inactive@stanford.edu') inactive_user.is_active = False inactive_user.save() request = self.request_factory.get('/shib-login') request.session = import_module(settings.SESSION_ENGINE).SessionStore() # empty session request.META.update({ 'Shib-Identity-Provider': 'https://idp.stanford.edu/', 'REMOTE_USER': 'inactive@stanford.edu', 'mail': 'inactive@stanford.edu' }) request.user = AnonymousUser() with patch('external_auth.views.AUDIT_LOG') as mock_audit_log: response = shib_login(request) audit_log_calls = mock_audit_log.method_calls # reload user from db, since the view function works via db side-effects inactive_user = User.objects.get(id=inactive_user.id) self.assertIsNotNone(ExternalAuthMap.objects.get(user=inactive_user)) self.assertTrue(inactive_user.is_active) self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, inactive_user) self.assertEqual(response['Location'], '/dashboard') # verify logging: self.assertEquals(len(audit_log_calls), 3) self._assert_shib_login_is_logged(audit_log_calls[0], log_user_string) method_name, args, _kwargs = audit_log_calls[2] self.assertEquals(method_name, 'info') self.assertEquals(len(args), 1) self.assertIn(u'Login success', args[0]) self.assertIn(log_user_string, args[0]) @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") @patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': True, 'SQUELCH_PII_IN_LOGS': False}) def test_extauth_auto_activate_user_with_flag_no_squelch(self): """ Wrapper to run base_test_extauth_auto_activate_user_with_flag with {'SQUELCH_PII_IN_LOGS': False} """ self._base_test_extauth_auto_activate_user_with_flag(log_user_string="inactive@stanford.edu") @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") @patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': True, 'SQUELCH_PII_IN_LOGS': True}) def test_extauth_auto_activate_user_with_flag_squelch(self): """ Wrapper to run base_test_extauth_auto_activate_user_with_flag with {'SQUELCH_PII_IN_LOGS': True} """ self._base_test_extauth_auto_activate_user_with_flag(log_user_string="user.id: 1") @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") @data(*gen_all_identities()) def test_registration_form(self, identity): """ Tests the registration form showing up with the proper parameters. Uses django test client for its session support """ client = DjangoTestClient() # identity k/v pairs will show up in request.META response = client.get(path='/shib-login/', data={}, follow=False, **identity) self.assertEquals(response.status_code, 200) mail_input_HTML = '