Commit 4d5a2380 by Jason Bau

external_auth: handle request.META values as str, not unicode

parent 48bdeeea
...@@ -37,12 +37,13 @@ TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, ...@@ -37,12 +37,13 @@ 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 # 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 # For the sake of python convention we'll make all of these variable names ALL_CAPS
IDP = u'https://idp.stanford.edu/' # These values would all returned from request.META, so they need to be str, not unicode
REMOTE_USER = u'test_user@stanford.edu' IDP = 'https://idp.stanford.edu/'
MAILS = [None, u'', u'test_user@stanford.edu'] REMOTE_USER = 'test_user@stanford.edu'
DISPLAYNAMES = [None, u'', u'Jason \u5305'] MAILS = [None, '', 'test_user@stanford.edu'] # unicode shouldn't be in emails, would fail django's email validator
GIVENNAMES = [None, u'', u'jas\xf6n; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' DISPLAYNAMES = [None, '', 'Jason 包']
SNS = [None, u'', u'\u5305; smith'] # At Stanford, the sns can be a list delimited by ';' 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(): def gen_all_identities():
...@@ -108,6 +109,7 @@ class ShibSPTest(ModuleStoreTestCase): ...@@ -108,6 +109,7 @@ class ShibSPTest(ModuleStoreTestCase):
def _assert_shib_login_is_logged(self, audit_log_call, remote_user): def _assert_shib_login_is_logged(self, audit_log_call, remote_user):
"""Asserts that shibboleth login attempt is being logged""" """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 method_name, args, _kwargs = audit_log_call
self.assertEquals(method_name, 'info') self.assertEquals(method_name, 'info')
self.assertEquals(len(args), 1) self.assertEquals(len(args), 1)
...@@ -312,12 +314,13 @@ class ShibSPTest(ModuleStoreTestCase): ...@@ -312,12 +314,13 @@ class ShibSPTest(ModuleStoreTestCase):
client = DjangoTestClient() client = DjangoTestClient()
response1 = client.get(path='/shib-login/', data={}, follow=False, **identity) response1 = client.get(path='/shib-login/', data={}, follow=False, **identity)
# Then we have the user answer the registration form # Then we have the user answer the registration form
postvars = {'email': 'post_email@stanford.edu', # These are unicode because request.POST returns unicode
'username': 'post_username', postvars = {'email': u'post_email@stanford.edu',
'password': 'post_password', 'username': u'post_username', # django usernames can't be unicode
'name': 'post_name', 'password': u'post_pássword',
'terms_of_service': 'true', 'name': u'post_náme',
'honor_code': 'true'} 'terms_of_service': u'true',
'honor_code': u'true'}
# use RequestFactory instead of TestClient here because we want access to request.user # use RequestFactory instead of TestClient here because we want access to request.user
request2 = self.request_factory.post('/create_account', data=postvars) request2 = self.request_factory.post('/create_account', data=postvars)
request2.session = client.session request2.session = client.session
...@@ -374,7 +377,7 @@ class ShibSPTest(ModuleStoreTestCase): ...@@ -374,7 +377,7 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertNotIn(u';', profile.name) self.assertNotIn(u';', profile.name)
else: else:
self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name) self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name)
self.assertEqual(profile.name, identity.get('displayName')) self.assertEqual(profile.name, identity.get('displayName').decode('utf-8'))
# clean up for next loop # clean up for next loop
request2.session['ExternalAuthMap'].delete() request2.session['ExternalAuthMap'].delete()
...@@ -596,9 +599,9 @@ class ShibUtilFnTest(TestCase): ...@@ -596,9 +599,9 @@ class ShibUtilFnTest(TestCase):
DIACRITIC = u"àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸåÅçÇ" # pylint: disable=C0103 DIACRITIC = u"àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸåÅçÇ" # pylint: disable=C0103
STR_DIACRI = "àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸåÅçÇ" # pylint: disable=C0103 STR_DIACRI = "àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸåÅçÇ" # pylint: disable=C0103
FLATTENED = u"aeiouAEIOUaeiouyAEIOUYaeiouAEIOUanoANOaeiouyAEIOUYaAcC" # pylint: disable=C0103 FLATTENED = u"aeiouAEIOUaeiouyAEIOUYaeiouAEIOUanoANOaeiouyAEIOUYaAcC" # pylint: disable=C0103
self.assertEqual(_flatten_to_ascii(u'jas\xf6n'), u'jason') # umlaut self.assertEqual(_flatten_to_ascii('jasön'), 'jason') # umlaut
self.assertEqual(_flatten_to_ascii(u'Jason\u5305'), u'Jason') # mandarin, so it just gets dropped self.assertEqual(_flatten_to_ascii('Jason包'), 'Jason') # mandarin, so it just gets dropped
self.assertEqual(_flatten_to_ascii(u'abc'), u'abc') # pass through self.assertEqual(_flatten_to_ascii('abc'), 'abc') # pass through
unicode_test = _flatten_to_ascii(DIACRITIC) unicode_test = _flatten_to_ascii(DIACRITIC)
self.assertEqual(unicode_test, FLATTENED) self.assertEqual(unicode_test, FLATTENED)
......
...@@ -137,7 +137,7 @@ def _external_login_or_signup(request, ...@@ -137,7 +137,7 @@ def _external_login_or_signup(request,
try: try:
eamap = ExternalAuthMap.objects.get(external_id=external_id, eamap = ExternalAuthMap.objects.get(external_id=external_id,
external_domain=external_domain) external_domain=external_domain)
log.debug('Found eamap=%s', eamap) log.debug(u'Found eamap=%s', eamap)
except ExternalAuthMap.DoesNotExist: except ExternalAuthMap.DoesNotExist:
# go render form for creating edX user # go render form for creating edX user
eamap = ExternalAuthMap(external_id=external_id, eamap = ExternalAuthMap(external_id=external_id,
...@@ -146,7 +146,7 @@ def _external_login_or_signup(request, ...@@ -146,7 +146,7 @@ def _external_login_or_signup(request,
eamap.external_email = email eamap.external_email = email
eamap.external_name = fullname eamap.external_name = fullname
eamap.internal_password = generate_password() eamap.internal_password = generate_password()
log.debug('Created eamap=%s', eamap) log.debug(u'Created eamap=%s', eamap)
eamap.save() eamap.save()
log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname) log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname)
...@@ -166,7 +166,7 @@ def _external_login_or_signup(request, ...@@ -166,7 +166,7 @@ def _external_login_or_signup(request,
eamap.user = link_user eamap.user = link_user
eamap.save() eamap.save()
internal_user = link_user internal_user = link_user
log.info('SHIB: Linking existing account for %s', eamap.external_id) log.info(u'SHIB: Linking existing account for %s', eamap.external_id)
# now pass through to log in # now pass through to log in
else: else:
# otherwise, there must have been an error, b/c we've already linked a user with these external # otherwise, there must have been an error, b/c we've already linked a user with these external
...@@ -177,10 +177,10 @@ def _external_login_or_signup(request, ...@@ -177,10 +177,10 @@ def _external_login_or_signup(request,
% getattr(settings, 'TECH_SUPPORT_EMAIL', 'techsupport@class.stanford.edu'))) % getattr(settings, 'TECH_SUPPORT_EMAIL', 'techsupport@class.stanford.edu')))
return default_render_failure(request, failure_msg) return default_render_failure(request, failure_msg)
except User.DoesNotExist: except User.DoesNotExist:
log.info('SHIB: No user for %s yet, doing signup', eamap.external_email) log.info(u'SHIB: No user for %s yet, doing signup', eamap.external_email)
return _signup(request, eamap, retfun) return _signup(request, eamap, retfun)
else: else:
log.info('No user for %s yet. doing signup', eamap.external_email) log.info(u'No user for %s yet. doing signup', eamap.external_email)
return _signup(request, eamap, retfun) return _signup(request, eamap, retfun)
# We trust shib's authentication, so no need to authenticate using the password again # We trust shib's authentication, so no need to authenticate using the password again
...@@ -194,25 +194,25 @@ def _external_login_or_signup(request, ...@@ -194,25 +194,25 @@ def _external_login_or_signup(request,
auth_backend = 'django.contrib.auth.backends.ModelBackend' auth_backend = 'django.contrib.auth.backends.ModelBackend'
user.backend = auth_backend user.backend = auth_backend
if settings.FEATURES['SQUELCH_PII_IN_LOGS']: if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
AUDIT_LOG.info('Linked user.id: {0} logged in via Shibboleth'.format(user.id)) AUDIT_LOG.info(u'Linked user.id: {0} logged in via Shibboleth'.format(user.id))
else: else:
AUDIT_LOG.info('Linked user "{0}" logged in via Shibboleth'.format(user.email)) AUDIT_LOG.info(u'Linked user "{0}" logged in via Shibboleth'.format(user.email))
elif uses_certs: elif uses_certs:
# Certificates are trusted, so just link the user and log the action # Certificates are trusted, so just link the user and log the action
user = internal_user user = internal_user
user.backend = 'django.contrib.auth.backends.ModelBackend' user.backend = 'django.contrib.auth.backends.ModelBackend'
if settings.FEATURES['SQUELCH_PII_IN_LOGS']: if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
AUDIT_LOG.info('Linked user_id {0} logged in via SSL certificate'.format(user.id)) AUDIT_LOG.info(u'Linked user_id {0} logged in via SSL certificate'.format(user.id))
else: else:
AUDIT_LOG.info('Linked user "{0}" logged in via SSL certificate'.format(user.email)) AUDIT_LOG.info(u'Linked user "{0}" logged in via SSL certificate'.format(user.email))
else: else:
user = authenticate(username=uname, password=eamap.internal_password, request=request) user = authenticate(username=uname, password=eamap.internal_password, request=request)
if user is None: if user is None:
# we want to log the failure, but don't want to log the password attempted: # we want to log the failure, but don't want to log the password attempted:
if settings.FEATURES['SQUELCH_PII_IN_LOGS']: if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
AUDIT_LOG.warning('External Auth Login failed') AUDIT_LOG.warning(u'External Auth Login failed')
else: else:
AUDIT_LOG.warning('External Auth Login failed for "{0}"'.format(uname)) AUDIT_LOG.warning(u'External Auth Login failed for "{0}"'.format(uname))
return _signup(request, eamap, retfun) return _signup(request, eamap, retfun)
if not user.is_active: if not user.is_active:
...@@ -222,14 +222,14 @@ def _external_login_or_signup(request, ...@@ -222,14 +222,14 @@ def _external_login_or_signup(request,
user.is_active = True user.is_active = True
user.save() user.save()
if settings.FEATURES['SQUELCH_PII_IN_LOGS']: if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
AUDIT_LOG.info('Activating user {0} due to external auth'.format(user.id)) AUDIT_LOG.info(u'Activating user {0} due to external auth'.format(user.id))
else: else:
AUDIT_LOG.info('Activating user "{0}" due to external auth'.format(uname)) AUDIT_LOG.info(u'Activating user "{0}" due to external auth'.format(uname))
else: else:
if settings.FEATURES['SQUELCH_PII_IN_LOGS']: if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
AUDIT_LOG.warning('User {0} is not active after external login'.format(user.id)) AUDIT_LOG.warning(u'User {0} is not active after external login'.format(user.id))
else: else:
AUDIT_LOG.warning('User "{0}" is not active after external login'.format(uname)) AUDIT_LOG.warning(u'User "{0}" is not active after external login'.format(uname))
# TODO: improve error page # TODO: improve error page
msg = 'Account not yet activated: please look for link in your email' msg = 'Account not yet activated: please look for link in your email'
return default_render_failure(request, msg) return default_render_failure(request, msg)
...@@ -246,9 +246,9 @@ def _external_login_or_signup(request, ...@@ -246,9 +246,9 @@ def _external_login_or_signup(request,
else: else:
student.views.try_change_enrollment(request) student.views.try_change_enrollment(request)
if settings.FEATURES['SQUELCH_PII_IN_LOGS']: if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
AUDIT_LOG.info("Login success - user.id: {0}".format(user.id)) AUDIT_LOG.info(u"Login success - user.id: {0}".format(user.id))
else: else:
AUDIT_LOG.info("Login success - {0} ({1})".format(user.username, user.email)) AUDIT_LOG.info(u"Login success - {0} ({1})".format(user.username, user.email))
if retfun is None: if retfun is None:
return redirect('/') return redirect('/')
return retfun() return retfun()
...@@ -292,7 +292,7 @@ def _signup(request, eamap, retfun=None): ...@@ -292,7 +292,7 @@ def _signup(request, eamap, retfun=None):
post_vars = dict(username=username, post_vars = dict(username=username,
honor_code=u'true', honor_code=u'true',
terms_of_service=u'true') terms_of_service=u'true')
log.info('doing immediate signup for %s, params=%s', username, post_vars) log.info(u'doing immediate signup for %s, params=%s', username, post_vars)
student.views.create_account(request, post_vars) student.views.create_account(request, post_vars)
# should check return content for successful completion before # should check return content for successful completion before
if retfun is not None: if retfun is not None:
...@@ -331,7 +331,7 @@ def _signup(request, eamap, retfun=None): ...@@ -331,7 +331,7 @@ def _signup(request, eamap, retfun=None):
except ValidationError: except ValidationError:
context['ask_for_email'] = True context['ask_for_email'] = True
log.info('EXTAUTH: Doing signup for %s', eamap.external_id) log.info(u'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)
...@@ -508,14 +508,14 @@ def shib_login(request): ...@@ -508,14 +508,14 @@ def shib_login(request):
""")) """))
if not request.META.get('REMOTE_USER'): if not request.META.get('REMOTE_USER'):
log.error("SHIB: no REMOTE_USER found in request.META") log.error(u"SHIB: no REMOTE_USER found in request.META")
return default_render_failure(request, shib_error_msg) return default_render_failure(request, shib_error_msg)
elif not request.META.get('Shib-Identity-Provider'): elif not request.META.get('Shib-Identity-Provider'):
log.error("SHIB: no Shib-Identity-Provider in request.META") log.error(u"SHIB: no Shib-Identity-Provider in request.META")
return default_render_failure(request, shib_error_msg) return default_render_failure(request, shib_error_msg)
else: else:
# If we get here, the user has authenticated properly # If we get here, the user has authenticated properly
shib = {attr: request.META.get(attr, '') shib = {attr: request.META.get(attr, '').decode('utf-8')
for attr in ['REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider', 'displayName']} for attr in ['REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider', 'displayName']}
# Clean up first name, last name, and email address # Clean up first name, last name, and email address
...@@ -525,7 +525,7 @@ def shib_login(request): ...@@ -525,7 +525,7 @@ def shib_login(request):
shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize() shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize()
# TODO: should we be logging creds here, at info level? # TODO: should we be logging creds here, at info level?
log.info("SHIB creds returned: %r", shib) log.info(u"SHIB creds returned: %r", shib)
fullname = shib['displayName'] if shib['displayName'] else u'%s %s' % (shib['givenName'], shib['sn']) fullname = shib['displayName'] if shib['displayName'] else u'%s %s' % (shib['givenName'], shib['sn'])
......
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