Merge pull request #20295 from edx/mroytman/EDUCATOR-4229-lms-redirection-idp

add LMS endpoint to redirect to identity provider
This commit is contained in:
Michael Roytman
2019-04-25 14:13:06 -04:00
committed by GitHub
5 changed files with 117 additions and 22 deletions

View File

@@ -0,0 +1,30 @@
"""
Unit tests for third_party_auth SAML auth providers
"""
import mock
from third_party_auth.tests.testutil import SAMLTestCase
from third_party_auth.saml import EdXSAMLIdentityProvider, get_saml_idp_class
from third_party_auth.tests.data.saml_identity_provider_mock_data import mock_conf, mock_attributes,\
expected_user_details
class TestEdXSAMLIdentityProvider(SAMLTestCase):
"""
Test EdXSAMLIdentityProvider.
"""
@mock.patch('third_party_auth.saml.log')
def test_get_saml_idp_class_with_fake_identifier(self, log_mock):
error_mock = log_mock.error
idp_class = get_saml_idp_class('fake_idp_class_option')
error_mock.assert_called_once_with(
u'%s is not a valid EdXSAMLIdentityProvider subclass; using EdXSAMLIdentityProvider base class.',
'fake_idp_class_option'
)
self.assertIs(idp_class, EdXSAMLIdentityProvider)
def test_get_user_details(self):
""" test get_attr and get_user_details of EdXSAMLIdentityProvider"""
edx_saml_identity_provider = EdXSAMLIdentityProvider('demo', **mock_conf)
self.assertEqual(edx_saml_identity_provider.get_user_details(mock_attributes), expected_user_details)

View File

@@ -6,9 +6,11 @@ import unittest
import ddt
from django.conf import settings
from django.urls import reverse
from lxml import etree
from onelogin.saml2.errors import OneLogin_Saml2_Error
from third_party_auth import pipeline
# Define some XML namespaces:
from third_party_auth.tasks import SAML_XML_NS
@@ -53,8 +55,8 @@ class SAMLMetadataTest(SAMLTestCase):
self.enable_saml(
other_config_str=(
'{'
'"TECHNICAL_CONTACT": {"givenName": "Jane Tech", "emailAddress": "jane@example.com"},' # pylint: disable=unicode-format-string,line-too-long
'"SUPPORT_CONTACT": {"givenName": "Joe Support", "emailAddress": "joe@example.com"}' # pylint: disable=unicode-format-string,line-too-long
'"TECHNICAL_CONTACT": {"givenName": "Jane Tech", "emailAddress": "jane@example.com"},' # pylint: disable=unicode-format-string
'"SUPPORT_CONTACT": {"givenName": "Joe Support", "emailAddress": "joe@example.com"}' # pylint: disable=unicode-format-string
'}'
)
)
@@ -153,3 +155,42 @@ class SAMLAuthTest(SAMLTestCase):
self.enable_saml(enabled=False)
response = self.client.get(self.LOGIN_URL)
self.assertEqual(response.status_code, 404)
@unittest.skipUnless(AUTH_FEATURE_ENABLED, AUTH_FEATURES_KEY + ' not enabled')
class IdPRedirectViewTest(SAMLTestCase):
"""
Test IdPRedirectView.
"""
def setUp(self):
super(IdPRedirectViewTest, self).setUp()
self.enable_saml()
self.configure_saml_provider(
name="Test",
slug="test",
enabled=True,
)
def test_with_valid_provider_slug(self):
endpoint_url = self.get_idp_redirect_url('saml-test')
expected_url = pipeline.get_login_url('saml-test', pipeline.AUTH_ENTRY_LOGIN, reverse('dashboard'))
response = self.client.get(endpoint_url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)
def test_with_invalid_provider_slug(self):
endpoint_url = self.get_idp_redirect_url('saml-test-invalid')
response = self.client.get(endpoint_url)
self.assertEqual(response.status_code, 404)
@staticmethod
def get_idp_redirect_url(provider_slug, next_destination=None):
return '{idp_redirect_url}?{next_destination}'.format(
idp_redirect_url=reverse('idp_redirect', kwargs={'provider_slug': provider_slug}),
next_destination=next_destination,
)

View File

@@ -25,9 +25,6 @@ from third_party_auth.models import (
SAMLConfiguration,
SAMLProviderConfig
)
from third_party_auth.saml import EdXSAMLIdentityProvider, get_saml_idp_class
from third_party_auth.tests.data.saml_identity_provider_mock_data import mock_conf, mock_attributes,\
expected_user_details
AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH'
AUTH_FEATURE_ENABLED = AUTH_FEATURES_KEY in settings.FEATURES
@@ -217,21 +214,6 @@ class SAMLTestCase(TestCase):
kwargs.setdefault('entity_id', "https://saml.example.none")
super(SAMLTestCase, self).enable_saml(**kwargs)
@mock.patch('third_party_auth.saml.log')
def test_get_saml_idp_class_with_fake_identifier(self, log_mock):
error_mock = log_mock.error
idp_class = get_saml_idp_class('fake_idp_class_option')
error_mock.assert_called_once_with(
u'%s is not a valid EdXSAMLIdentityProvider subclass; using EdXSAMLIdentityProvider base class.',
'fake_idp_class_option'
)
self.assertIs(idp_class, EdXSAMLIdentityProvider)
def test_get_user_details(self):
""" test get_attr and get_user_details of EdXSAMLIdentityProvider"""
edx_smal_identity_provider = EdXSAMLIdentityProvider('demo', **mock_conf)
self.assertEqual(edx_smal_identity_provider.get_user_details(mock_attributes), expected_user_details)
@contextmanager
def simulate_running_pipeline(pipeline_target, backend, email=None, fullname=None, username=None, **kwargs):

View File

@@ -2,12 +2,14 @@
from django.conf.urls import include, url
from .views import inactive_user_view, lti_login_and_complete_view, post_to_custom_auth_form, saml_metadata_view
from .views import (inactive_user_view, lti_login_and_complete_view,
post_to_custom_auth_form, saml_metadata_view, IdPRedirectView)
urlpatterns = [
url(r'^auth/inactive', inactive_user_view, name="third_party_inactive_redirect"),
url(r'^auth/custom_auth_entry', post_to_custom_auth_form, name='tpa_post_to_custom_auth_form'),
url(r'^auth/saml/metadata.xml', saml_metadata_view),
url(r'^auth/login/(?P<backend>lti)/$', lti_login_and_complete_view),
url(r'^auth/idp_redirect/(?P<provider_slug>[\w-]+)', IdPRedirectView.as_view(), name="idp_redirect"),
url(r'^auth/', include('social_django.urls', namespace='social')),
]

View File

@@ -3,13 +3,15 @@ Extra views required for SSO
"""
from django.conf import settings
from django.urls import reverse
from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseServerError
from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseServerError, HttpResponseNotFound
from django.shortcuts import redirect, render
from django.views.generic.base import View
from django.views.decorators.csrf import csrf_exempt
from social_django.utils import load_strategy, load_backend, psa
from social_django.views import complete
from social_core.utils import setting_name
from student.helpers import get_next_url_for_login_page
from student.models import UserProfile
from student.views import compose_and_send_activation_email
import third_party_auth
@@ -110,3 +112,41 @@ def post_to_custom_auth_form(request):
'hmac': pipeline_data['hmac'],
}
return render(request, 'third_party_auth/post_custom_auth_entry.html', data)
class IdPRedirectView(View):
"""
Redirect to an IdP's login page if the IdP exists; otherwise, return a 404.
Example usage:
GET auth/idp_redirect/saml-default
"""
def get(self, request, *args, **kwargs):
"""
Return either a redirect to the login page of an identity provider that
corresponds to the provider_slug keyword argument or a 404 if the
provider_slug does not correspond to an identity provider.
Args:
request (HttpRequest)
Keyword Args:
provider_slug (str): a slug corresponding to a configured identity provider
Returns:
HttpResponse: 302 to a provider's login url if the provider_slug kwarg matches an identity provider
HttpResponse: 404 if the provider_slug kwarg does not match an identity provider
"""
# this gets the url to redirect to after login/registration/third_party_auth
# it also handles checking the safety of the redirect url (next query parameter)
# it checks against settings.LOGIN_REDIRECT_WHITELIST, so be sure to add the url
# to this setting
next_destination_url = get_next_url_for_login_page(request)
try:
url = pipeline.get_login_url(kwargs['provider_slug'], pipeline.AUTH_ENTRY_LOGIN, next_destination_url)
return redirect(url)
except ValueError:
return HttpResponseNotFound()