From f56931f4f84f97a6354e4e22ad666723a33abc18 Mon Sep 17 00:00:00 2001 From: Artur Gaspar Date: Mon, 16 Oct 2023 17:36:33 -0300 Subject: [PATCH] feat: redirect to custom URL when third-party auth account is unlinked --- .../djangoapps/user_authn/views/login_form.py | 23 ++++++- .../views/tests/test_logistration.py | 67 ++++++++++++++----- 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/openedx/core/djangoapps/user_authn/views/login_form.py b/openedx/core/djangoapps/user_authn/views/login_form.py index bb78a9df1a3c..7416d1022314 100644 --- a/openedx/core/djangoapps/user_authn/views/login_form.py +++ b/openedx/core/djangoapps/user_authn/views/login_form.py @@ -35,7 +35,7 @@ update_logistration_context_for_enterprise ) from common.djangoapps.student.helpers import get_next_url_for_login_page -from common.djangoapps.third_party_auth import pipeline +from common.djangoapps.third_party_auth import pipeline, provider from common.djangoapps.third_party_auth.decorators import xframe_allow_whitelisted from common.djangoapps.util.password_policy_validators import DEFAULT_MAX_PASSWORD_LENGTH @@ -126,6 +126,19 @@ def get_login_session_form(request): return form_desc +def _get_unlinked_provision_url(current_provider): + try: + unlinked_provision_url = current_provider.get_setting( + 'unlinked_account_provision_url' + ) + except (AttributeError, KeyError): + # Not all provider subclasses implement get_setting(). + pass + else: + if isinstance(unlinked_provision_url, str): + return unlinked_provision_url + + @require_http_methods(['GET']) @ratelimit( key='openedx.core.djangoapps.util.ratelimit.real_ip', @@ -196,6 +209,14 @@ def login_and_registration_form(request, initial_mode="login"): saml_provider = False running_pipeline = pipeline.get(request) if running_pipeline: + # Redirect to provisioning URL if there is a current third-party + # authentication provider but the user is not authenticated. + current_provider = provider.Registry.get_from_pipeline(running_pipeline) + if current_provider: + unlinked_provision_url = _get_unlinked_provision_url(current_provider) + if unlinked_provision_url: + return redirect(unlinked_provision_url) + saml_provider, __ = third_party_auth.utils.is_saml_provider( running_pipeline.get('backend'), running_pipeline.get('kwargs') ) diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py index 3220cd513974..1c88f5b59d06 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py @@ -193,16 +193,17 @@ def test_third_party_auth_disabled(self, url_name): response = self.client.get(reverse(url_name)) self._assert_third_party_auth_data(response, None, None, [], None) + @mock.patch('common.djangoapps.third_party_auth.models.OAuth2ProviderConfig.get_setting') @mock.patch('openedx.core.djangoapps.user_authn.views.login_form.enterprise_customer_for_request') @ddt.data( - ("signin_user", None, None, None, False), - ("register_user", None, None, None, False), - ("signin_user", "google-oauth2", "Google", None, False), - ("register_user", "google-oauth2", "Google", None, False), - ("signin_user", "facebook", "Facebook", None, False), - ("register_user", "facebook", "Facebook", None, False), - ("signin_user", "dummy", "Dummy", None, False), - ("register_user", "dummy", "Dummy", None, False), + ("signin_user", None, None, None, False, None), + ("register_user", None, None, None, False, None), + ("signin_user", "google-oauth2", "Google", None, False, None), + ("register_user", "google-oauth2", "Google", None, False, None), + ("signin_user", "facebook", "Facebook", None, False, None), + ("register_user", "facebook", "Facebook", None, False, None), + ("signin_user", "dummy", "Dummy", None, False, None), + ("register_user", "dummy", "Dummy", None, False, None), ( "signin_user", "google-oauth2", @@ -212,7 +213,24 @@ def test_third_party_auth_disabled(self, url_name): 'logo': 'https://host.com/logo.jpg', 'welcome_msg': 'No message' }, - True + True, + None + ), + ( + "signin_user", + "google-oauth2", + "Google", + None, + False, + 'http://example.com' + ), + ( + "signin_user", + "google-oauth2", + "Google", + None, + False, + 123 # Test invalid configuration. ) ) @ddt.unpack @@ -223,7 +241,9 @@ def test_third_party_auth( current_provider, expected_enterprise_customer_mock_attrs, add_user_details, + unlinked_account_provision_url, enterprise_customer_mock, + get_setting_mock ): params = [ ('course_id', 'course-v1:Org+Course+Run'), @@ -249,6 +269,12 @@ def test_third_party_auth( email = 'test@test.com' enterprise_customer_mock.return_value = expected_ec + if unlinked_account_provision_url is not None: + other_settings = { + 'unlinked_account_provision_url': unlinked_account_provision_url + } + get_setting_mock.side_effect = other_settings.__getitem__ + # Simulate a running pipeline if current_backend is not None: pipeline_target = "openedx.core.djangoapps.user_authn.views.login_form.third_party_auth.pipeline" @@ -292,14 +318,21 @@ def test_third_party_auth( "registerUrl": self._third_party_login_url("google-oauth2", "register", params) }, ] - self._assert_third_party_auth_data( - response, - current_backend, - current_provider, - expected_providers, - expected_ec, - add_user_details - ) + if unlinked_account_provision_url and isinstance(unlinked_account_provision_url, str): + self.assertRedirects( + response, + unlinked_account_provision_url, + fetch_redirect_response=False + ) + else: + self._assert_third_party_auth_data( + response, + current_backend, + current_provider, + expected_providers, + expected_ec, + add_user_details + ) def _configure_testshib_provider(self, provider_name, idp_slug): """