Merge pull request #16919 from edx/zub/ENT-768-display-friendly-error-message-for-saml
ENT-768 Display friendly error message for SAML rejection
This commit is contained in:
@@ -279,7 +279,8 @@ def simulate_running_pipeline(pipeline_target, backend, email=None, fullname=Non
|
||||
pipeline_data = {
|
||||
"backend": backend,
|
||||
"kwargs": {
|
||||
"details": kwargs
|
||||
"details": kwargs,
|
||||
"response": kwargs.get("response", {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,17 @@ import pytest
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core import mail
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpRequest
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from edx_oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory, RefreshTokenFactory
|
||||
from edx_rest_api_client import exceptions
|
||||
from http.cookies import SimpleCookie
|
||||
@@ -33,6 +37,7 @@ from course_modes.models import CourseMode
|
||||
from lms.djangoapps.commerce.models import CommerceConfiguration
|
||||
from lms.djangoapps.commerce.tests import factories
|
||||
from lms.djangoapps.commerce.tests.mocks import mock_get_orders
|
||||
from lms.djangoapps.student_account.views import login_and_registration_form
|
||||
from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
@@ -40,6 +45,7 @@ from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme_context
|
||||
from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from student_account.views import account_settings_context, get_user_orders
|
||||
@@ -452,6 +458,111 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
|
||||
expected_ec
|
||||
)
|
||||
|
||||
def _configure_testshib_provider(self, provider_name, idp_slug):
|
||||
"""
|
||||
Enable and configure the TestShib SAML IdP as a third_party_auth provider.
|
||||
"""
|
||||
kwargs = {}
|
||||
kwargs.setdefault('name', provider_name)
|
||||
kwargs.setdefault('enabled', True)
|
||||
kwargs.setdefault('visible', True)
|
||||
kwargs.setdefault('idp_slug', idp_slug)
|
||||
kwargs.setdefault('entity_id', 'https://idp.testshib.org/idp/shibboleth')
|
||||
kwargs.setdefault('metadata_source', 'https://mock.testshib.org/metadata/testshib-providers.xml')
|
||||
kwargs.setdefault('icon_class', 'fa-university')
|
||||
kwargs.setdefault('attr_email', 'dummy-email-attr')
|
||||
kwargs.setdefault('max_session_length', None)
|
||||
self.configure_saml_provider(**kwargs)
|
||||
|
||||
@mock.patch('django.conf.settings.MESSAGE_STORAGE', 'django.contrib.messages.storage.cookie.CookieStorage')
|
||||
@mock.patch('lms.djangoapps.student_account.views.enterprise_customer_for_request')
|
||||
@ddt.data(
|
||||
(
|
||||
'signin_user',
|
||||
'tpa-saml',
|
||||
'TestShib',
|
||||
{
|
||||
'name': 'FakeName',
|
||||
'logo': 'https://host.com/logo.jpg',
|
||||
'welcome_msg': 'No message'
|
||||
}
|
||||
)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_saml_auth_with_error(
|
||||
self,
|
||||
url_name,
|
||||
current_backend,
|
||||
current_provider,
|
||||
expected_enterprise_customer_mock_attrs,
|
||||
enterprise_customer_mock,
|
||||
):
|
||||
params = []
|
||||
request = RequestFactory().get(reverse(url_name), params, HTTP_ACCEPT='text/html')
|
||||
SessionMiddleware().process_request(request)
|
||||
request.user = AnonymousUser()
|
||||
|
||||
self.enable_saml()
|
||||
dummy_idp = 'testshib'
|
||||
self._configure_testshib_provider(current_provider, dummy_idp)
|
||||
expected_ec = mock.MagicMock(
|
||||
branding_configuration=mock.MagicMock(
|
||||
logo=mock.MagicMock(
|
||||
url=expected_enterprise_customer_mock_attrs['logo']
|
||||
),
|
||||
welcome_message=expected_enterprise_customer_mock_attrs['welcome_msg']
|
||||
)
|
||||
)
|
||||
expected_ec.name = expected_enterprise_customer_mock_attrs['name']
|
||||
enterprise_customer_data = {
|
||||
'uuid': '72416e52-8c77-4860-9584-15e5b06220fb',
|
||||
'name': 'Dummy Enterprise',
|
||||
'identity_provider': dummy_idp,
|
||||
}
|
||||
enterprise_customer_mock.return_value = enterprise_customer_data
|
||||
dummy_error_message = 'Authentication failed: SAML login failed ' \
|
||||
'["invalid_response"] [SAML Response must contain 1 assertion]'
|
||||
|
||||
# Add error message for error in auth pipeline
|
||||
MessageMiddleware().process_request(request)
|
||||
messages.error(request, dummy_error_message, extra_tags='social-auth')
|
||||
|
||||
# Simulate a running pipeline
|
||||
pipeline_response = {
|
||||
'response': {
|
||||
'idp_name': dummy_idp
|
||||
}
|
||||
}
|
||||
pipeline_target = 'student_account.views.third_party_auth.pipeline'
|
||||
with simulate_running_pipeline(pipeline_target, current_backend, **pipeline_response):
|
||||
with mock.patch('edxmako.request_context.get_current_request', return_value=request):
|
||||
response = login_and_registration_form(request)
|
||||
|
||||
expected_error_message = Text(_(
|
||||
u'We are sorry, you are not authorized to access {platform_name} via this channel. '
|
||||
u'Please contact your {enterprise} administrator in order to access {platform_name} '
|
||||
u'or contact {edx_support_link}.{line_break}'
|
||||
u'{line_break}'
|
||||
u'Error Details:{line_break}{error_message}')
|
||||
).format(
|
||||
platform_name=settings.PLATFORM_NAME,
|
||||
enterprise=enterprise_customer_data['name'],
|
||||
error_message=dummy_error_message,
|
||||
edx_support_link=HTML(
|
||||
'<a href="{edx_support_url}">{support_url_name}</a>'
|
||||
).format(
|
||||
edx_support_url=settings.SUPPORT_SITE_LINK,
|
||||
support_url_name=_('edX Support'),
|
||||
),
|
||||
line_break=HTML('<br/>')
|
||||
)
|
||||
self._assert_saml_auth_data_with_error(
|
||||
response,
|
||||
current_backend,
|
||||
current_provider,
|
||||
expected_error_message
|
||||
)
|
||||
|
||||
def test_hinted_login(self):
|
||||
params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")]
|
||||
response = self.client.get(reverse('signin_user'), params, HTTP_ACCEPT="text/html")
|
||||
@@ -650,7 +761,32 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
|
||||
expected_data = '"third_party_auth": {auth_info}'.format(
|
||||
auth_info=auth_info
|
||||
)
|
||||
self.assertContains(response, expected_data)
|
||||
|
||||
def _assert_saml_auth_data_with_error(
|
||||
self, response, current_backend, current_provider, expected_error_message
|
||||
):
|
||||
"""
|
||||
Verify that third party auth info is rendered correctly in a DOM data attribute.
|
||||
"""
|
||||
finish_auth_url = None
|
||||
if current_backend:
|
||||
finish_auth_url = reverse('social:complete', kwargs={'backend': current_backend}) + '?'
|
||||
|
||||
auth_info = {
|
||||
'currentProvider': current_provider,
|
||||
'providers': [],
|
||||
'secondaryProviders': [],
|
||||
'finishAuthUrl': finish_auth_url,
|
||||
'errorMessage': expected_error_message,
|
||||
'registerFormSubmitButtonText': 'Create Account',
|
||||
'syncLearnerProfileData': False,
|
||||
}
|
||||
auth_info = dump_js_escaped_json(auth_info)
|
||||
|
||||
expected_data = '"third_party_auth": {auth_info}'.format(
|
||||
auth_info=auth_info
|
||||
)
|
||||
self.assertContains(response, expected_data)
|
||||
|
||||
def _third_party_login_url(self, backend_name, auth_entry, login_params):
|
||||
|
||||
@@ -41,6 +41,7 @@ from openedx.core.lib.edx_api_utils import get_edx_api_data
|
||||
from openedx.core.lib.time_zone_utils import TIME_ZONE_CHOICES
|
||||
from openedx.features.enterprise_support.api import enterprise_customer_for_request, get_enterprise_learner_data
|
||||
from student.cookies import set_experiments_is_enterprise_cookie
|
||||
from openedx.features.enterprise_support.utils import update_third_party_auth_context_for_enterprise
|
||||
from student.helpers import destroy_oauth_tokens, get_next_url_for_login_page
|
||||
from student.models import UserProfile
|
||||
from student.views import register_user as old_register_view
|
||||
@@ -388,6 +389,8 @@ def _third_party_auth_context(request, redirect_to, tpa_hint=None):
|
||||
context['errorMessage'] = _(unicode(msg)) # pylint: disable=translation-of-non-string
|
||||
break
|
||||
|
||||
context = update_third_party_auth_context_for_enterprise(context, enterprise_customer)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ from __future__ import unicode_literals
|
||||
import hashlib
|
||||
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
|
||||
|
||||
def get_cache_key(**kwargs):
|
||||
@@ -27,3 +31,40 @@ def get_cache_key(**kwargs):
|
||||
key = '__'.join(['{}:{}'.format(item, value) for item, value in six.iteritems(kwargs)])
|
||||
|
||||
return hashlib.md5(key).hexdigest()
|
||||
|
||||
|
||||
def update_third_party_auth_context_for_enterprise(context, enterprise_customer=None):
|
||||
"""
|
||||
Return updated context of third party auth with modified for enterprise.
|
||||
|
||||
Arguments:
|
||||
context (dict): Context for third party auth providers and auth pipeline.
|
||||
enterprise_customer (dict): data for enterprise customer
|
||||
|
||||
Returns:
|
||||
context (dict): Updated context of third party auth with modified
|
||||
`errorMessage`.
|
||||
"""
|
||||
if enterprise_customer and context['errorMessage']:
|
||||
context['errorMessage'] = Text(_(
|
||||
u'We are sorry, you are not authorized to access {platform_name} via this channel. '
|
||||
u'Please contact your {enterprise} administrator in order to access {platform_name} '
|
||||
u'or contact {edx_support_link}.{line_break}'
|
||||
u'{line_break}'
|
||||
u'Error Details:{line_break}{error_message}')
|
||||
).format(
|
||||
platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
|
||||
enterprise=enterprise_customer['name'],
|
||||
error_message=context['errorMessage'],
|
||||
edx_support_link=HTML(
|
||||
'<a href="{edx_support_url}">{support_url_name}</a>'
|
||||
).format(
|
||||
edx_support_url=configuration_helpers.get_value(
|
||||
'SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK
|
||||
),
|
||||
support_url_name=_('edX Support'),
|
||||
),
|
||||
line_break=HTML('<br/>')
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
Reference in New Issue
Block a user