Commit 98ee25fb by George Song Committed by GitHub

Merge pull request #15587 from open-craft/jill/tpa-hint-setting

Adds settings.FEATURES['THIRD_PARTY_AUTH_HINT']
parents 36e4c801 84c6c5ac
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import logging import logging
import mimetypes import mimetypes
import urllib import urllib
import urlparse
from datetime import datetime from datetime import datetime
from django.conf import settings from django.conf import settings
...@@ -16,6 +17,7 @@ from pytz import UTC ...@@ -16,6 +17,7 @@ from pytz import UTC
import third_party_auth import third_party_auth
from course_modes.models import CourseMode from course_modes.models import CourseMode
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers import get_themes from openedx.core.djangoapps.theming.helpers import get_themes
# Enumeration of per-course verification statuses # Enumeration of per-course verification statuses
...@@ -240,6 +242,8 @@ def get_next_url_for_login_page(request): ...@@ -240,6 +242,8 @@ def get_next_url_for_login_page(request):
Otherwise, we go to the ?next= query param or to the dashboard if nothing else is Otherwise, we go to the ?next= query param or to the dashboard if nothing else is
specified. specified.
If THIRD_PARTY_AUTH_HINT is set, then `tpa_hint=<hint>` is added as a query parameter.
""" """
redirect_to = get_redirect_to(request) redirect_to = get_redirect_to(request)
if not redirect_to: if not redirect_to:
...@@ -247,6 +251,7 @@ def get_next_url_for_login_page(request): ...@@ -247,6 +251,7 @@ def get_next_url_for_login_page(request):
redirect_to = reverse('dashboard') redirect_to = reverse('dashboard')
except NoReverseMatch: except NoReverseMatch:
redirect_to = reverse('home') redirect_to = reverse('home')
if any(param in request.GET for param in POST_AUTH_PARAMS): if any(param in request.GET for param in POST_AUTH_PARAMS):
# Before we redirect to next/dashboard, we need to handle auto-enrollment: # Before we redirect to next/dashboard, we need to handle auto-enrollment:
params = [(param, request.GET[param]) for param in POST_AUTH_PARAMS if param in request.GET] params = [(param, request.GET[param]) for param in POST_AUTH_PARAMS if param in request.GET]
...@@ -255,6 +260,23 @@ def get_next_url_for_login_page(request): ...@@ -255,6 +260,23 @@ def get_next_url_for_login_page(request):
# Note: if we are resuming a third party auth pipeline, then the next URL will already # Note: if we are resuming a third party auth pipeline, then the next URL will already
# be saved in the session as part of the pipeline state. That URL will take priority # be saved in the session as part of the pipeline state. That URL will take priority
# over this one. # over this one.
# Append a tpa_hint query parameter, if one is configured
tpa_hint = configuration_helpers.get_value(
"THIRD_PARTY_AUTH_HINT",
settings.FEATURES.get("THIRD_PARTY_AUTH_HINT", '')
)
if tpa_hint:
# Don't add tpa_hint if we're already in the TPA pipeline (prevent infinite loop),
# and don't overwrite any existing tpa_hint params (allow tpa_hint override).
running_pipeline = third_party_auth.pipeline.get(request)
(scheme, netloc, path, query, fragment) = list(urlparse.urlsplit(redirect_to))
if not running_pipeline and 'tpa_hint' not in query:
params = urlparse.parse_qs(query)
params['tpa_hint'] = [tpa_hint]
query = urllib.urlencode(params, doseq=True)
redirect_to = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
return redirect_to return redirect_to
......
...@@ -4,12 +4,16 @@ import logging ...@@ -4,12 +4,16 @@ import logging
import ddt import ddt
from django.conf import settings from django.conf import settings
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.test.utils import override_settings
from mock import patch
from testfixtures import LogCapture from testfixtures import LogCapture
from student.helpers import get_next_url_for_login_page from student.helpers import get_next_url_for_login_page
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
LOGGER_NAME = "student.helpers" LOGGER_NAME = "student.helpers"
...@@ -23,6 +27,13 @@ class TestLoginHelper(TestCase): ...@@ -23,6 +27,13 @@ class TestLoginHelper(TestCase):
super(TestLoginHelper, self).setUp() super(TestLoginHelper, self).setUp()
self.request = RequestFactory() self.request = RequestFactory()
@staticmethod
def _add_session(request):
"""Annotate the request object with a session"""
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
@ddt.data( @ddt.data(
("https://www.amazon.com", "text/html", None, ("https://www.amazon.com", "text/html", None,
"Unsafe redirect parameter detected after login page: u'https://www.amazon.com'"), "Unsafe redirect parameter detected after login page: u'https://www.amazon.com'"),
...@@ -56,3 +67,38 @@ class TestLoginHelper(TestCase): ...@@ -56,3 +67,38 @@ class TestLoginHelper(TestCase):
req.META["HTTP_ACCEPT"] = "text/html" # pylint: disable=no-member req.META["HTTP_ACCEPT"] = "text/html" # pylint: disable=no-member
next_page = get_next_url_for_login_page(req) next_page = get_next_url_for_login_page(req)
self.assertEqual(next_page, u'/dashboard') self.assertEqual(next_page, u'/dashboard')
@patch('student.helpers.third_party_auth.pipeline.get')
@ddt.data(
# Test requests outside the TPA pipeline - tpa_hint should be added.
(None, '/dashboard', '/dashboard', False),
('', '/dashboard', '/dashboard', False),
('', '/dashboard?tpa_hint=oa2-google-oauth2', '/dashboard?tpa_hint=oa2-google-oauth2', False),
('saml-idp', '/dashboard', '/dashboard?tpa_hint=saml-idp', False),
# THIRD_PARTY_AUTH_HINT can be overridden via the query string
('saml-idp', '/dashboard?tpa_hint=oa2-google-oauth2', '/dashboard?tpa_hint=oa2-google-oauth2', False),
# Test requests inside the TPA pipeline - tpa_hint should not be added, preventing infinite loop.
(None, '/dashboard', '/dashboard', True),
('', '/dashboard', '/dashboard', True),
('', '/dashboard?tpa_hint=oa2-google-oauth2', '/dashboard?tpa_hint=oa2-google-oauth2', True),
('saml-idp', '/dashboard', '/dashboard', True),
# OK to leave tpa_hint overrides in place.
('saml-idp', '/dashboard?tpa_hint=oa2-google-oauth2', '/dashboard?tpa_hint=oa2-google-oauth2', True),
)
@ddt.unpack
def test_third_party_auth_hint(self, tpa_hint, next_url, expected_url, running_pipeline, mock_running_pipeline):
mock_running_pipeline.return_value = running_pipeline
def validate_login():
req = self.request.get(reverse("login") + "?next={url}".format(url=next_url))
req.META["HTTP_ACCEPT"] = "text/html" # pylint: disable=no-member
self._add_session(req)
next_page = get_next_url_for_login_page(req)
self.assertEqual(next_page, expected_url)
with override_settings(FEATURES=dict(settings.FEATURES, THIRD_PARTY_AUTH_HINT=tpa_hint)):
validate_login()
with with_site_configuration_context(configuration=dict(THIRD_PARTY_AUTH_HINT=tpa_hint)):
validate_login()
...@@ -458,15 +458,63 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi ...@@ -458,15 +458,63 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
response = self.client.get(reverse('signin_user'), params, HTTP_ACCEPT="text/html") response = self.client.get(reverse('signin_user'), params, HTTP_ACCEPT="text/html")
self.assertNotIn(response.content, tpa_hint) self.assertNotIn(response.content, tpa_hint)
def test_hinted_login_dialog_disabled(self): @ddt.data(
('signin_user', 'login'),
('register_user', 'register'),
)
@ddt.unpack
def test_hinted_login_dialog_disabled(self, url_name, auth_entry):
"""Test that the dialog doesn't show up for hinted logins when disabled. """ """Test that the dialog doesn't show up for hinted logins when disabled. """
self.google_provider.skip_hinted_login_dialog = True self.google_provider.skip_hinted_login_dialog = True
self.google_provider.save() self.google_provider.save()
params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")] params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")]
response = self.client.get(reverse('signin_user'), params, HTTP_ACCEPT="text/html") response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html")
self.assertRedirects(
response,
'auth/login/google-oauth2/?auth_entry={}&next=%2Fcourses%2Fsomething%2F%3Ftpa_hint%3Doa2-google-oauth2'.format(auth_entry),
target_status_code=302
)
@override_settings(FEATURES=dict(settings.FEATURES, THIRD_PARTY_AUTH_HINT='oa2-google-oauth2'))
@ddt.data(
'signin_user',
'register_user',
)
def test_settings_tpa_hinted_login(self, url_name):
"""
Ensure that settings.FEATURES['THIRD_PARTY_AUTH_HINT'] can set third_party_auth_hint.
"""
params = [("next", "/courses/something/")]
response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html")
self.assertContains(response, '"third_party_auth_hint": "oa2-google-oauth2"')
# THIRD_PARTY_AUTH_HINT can be overridden via the query string
tpa_hint = self.hidden_enabled_provider.provider_id
params = [("next", "/courses/something/?tpa_hint={0}".format(tpa_hint))]
response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html")
self.assertContains(response, '"third_party_auth_hint": "{0}"'.format(tpa_hint))
# Even disabled providers in the query string will override THIRD_PARTY_AUTH_HINT
tpa_hint = self.hidden_disabled_provider.provider_id
params = [("next", "/courses/something/?tpa_hint={0}".format(tpa_hint))]
response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html")
self.assertNotIn(response.content, tpa_hint)
@override_settings(FEATURES=dict(settings.FEATURES, THIRD_PARTY_AUTH_HINT='oa2-google-oauth2'))
@ddt.data(
('signin_user', 'login'),
('register_user', 'register'),
)
@ddt.unpack
def test_settings_tpa_hinted_login_dialog_disabled(self, url_name, auth_entry):
"""Test that the dialog doesn't show up for hinted logins when disabled via settings.THIRD_PARTY_AUTH_HINT. """
self.google_provider.skip_hinted_login_dialog = True
self.google_provider.save()
params = [("next", "/courses/something/")]
response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html")
self.assertRedirects( self.assertRedirects(
response, response,
'auth/login/google-oauth2/?auth_entry=login&next=%2Fcourses%2Fsomething%2F%3Ftpa_hint%3Doa2-google-oauth2', 'auth/login/google-oauth2/?auth_entry={}&next=%2Fcourses%2Fsomething%2F%3Ftpa_hint%3Doa2-google-oauth2'.format(auth_entry),
target_status_code=302 target_status_code=302
) )
......
...@@ -86,13 +86,17 @@ def login_and_registration_form(request, initial_mode="login"): ...@@ -86,13 +86,17 @@ def login_and_registration_form(request, initial_mode="login"):
if tpa_hint_provider.skip_hinted_login_dialog: if tpa_hint_provider.skip_hinted_login_dialog:
# Forward the user directly to the provider's login URL when the provider is configured # Forward the user directly to the provider's login URL when the provider is configured
# to skip the dialog. # to skip the dialog.
if initial_mode == "register":
auth_entry = pipeline.AUTH_ENTRY_REGISTER
else:
auth_entry = pipeline.AUTH_ENTRY_LOGIN
return redirect( return redirect(
pipeline.get_login_url(provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to) pipeline.get_login_url(provider_id, auth_entry, redirect_url=redirect_to)
) )
third_party_auth_hint = provider_id third_party_auth_hint = provider_id
initial_mode = "hinted_login" initial_mode = "hinted_login"
except (KeyError, ValueError, IndexError): except (KeyError, ValueError, IndexError) as ex:
pass log.error("Unknown tpa_hint provider: %s", ex)
# If this is a themed site, revert to the old login/registration pages. # If this is a themed site, revert to the old login/registration pages.
# We need to do this for now to support existing themes. # We need to do this for now to support existing themes.
......
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