def test_enterprise_customer_for_request_with_session(self): """ Verify enterprise_customer_for_request stores and retrieves data from session appropriately """ dummy_request = mock.MagicMock(session={}, user=self.user) enterprise_data = {'name': 'dummy-enterprise-customer', 'uuid': '8dc65e66-27c9-447b-87ff-ede6d66e3a5d'} # Verify enterprise customer data fetched from API when it is not available in session with mock.patch( 'openedx.features.enterprise_support.api.enterprise_customer_from_api', return_value=enterprise_data ): self.assertEqual(dummy_request.session.get('enterprise_customer'), None) enterprise_customer = enterprise_customer_for_request(dummy_request) self.assertEqual(enterprise_customer, enterprise_data) self.assertEqual(dummy_request.session.get('enterprise_customer'), enterprise_data) # Verify enterprise customer data fetched from session for subsequent calls with mock.patch( 'openedx.features.enterprise_support.api.enterprise_customer_from_api', return_value=enterprise_data ) as mock_enterprise_customer_from_api, mock.patch( 'openedx.features.enterprise_support.api.enterprise_customer_from_cache', return_value=enterprise_data ) as mock_enterprise_customer_from_cache: enterprise_customer = enterprise_customer_for_request(dummy_request) self.assertEqual(enterprise_customer, enterprise_data) self.assertEqual(mock_enterprise_customer_from_api.called, False) self.assertEqual(mock_enterprise_customer_from_cache.called, True)
def test_enterprise_customer_for_request( self, mock_registry, mock_partial, mock_enterprise_customer_model, ): def mock_get_enterprise_customer(**kwargs): uuid = kwargs.get( 'enterprise_customer_identity_provider__provider_id') if uuid: return mock.MagicMock(uuid=uuid, user=self.user) raise Exception dummy_request = mock.MagicMock(session={}, user=self.user) mock_enterprise_customer_model.objects.get.side_effect = mock_get_enterprise_customer mock_enterprise_customer_model.DoesNotExist = Exception mock_partial.return_value = True mock_registry.get_from_pipeline.return_value.provider_id = 'real-ent-uuid' # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user. self.mock_get_enterprise_customer('real-ent-uuid', {'real': 'enterprisecustomer'}, 200) enterprise_customer = enterprise_customer_for_request(dummy_request) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) httpretty.reset() # Verify that the method `enterprise_customer_for_request` returns no # enterprise customer if the enterprise customer API throws 404. self.mock_get_enterprise_customer('real-ent-uuid', {'detail': 'Not found.'}, 404) enterprise_customer = enterprise_customer_for_request(dummy_request) self.assertIsNone(enterprise_customer) httpretty.reset() # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user even if # the third-party auth pipeline has no `provider_id`. mock_registry.get_from_pipeline.return_value.provider_id = None self.mock_get_enterprise_customer('real-ent-uuid', {'real': 'enterprisecustomer'}, 200) enterprise_customer = enterprise_customer_for_request( mock.MagicMock(GET={'enterprise_customer': 'real-ent-uuid'}, user=self.user)) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user even if # the third-party auth pipeline has no `provider_id` but there is # enterprise customer UUID in the cookie. enterprise_customer = enterprise_customer_for_request( mock.MagicMock(GET={}, COOKIES={ settings.ENTERPRISE_CUSTOMER_COOKIE_NAME: 'real-ent-uuid' }, user=self.user)) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'})
def test_enterprise_customer_for_request( self, mock_registry, mock_partial, mock_ec_model, mock_get_el_data ): def mock_get_ec(**kwargs): uuid = kwargs.get('enterprise_customer_identity_provider__provider_id') if uuid: return mock.MagicMock(uuid=uuid) raise Exception mock_ec_model.objects.get.side_effect = mock_get_ec mock_ec_model.DoesNotExist = Exception mock_partial.return_value = True mock_registry.get_from_pipeline.return_value.provider_id = 'real-ent-uuid' self.mock_get_enterprise_customer('real-ent-uuid', {"real": "enterprisecustomer"}, 200) ec = enterprise_customer_for_request(mock.MagicMock()) self.assertEqual(ec, {"real": "enterprisecustomer"}) httpretty.reset() self.mock_get_enterprise_customer('real-ent-uuid', {"detail": "Not found."}, 404) ec = enterprise_customer_for_request(mock.MagicMock()) self.assertIsNone(ec) mock_registry.get_from_pipeline.return_value.provider_id = None httpretty.reset() self.mock_get_enterprise_customer('real-ent-uuid', {"real": "enterprisecustomer"}, 200) ec = enterprise_customer_for_request(mock.MagicMock(GET={"enterprise_customer": 'real-ent-uuid'})) self.assertEqual(ec, {"real": "enterprisecustomer"}) ec = enterprise_customer_for_request( mock.MagicMock(GET={}, COOKIES={settings.ENTERPRISE_CUSTOMER_COOKIE_NAME: 'real-ent-uuid'}) ) self.assertEqual(ec, {"real": "enterprisecustomer"}) mock_get_el_data.return_value = [{'enterprise_customer': {'uuid': 'real-ent-uuid'}}] ec = enterprise_customer_for_request( mock.MagicMock(GET={}, COOKIES={}, user=mock.MagicMock(is_authenticated=lambda: True), site=1) ) self.assertEqual(ec, {"real": "enterprisecustomer"})
def _apply_third_party_auth_overrides(request, form_desc): """Modify the login form if the user has authenticated with a third-party provider. If a user has successfully authenticated with a third-party provider, and an email is associated with it then we fill in the email field with readonly property. Arguments: request (HttpRequest): The request for the registration form, used to determine if the user has successfully authenticated with a third-party provider. form_desc (FormDescription): The registration form description """ if third_party_auth.is_enabled(): running_pipeline = third_party_auth.pipeline.get(request) if running_pipeline: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) if current_provider and enterprise_customer_for_request(request): pipeline_kwargs = running_pipeline.get('kwargs') # Details about the user sent back from the provider. details = pipeline_kwargs.get('details') email = details.get('email', '') # override the email field. form_desc.override_field_properties( "email", default=email, restrictions={"readonly": "readonly"} if email else { "min_length": accounts.EMAIL_MIN_LENGTH, "max_length": accounts.EMAIL_MAX_LENGTH, } )
def get(self, request): # lint-amnesty, pylint: disable=missing-function-docstring if not configuration_helpers.get_value('CONTACT_US_PAGE', True): raise Http404 context = { 'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME), 'support_email': configuration_helpers.get_value('CONTACT_EMAIL', settings.CONTACT_EMAIL), 'custom_fields': settings.ZENDESK_CUSTOM_FIELDS } # Tag all issues with LMS to distinguish channel which received the request tags = ['LMS'] # Per edX support, we would like to be able to route feedback items by site via tagging current_site_name = configuration_helpers.get_value("SITE_NAME") if current_site_name: current_site_name = current_site_name.replace(".", "_") tags.append(f"site_name_{current_site_name}") if request.user.is_authenticated: context['course_id'] = request.session.get('course_id', '') context['user_enrollments'] = CourseEnrollment.enrollments_for_user_with_overviews_preload(request.user) enterprise_customer = enterprise_api.enterprise_customer_for_request(request) if enterprise_customer: tags.append('enterprise_learner') context['tags'] = tags return render_to_response("support/contact_us.html", context)
def process_request(self, request): """ Fill the request with Enterprise-related content. """ if 'enterprise_customer' not in request.session and request.user.is_authenticated: request.session[ 'enterprise_customer'] = api.enterprise_customer_for_request( request)
def test_enterprise_customer_for_request_with_session(self): """ Verify enterprise_customer_for_request stores and retrieves data from session appropriately """ dummy_request = mock.MagicMock(session={}, user=self.user) enterprise_data = {'name': 'dummy-enterprise-customer', 'uuid': '8dc65e66-27c9-447b-87ff-ede6d66e3a5d'} # Verify enterprise customer data fetched from API when it is not available in session with mock.patch( 'openedx.features.enterprise_support.api.enterprise_customer_from_api', return_value=enterprise_data ): assert dummy_request.session.get('enterprise_customer') is None enterprise_customer = enterprise_customer_for_request(dummy_request) assert enterprise_customer == enterprise_data assert dummy_request.session.get('enterprise_customer') == enterprise_data # Verify enterprise customer data fetched from session for subsequent calls with mock.patch( 'openedx.features.enterprise_support.api.enterprise_customer_from_api', return_value=enterprise_data ) as mock_enterprise_customer_from_api, mock.patch( 'openedx.features.enterprise_support.api.enterprise_customer_from_session', return_value=enterprise_data ) as mock_enterprise_customer_from_session: enterprise_customer = enterprise_customer_for_request(dummy_request) assert enterprise_customer == enterprise_data assert mock_enterprise_customer_from_api.called is False assert mock_enterprise_customer_from_session.called is True # Verify enterprise customer data fetched from session for subsequent calls # with unauthenticated user in SAML case del dummy_request.user with mock.patch( 'openedx.features.enterprise_support.api.enterprise_customer_from_api', return_value=enterprise_data ) as mock_enterprise_customer_from_api, mock.patch( 'openedx.features.enterprise_support.api.enterprise_customer_from_session', return_value=enterprise_data ) as mock_enterprise_customer_from_session: enterprise_customer = enterprise_customer_for_request(dummy_request) assert enterprise_customer == enterprise_data assert mock_enterprise_customer_from_api.called is False assert mock_enterprise_customer_from_session.called is True
def get_enterprise_learner_generic_name(request): """ Get a generic name concatenating the Enterprise Customer name and 'Learner'. ENT-924: Temporary solution for hiding potentially sensitive SSO names. When a more complete solution is put in place, delete this function and all of its uses. """ # Prevent a circular import. This function makes sense to be in this module though. And see function description. from openedx.features.enterprise_support.api import enterprise_customer_for_request enterprise_customer = enterprise_customer_for_request(request) return (enterprise_customer['name'] + 'Learner' if enterprise_customer and enterprise_customer['replace_sensitive_sso_username'] else '')
def get_enterprise_learner_generic_name(request): """ Get a generic name concatenating the Enterprise Customer name and 'Learner'. ENT-924: Temporary solution for hiding potentially sensitive SSO names. When a more complete solution is put in place, delete this function and all of its uses. """ # Prevent a circular import. This function makes sense to be in this module though. And see function description. from openedx.features.enterprise_support.api import enterprise_customer_for_request enterprise_customer = enterprise_customer_for_request(request) return ( enterprise_customer['name'] + 'Learner' if enterprise_customer and enterprise_customer['replace_sensitive_sso_username'] else '' )
def enterprise_sidebar_context(request): """ Given the current request, render the HTML of a sidebar for the current logistration view that depicts Enterprise-related information. """ enterprise_customer = enterprise_customer_for_request(request) if not enterprise_customer: return {} platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) if enterprise_customer.branding_configuration.logo: enterprise_logo_url = enterprise_customer.branding_configuration.logo.url else: enterprise_logo_url = '' if getattr(enterprise_customer.branding_configuration, 'welcome_message', None): branded_welcome_template = enterprise_customer.branding_configuration.welcome_message else: branded_welcome_template = configuration_helpers.get_value( 'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE', settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE) branded_welcome_string = branded_welcome_template.format( start_bold=u'<b>', end_bold=u'</b>', enterprise_name=enterprise_customer.name, platform_name=platform_name) platform_welcome_template = configuration_helpers.get_value( 'ENTERPRISE_PLATFORM_WELCOME_TEMPLATE', settings.ENTERPRISE_PLATFORM_WELCOME_TEMPLATE) platform_welcome_string = platform_welcome_template.format( platform_name=platform_name) context = { 'enterprise_name': enterprise_customer.name, 'enterprise_logo_url': enterprise_logo_url, 'enterprise_branded_welcome_string': branded_welcome_string, 'platform_welcome_string': platform_welcome_string, } return context
def enterprise_sidebar_context(request): """ Given the current request, render the HTML of a sidebar for the current logistration view that depicts Enterprise-related information. """ enterprise_customer = enterprise_customer_for_request(request) if not enterprise_customer: return {} platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) if enterprise_customer.branding_configuration.logo: enterprise_logo_url = enterprise_customer.branding_configuration.logo.url else: enterprise_logo_url = '' if getattr(enterprise_customer.branding_configuration, 'welcome_message', None): branded_welcome_template = enterprise_customer.branding_configuration.welcome_message else: branded_welcome_template = configuration_helpers.get_value( 'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE', settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE ) branded_welcome_string = branded_welcome_template.format( start_bold=u'<b>', end_bold=u'</b>', enterprise_name=enterprise_customer.name, platform_name=platform_name ) platform_welcome_template = configuration_helpers.get_value( 'ENTERPRISE_PLATFORM_WELCOME_TEMPLATE', settings.ENTERPRISE_PLATFORM_WELCOME_TEMPLATE ) platform_welcome_string = platform_welcome_template.format(platform_name=platform_name) context = { 'enterprise_name': enterprise_customer.name, 'enterprise_logo_url': enterprise_logo_url, 'enterprise_branded_welcome_string': branded_welcome_string, 'platform_welcome_string': platform_welcome_string, } return context
def get_enterprise_readonly_account_fields(user): """ Returns a set of account fields that are read-only for enterprise users. """ # TODO circular dependency between enterprise_support.api and enterprise_support.utils from openedx.features.enterprise_support.api import enterprise_customer_for_request enterprise_customer = enterprise_customer_for_request(get_current_request()) enterprise_readonly_account_fields = list(settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS) # if user has no `UserSocialAuth` record then allow to edit `fullname` # whether the `sync_learner_profile_data` is enabled or disabled user_social_auth_record = _user_has_social_auth_record(user, enterprise_customer) if not user_social_auth_record: enterprise_readonly_account_fields.remove('name') sync_learner_profile_data = _get_sync_learner_profile_data(enterprise_customer) return set(enterprise_readonly_account_fields) if sync_learner_profile_data else set()
def enterprise_sidebar_context(request): """ Given the current request, render the HTML of a sidebar for the current logistration view that depicts Enterprise-related information. """ enterprise_customer = enterprise_customer_for_request(request) if not enterprise_customer: return {} platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) branding_configuration = enterprise_customer.get('branding_configuration', {}) logo_url = branding_configuration.get('logo', '') if isinstance( branding_configuration, dict) else '' branded_welcome_template = configuration_helpers.get_value( 'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE', settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE) branded_welcome_string = branded_welcome_template.format( start_bold=u'<b>', end_bold=u'</b>', enterprise_name=enterprise_customer['name'], platform_name=platform_name) platform_welcome_template = configuration_helpers.get_value( 'ENTERPRISE_PLATFORM_WELCOME_TEMPLATE', settings.ENTERPRISE_PLATFORM_WELCOME_TEMPLATE) platform_welcome_string = platform_welcome_template.format( platform_name=platform_name) context = { 'enterprise_name': enterprise_customer['name'], 'enterprise_logo_url': logo_url, 'enterprise_branded_welcome_string': branded_welcome_string, 'platform_welcome_string': platform_welcome_string, } return context
def test_enterprise_customer_for_request(self, ec_class_mock, get_ec_pipeline_mock): """ Test that the correct EnterpriseCustomer, if any, is returned. """ def get_ec_mock(**kwargs): by_provider_id_kw = 'enterprise_customer_identity_provider__provider_id' provider_id = kwargs.get(by_provider_id_kw, '') uuid = kwargs.get('uuid', '') if uuid == 'real-uuid' or provider_id == 'real-provider-id': return 'this-is-actually-an-enterprise-customer' elif uuid == 'not-a-uuid': raise ValueError else: raise Exception ec_class_mock.DoesNotExist = Exception ec_class_mock.objects.get.side_effect = get_ec_mock get_ec_pipeline_mock.return_value = None request = mock.MagicMock() request.GET.get.return_value = 'real-uuid' self.assertEqual(enterprise_customer_for_request(request), 'this-is-actually-an-enterprise-customer') request.GET.get.return_value = 'not-a-uuid' self.assertEqual(enterprise_customer_for_request(request), None) request.GET.get.return_value = 'fake-uuid' self.assertEqual(enterprise_customer_for_request(request), None) request.GET.get.return_value = None self.assertEqual( enterprise_customer_for_request(request, tpa_hint='real-provider-id'), 'this-is-actually-an-enterprise-customer') self.assertEqual( enterprise_customer_for_request(request, tpa_hint='fake-provider-id'), None) self.assertEqual( enterprise_customer_for_request(request, tpa_hint=None), None) get_ec_pipeline_mock.return_value = 'also-a-real-enterprise' self.assertEqual(enterprise_customer_for_request(request), 'also-a-real-enterprise')
def enterprise_sidebar_context(request): """ Given the current request, render the HTML of a sidebar for the current logistration view that depicts Enterprise-related information. """ enterprise_customer = enterprise_customer_for_request(request) if not enterprise_customer: return {} platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) branding_configuration = enterprise_customer.get('branding_configuration', {}) logo_url = branding_configuration.get('logo', '') if isinstance(branding_configuration, dict) else '' branded_welcome_template = configuration_helpers.get_value( 'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE', settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE ) branded_welcome_string = branded_welcome_template.format( start_bold=u'<b>', end_bold=u'</b>', enterprise_name=enterprise_customer['name'], platform_name=platform_name ) platform_welcome_template = configuration_helpers.get_value( 'ENTERPRISE_PLATFORM_WELCOME_TEMPLATE', settings.ENTERPRISE_PLATFORM_WELCOME_TEMPLATE ) platform_welcome_string = platform_welcome_template.format(platform_name=platform_name) context = { 'enterprise_name': enterprise_customer['name'], 'enterprise_logo_url': logo_url, 'enterprise_branded_welcome_string': branded_welcome_string, 'platform_welcome_string': platform_welcome_string, } return context
def test_enterprise_customer_for_request(self, ec_class_mock, get_ec_pipeline_mock): """ Test that the correct EnterpriseCustomer, if any, is returned. """ def get_ec_mock(**kwargs): by_provider_id_kw = 'enterprise_customer_identity_provider__provider_id' provider_id = kwargs.get(by_provider_id_kw, '') uuid = kwargs.get('uuid', '') if uuid == 'real-uuid' or provider_id == 'real-provider-id': return 'this-is-actually-an-enterprise-customer' elif uuid == 'not-a-uuid': raise ValueError else: raise Exception ec_class_mock.DoesNotExist = Exception ec_class_mock.objects.get.side_effect = get_ec_mock get_ec_pipeline_mock.return_value = None request = mock.MagicMock() request.GET.get.return_value = 'real-uuid' self.assertEqual(enterprise_customer_for_request(request), 'this-is-actually-an-enterprise-customer') request.GET.get.return_value = 'not-a-uuid' self.assertEqual(enterprise_customer_for_request(request), None) request.GET.get.return_value = 'fake-uuid' self.assertEqual(enterprise_customer_for_request(request), None) request.GET.get.return_value = None self.assertEqual( enterprise_customer_for_request(request, tpa_hint='real-provider-id'), 'this-is-actually-an-enterprise-customer' ) self.assertEqual(enterprise_customer_for_request(request, tpa_hint='fake-provider-id'), None) self.assertEqual(enterprise_customer_for_request(request, tpa_hint=None), None) get_ec_pipeline_mock.return_value = 'also-a-real-enterprise' self.assertEqual(enterprise_customer_for_request(request), 'also-a-real-enterprise')
def login_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard # Note: We check for the existence of login-related cookies in addition to is_authenticated # since Django's SessionAuthentication middleware auto-updates session cookies but not # the other login-related cookies. See ARCH-282. if request.user.is_authenticated and are_logged_in_cookies_set(request): return redirect(redirect_to) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: try: next_args = six.moves.urllib.parse.parse_qs(six.moves.urllib.parse.urlparse(redirect_to).query) provider_id = next_args['tpa_hint'][0] tpa_hint_provider = third_party_auth.provider.Registry.get(provider_id=provider_id) if tpa_hint_provider: if tpa_hint_provider.skip_hinted_login_dialog: # Forward the user directly to the provider's login URL when the provider is configured # to skip the dialog. if initial_mode == "register": auth_entry = pipeline.AUTH_ENTRY_REGISTER else: auth_entry = pipeline.AUTH_ENTRY_LOGIN return redirect( pipeline.get_login_url(provider_id, auth_entry, redirect_url=redirect_to) ) third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError) as ex: log.exception(u"Unknown tpa_hint provider: %s", ex) # We are defaulting to true because all themes should now be using the newer page. if is_request_in_themed_site() and not configuration_helpers.get_value('ENABLE_COMBINED_LOGIN_REGISTRATION', True): if initial_mode == "login": return old_login_view(request) elif initial_mode == "register": return old_register_view(request) # Account activation message account_activation_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-activation' in message.tags ] account_recovery_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-recovery' in message.tags ] # Otherwise, render the combined login/registration page context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': _third_party_auth_context(request, redirect_to, third_party_auth_hint), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK, 'account_activation_messages': account_activation_messages, 'account_recovery_messages': account_recovery_messages, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), 'account_creation_allowed': configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)), 'is_account_recovery_feature_enabled': is_secondary_email_feature_enabled() }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'combined_login_and_register': True, 'disable_footer': not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER', settings.FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER'] ), } enterprise_customer = enterprise_customer_for_request(request) update_logistration_context_for_enterprise(request, context, enterprise_customer) response = render_to_response('student_account/login_and_register.html', context) handle_enterprise_cookies_for_logistration(request, response, context) return response
def _apply_third_party_auth_overrides(self, request, form_desc): """Modify the registration form if the user has authenticated with a third-party provider. If a user has successfully authenticated with a third-party provider, but does not yet have an account with EdX, we want to fill in the registration form with any info that we get from the provider. This will also hide the password field, since we assign users a default (random) password on the assumption that they will be using third-party auth to log in. Arguments: request (HttpRequest): The request for the registration form, used to determine if the user has successfully authenticated with a third-party provider. form_desc (FormDescription): The registration form description """ if third_party_auth.is_enabled(): running_pipeline = third_party_auth.pipeline.get(request) if running_pipeline: current_provider = third_party_auth.provider.Registry.get_from_pipeline( running_pipeline) if current_provider: # Override username / email / full name field_overrides = current_provider.get_register_form_data( running_pipeline.get('kwargs')) # When the TPA Provider is configured to skip the registration form and we are in an # enterprise context, we need to hide all fields except for terms of service and # ensure that the user explicitly checks that field. hide_registration_fields_except_tos = ( current_provider.skip_registration_form and enterprise_customer_for_request(request)) for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS: if field_name in field_overrides: form_desc.override_field_properties( field_name, default=field_overrides[field_name]) if (field_name not in ['terms_of_service', 'honor_code'] and field_overrides[field_name] and hide_registration_fields_except_tos): form_desc.override_field_properties( field_name, field_type="hidden", label="", instructions="", ) # Hide the password field form_desc.override_field_properties("password", default="", field_type="hidden", required=False, label="", instructions="", restrictions={}) # used to identify that request is running third party social auth form_desc.add_field( "social_auth_provider", field_type="hidden", label="", default=current_provider.name if current_provider.name else "Third Party", required=False, )
def test_enterprise_customer_for_request(self, mock_registry, mock_partial, mock_ec_model, mock_get_el_data): def mock_get_ec(**kwargs): uuid = kwargs.get( 'enterprise_customer_identity_provider__provider_id') if uuid: return mock.MagicMock(uuid=uuid) raise Exception mock_ec_model.objects.get.side_effect = mock_get_ec mock_ec_model.DoesNotExist = Exception mock_partial.return_value = True mock_registry.get_from_pipeline.return_value.provider_id = 'real-ent-uuid' self.mock_get_enterprise_customer('real-ent-uuid', {"real": "enterprisecustomer"}, 200) ec = enterprise_customer_for_request(mock.MagicMock()) self.assertEqual(ec, {"real": "enterprisecustomer"}) httpretty.reset() self.mock_get_enterprise_customer('real-ent-uuid', {"detail": "Not found."}, 404) ec = enterprise_customer_for_request(mock.MagicMock()) self.assertIsNone(ec) mock_registry.get_from_pipeline.return_value.provider_id = None httpretty.reset() self.mock_get_enterprise_customer('real-ent-uuid', {"real": "enterprisecustomer"}, 200) ec = enterprise_customer_for_request( mock.MagicMock(GET={"enterprise_customer": 'real-ent-uuid'})) self.assertEqual(ec, {"real": "enterprisecustomer"}) ec = enterprise_customer_for_request( mock.MagicMock(GET={}, COOKIES={ settings.ENTERPRISE_CUSTOMER_COOKIE_NAME: 'real-ent-uuid' })) self.assertEqual(ec, {"real": "enterprisecustomer"}) mock_get_el_data.return_value = [{ 'enterprise_customer': { 'uuid': 'real-ent-uuid' } }] ec = enterprise_customer_for_request( mock.MagicMock(GET={}, COOKIES={}, user=mock.MagicMock(is_authenticated=lambda: True), site=1)) self.assertEqual(ec, {"real": "enterprisecustomer"})
def login_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard if request.user.is_authenticated: return redirect(redirect_to) if settings.FEATURES.get("ENABLE_EDRAAK_LOGISTRATION", False) and settings.PROGS_URLS: redirect_to = get_next_url_for_progs_login_page(request, initial_mode) return redirect(redirect_to) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: try: next_args = urlparse.parse_qs(urlparse.urlparse(redirect_to).query) provider_id = next_args['tpa_hint'][0] tpa_hint_provider = third_party_auth.provider.Registry.get( provider_id=provider_id) if tpa_hint_provider: if tpa_hint_provider.skip_hinted_login_dialog: # Forward the user directly to the provider's login URL when the provider is configured # to skip the dialog. if initial_mode == "register": auth_entry = pipeline.AUTH_ENTRY_REGISTER else: auth_entry = pipeline.AUTH_ENTRY_LOGIN return redirect( pipeline.get_login_url(provider_id, auth_entry, redirect_url=redirect_to)) third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError) as ex: log.error("Unknown tpa_hint provider: %s", ex) # If this is a themed site, revert to the old login/registration pages. # We need to do this for now to support existing themes. # Themed sites can use the new logistration page by setting # 'ENABLE_COMBINED_LOGIN_REGISTRATION' in their # configuration settings. if is_request_in_themed_site() and not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION', False): if initial_mode == "login": return old_login_view(request) elif initial_mode == "register": return old_register_view(request) # Allow external auth to intercept and handle the request ext_auth_response = _external_auth_intercept(request, initial_mode) if ext_auth_response is not None: return ext_auth_response # Account activation message account_activation_messages = [{ 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-activation' in message.tags] # Otherwise, render the combined login/registration page context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': _third_party_auth_context(request, redirect_to, third_party_auth_hint), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK, 'account_activation_messages': account_activation_messages, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), 'account_creation_allowed': configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)) }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'combined_login_and_register': True, 'disable_footer': not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER', settings.FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER']), } enterprise_customer = enterprise_customer_for_request(request) update_logistration_context_for_enterprise(request, context, enterprise_customer) # Edraak: add (origin) option to the context if applicable edraak_update_origin( request, context) # Warning: This modifies `context['data']['origin']` response = render_to_response('student_account/login_and_register.html', context) handle_enterprise_cookies_for_logistration(request, response, context) return response
def _apply_third_party_auth_overrides(self, request, form_desc): """Modify the registration form if the user has authenticated with a third-party provider. If a user has successfully authenticated with a third-party provider, but does not yet have an account with EdX, we want to fill in the registration form with any info that we get from the provider. This will also hide the password field, since we assign users a default (random) password on the assumption that they will be using third-party auth to log in. Arguments: request (HttpRequest): The request for the registration form, used to determine if the user has successfully authenticated with a third-party provider. form_desc (FormDescription): The registration form description """ if third_party_auth.is_enabled(): running_pipeline = third_party_auth.pipeline.get(request) if running_pipeline: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) if current_provider: # Override username / email / full name field_overrides = current_provider.get_register_form_data( running_pipeline.get('kwargs') ) # When the TPA Provider is configured to skip the registration form and we are in an # enterprise context, we need to hide all fields except for terms of service and # ensure that the user explicitly checks that field. hide_registration_fields_except_tos = ( ( current_provider.skip_registration_form and enterprise_customer_for_request(request) ) or current_provider.sync_learner_profile_data ) for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS: if field_name in field_overrides: form_desc.override_field_properties( field_name, default=field_overrides[field_name] ) if (field_name not in ['terms_of_service', 'honor_code'] and field_overrides[field_name] and hide_registration_fields_except_tos): form_desc.override_field_properties( field_name, field_type="hidden", label="", instructions="", ) # Hide the password field form_desc.override_field_properties( "password", default="", field_type="hidden", required=False, label="", instructions="", restrictions={} ) # used to identify that request is running third party social auth form_desc.add_field( "social_auth_provider", field_type="hidden", label="", default=current_provider.name if current_provider.name else "Third Party", required=False, )
def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable=too-many-statements """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_client_ip(request)[0], url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) increment('track-selection.{}.{}'.format( enrollment_mode, 'active' if is_active else 'inactive')) increment('track-selection.views') if enrollment_mode is None: LOG.info( 'Rendering track selection for unenrolled user, referred by %s', request.META.get('HTTP_REFERER')) modes = CourseMode.modes_for_course_dict(course_key) ecommerce_service = EcommerceService() # We assume that, if 'professional' is one of the modes, it should be the *only* mode. # If there are both modes, default to 'no-id-professional'. has_enrolled_professional = ( CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode( modes) and not has_enrolled_professional: purchase_workflow = request.GET.get("purchase_workflow", "single") redirect_url = IDVerificationService.get_verify_location( course_id=course_key) if ecommerce_service.is_enabled(request.user): professional_mode = modes.get( CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get( CourseMode.PROFESSIONAL) if purchase_workflow == "single" and professional_mode.sku: redirect_url = ecommerce_service.get_checkout_page_url( professional_mode.sku) if purchase_workflow == "bulk" and professional_mode.bulk_sku: redirect_url = ecommerce_service.get_checkout_page_url( professional_mode.bulk_sku) return redirect(redirect_url) course = modulestore().get_course(course_key) # If there isn't a verified mode available, then there's nothing # to do on this page. Send the user to the dashboard. if not CourseMode.has_verified_mode(modes): return self._redirect_to_course_or_dashboard( course, course_key, request.user) # If a user has already paid, redirect them to the dashboard. if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): return self._redirect_to_course_or_dashboard( course, course_key, request.user) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(str(course_key), None) if CourseEnrollment.is_enrollment_closed(request.user, course): locale = to_locale(get_language()) enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale) params = six.moves.urllib.parse.urlencode( {'course_closed': enrollment_end_date}) return redirect('{}?{}'.format(reverse('dashboard'), params)) # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible # for university credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging # for the verified track. has_credit_upsell = any( CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False)) course_id = str(course_key) gated_content = ContentTypeGatingConfig.enabled_for_enrollment( user=request.user, course_key=course_key) context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id}), "modes": modes, "has_credit_upsell": has_credit_upsell, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "responsive": True, "nav_hidden": True, "content_gating_enabled": gated_content, "course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment( request.user, course), } context.update( get_experiment_user_metadata_context( course, request.user, )) title_content = '' if enrollment_mode: title_content = _( "Congratulations! You are now enrolled in {course_name}" ).format(course_name=course.display_name_with_default) context["title_content"] = title_content if "verified" in modes: verified_mode = modes["verified"] context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in verified_mode.suggested_prices.split(",") if x.strip() ] price_before_discount = verified_mode.min_price course_price = price_before_discount enterprise_customer = enterprise_customer_for_request(request) LOG.info( '[e-commerce calculate API] Going to hit the API for user [%s] linked to [%s] enterprise', request.user.username, enterprise_customer.get('name') if isinstance( enterprise_customer, dict) else None # Test Purpose ) if enterprise_customer and verified_mode.sku: course_price = get_course_final_price(request.user, verified_mode.sku, price_before_discount) context["currency"] = verified_mode.currency.upper() context["currency_symbol"] = get_currency_symbol( verified_mode.currency.upper()) context["min_price"] = course_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description # if course_price is equal to price_before_discount then user doesn't entitle to any discount. if course_price != price_before_discount: context["price_before_discount"] = price_before_discount if verified_mode.sku: context[ "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled( request.user) context[ "ecommerce_payment_page"] = ecommerce_service.payment_page_url( ) context["sku"] = verified_mode.sku context["bulk_sku"] = verified_mode.bulk_sku context['currency_data'] = [] if waffle.switch_is_active('local_currency'): if 'edx-price-l10n' not in request.COOKIES: currency_data = get_currency_data() try: context['currency_data'] = json.dumps(currency_data) except TypeError: pass language = get_language() context['track_links'] = get_verified_track_links(language) duration = get_user_course_duration(request.user, course) deadline = duration and get_user_course_expiration_date( request.user, course) if deadline: formatted_audit_access_date = strftime_localized_html( deadline, 'SHORT_DATE') context['audit_access_deadline'] = formatted_audit_access_date fbe_is_on = deadline and gated_content # Route to correct Track Selection page. # REV-2133 TODO Value Prop: remove waffle flag after testing is completed # and happy path version is ready to be rolled out to all users. if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled(): if not error: # TODO: Remove by executing REV-2355 if not enterprise_customer_for_request( request): # TODO: Remove by executing REV-2342 if fbe_is_on: return render_to_response("course_modes/fbe.html", context) else: return render_to_response("course_modes/unfbe.html", context) # If error or enterprise_customer, failover to old choose.html page return render_to_response("course_modes/choose.html", context)
def process_request(self, request): """ Fill the request with Enterprise-related content. """ if 'enterprise_customer' not in request.session and request.user.is_authenticated: request.session['enterprise_customer'] = api.enterprise_customer_for_request(request)
def test_enterprise_customer_for_request( self, mock_registry, mock_partial, mock_enterprise_customer_model, mock_get_enterprise_learner_data, ): def mock_get_enterprise_customer(**kwargs): uuid = kwargs.get( 'enterprise_customer_identity_provider__provider_id') if uuid: return mock.MagicMock(uuid=uuid, user=self.user) raise Exception dummy_request = mock.MagicMock(session={}, user=self.user) mock_enterprise_customer_model.objects.get.side_effect = mock_get_enterprise_customer mock_enterprise_customer_model.DoesNotExist = Exception mock_partial.return_value = True mock_registry.get_from_pipeline.return_value.provider_id = 'real-ent-uuid' # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user. self.mock_get_enterprise_customer('real-ent-uuid', {'real': 'enterprisecustomer'}, 200) enterprise_customer = enterprise_customer_for_request(dummy_request) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) httpretty.reset() # Verify that the method `enterprise_customer_for_request` returns no # enterprise customer if the enterprise customer API throws 404. del dummy_request.session['enterprise_customer'] self.mock_get_enterprise_customer('real-ent-uuid', {'detail': 'Not found.'}, 404) enterprise_customer = enterprise_customer_for_request(dummy_request) self.assertIsNone(enterprise_customer) httpretty.reset() # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user even if # the third-party auth pipeline has no `provider_id`. mock_registry.get_from_pipeline.return_value.provider_id = None self.mock_get_enterprise_customer('real-ent-uuid', {'real': 'enterprisecustomer'}, 200) mock_request = mock.MagicMock( GET={'enterprise_customer': 'real-ent-uuid'}, COOKIES={}, session={}, user=self.user) enterprise_customer = enterprise_customer_for_request(mock_request) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user even if # the third-party auth pipeline has no `provider_id` but there is # enterprise customer UUID in the cookie. mock_request = mock.MagicMock( GET={}, COOKIES={ settings.ENTERPRISE_CUSTOMER_COOKIE_NAME: 'real-ent-uuid' }, session={}, user=self.user) enterprise_customer = enterprise_customer_for_request(mock_request) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user if # data is cached only in the request session mock_registry.get_from_pipeline.return_value.provider_id = None self.mock_get_enterprise_customer('real-ent-uuid', {'real': 'enterprisecustomer'}, 200) mock_request = mock.MagicMock( GET={}, COOKIES={}, session={'enterprise_customer': { 'real': 'enterprisecustomer' }}, user=self.user) enterprise_customer = enterprise_customer_for_request(mock_request) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) # Verify that we can still get enterprise customer from enterprise # learner API even if we are unable to get it from preferred sources, # e.g. url query parameters, third-party auth pipeline, enterprise # cookie, or session. mock_get_enterprise_learner_data.return_value = [{ 'enterprise_customer': { 'uuid': 'real-ent-uuid' } }] mock_request = mock.MagicMock(GET={}, COOKIES={}, session={}, user=self.user, site=1) enterprise_customer = enterprise_customer_for_request(mock_request) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'})
def _third_party_auth_context(request, redirect_to, tpa_hint=None): """Context for third party auth providers and the currently running pipeline. Arguments: request (HttpRequest): The request, used to determine if a pipeline is currently running. redirect_to: The URL to send the user to following successful authentication. tpa_hint (string): An override flag that will return a matching provider as long as its configuration has been enabled Returns: dict """ context = { "currentProvider": None, "providers": [], "secondaryProviders": [], "finishAuthUrl": None, "errorMessage": None, } if third_party_auth.is_enabled(): if not enterprise_customer_for_request(request): for enabled in third_party_auth.provider.Registry.displayed_for_login(tpa_hint=tpa_hint): info = { "id": enabled.provider_id, "name": enabled.name, "iconClass": enabled.icon_class or None, "iconImage": enabled.icon_image.url if enabled.icon_image else None, "loginUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to, ), "registerUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to, ), } context["providers" if not enabled.secondary else "secondaryProviders"].append(info) running_pipeline = pipeline.get(request) if running_pipeline is not None: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) if current_provider is not None: context["currentProvider"] = current_provider.name context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name) if current_provider.skip_registration_form: # As a reliable way of "skipping" the registration form, we just submit it automatically context["autoSubmitRegForm"] = True # Check for any error messages we may want to display: for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": # msg may or may not be translated. Try translating [again] in case we are able to: context['errorMessage'] = _(unicode(msg)) # pylint: disable=translation-of-non-string break return context
def login_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard # Note: We check for the existence of login-related cookies in addition to is_authenticated # since Django's SessionAuthentication middleware auto-updates session cookies but not # the other login-related cookies. See ARCH-282. if request.user.is_authenticated and are_logged_in_cookies_set(request): return redirect(redirect_to) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: # lint-amnesty, pylint: disable=too-many-nested-blocks try: next_args = six.moves.urllib.parse.parse_qs(six.moves.urllib.parse.urlparse(redirect_to).query) if 'tpa_hint' in next_args: provider_id = next_args['tpa_hint'][0] tpa_hint_provider = third_party_auth.provider.Registry.get(provider_id=provider_id) if tpa_hint_provider: if tpa_hint_provider.skip_hinted_login_dialog: # Forward the user directly to the provider's login URL when the provider is configured # to skip the dialog. if initial_mode == "register": auth_entry = pipeline.AUTH_ENTRY_REGISTER else: auth_entry = pipeline.AUTH_ENTRY_LOGIN return redirect( pipeline.get_login_url(provider_id, auth_entry, redirect_url=redirect_to) ) third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError) as ex: log.exception(u"Unknown tpa_hint provider: %s", ex) # Redirect to authn MFE if it is enabled or user is not an enterprise user or not coming from a SAML IDP. saml_provider = False running_pipeline = pipeline.get(request) enterprise_customer = enterprise_customer_for_request(request) if running_pipeline: saml_provider, __ = third_party_auth.utils.is_saml_provider( running_pipeline.get('backend'), running_pipeline.get('kwargs') ) if should_redirect_to_authn_microfrontend() and not enterprise_customer and not saml_provider: # This is to handle a case where a logged-in cookie is not present but the user is authenticated. # Note: If we don't handle this learner is redirected to authn MFE and then back to dashboard # instead of the desired redirect URL (e.g. finish_auth) resulting in learners not enrolling # into the courses. if request.user.is_authenticated and redirect_to: return redirect(redirect_to) query_params = request.GET.urlencode() url_path = '/{}{}'.format( initial_mode, '?' + query_params if query_params else '' ) return redirect(settings.AUTHN_MICROFRONTEND_URL + url_path) # Account activation message account_activation_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-activation' in message.tags ] account_recovery_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-recovery' in message.tags ] # Otherwise, render the combined login/registration page context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': third_party_auth_context(request, redirect_to, third_party_auth_hint), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK, 'account_activation_messages': account_activation_messages, 'account_recovery_messages': account_recovery_messages, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), 'account_creation_allowed': configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)), 'is_account_recovery_feature_enabled': is_secondary_email_feature_enabled(), 'is_multiple_user_enterprises_feature_enabled': is_multiple_user_enterprises_feature_enabled(), 'enterprise_slug_login_url': get_enterprise_slug_login_url(), 'is_require_third_party_auth_enabled': is_require_third_party_auth_enabled(), }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'combined_login_and_register': True, 'disable_footer': not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER', settings.FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER'] ), } update_logistration_context_for_enterprise(request, context, enterprise_customer) response = render_to_response('student_account/login_and_register.html', context) handle_enterprise_cookies_for_logistration(request, response, context) return response
def get(self, request, course_id, error=None): """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_ip(request), url=request.path ) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key) increment('track-selection.{}.{}'.format(enrollment_mode, 'active' if is_active else 'inactive')) increment('track-selection.views') if enrollment_mode is None: LOG.info('Rendering track selection for unenrolled user, referred by %s', request.META.get('HTTP_REFERER')) modes = CourseMode.modes_for_course_dict(course_key) ecommerce_service = EcommerceService() # We assume that, if 'professional' is one of the modes, it should be the *only* mode. # If there are both modes, default to non-id-professional. has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode(modes) and not has_enrolled_professional: purchase_workflow = request.GET.get("purchase_workflow", "single") verify_url = reverse('verify_student_start_flow', kwargs={'course_id': six.text_type(course_key)}) redirect_url = "{url}?purchase_workflow={workflow}".format(url=verify_url, workflow=purchase_workflow) if ecommerce_service.is_enabled(request.user): professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL) if purchase_workflow == "single" and professional_mode.sku: redirect_url = ecommerce_service.get_checkout_page_url(professional_mode.sku) if purchase_workflow == "bulk" and professional_mode.bulk_sku: redirect_url = ecommerce_service.get_checkout_page_url(professional_mode.bulk_sku) return redirect(redirect_url) course = modulestore().get_course(course_key) # If there isn't a verified mode available, then there's nothing # to do on this page. Send the user to the dashboard. if not CourseMode.has_verified_mode(modes): return redirect(reverse('dashboard')) # If a user has already paid, redirect them to the dashboard. if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): # If the course has started redirect to course home instead if course.has_started(): return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key})) return redirect(reverse('dashboard')) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(six.text_type(course_key), None) if CourseEnrollment.is_enrollment_closed(request.user, course): locale = to_locale(get_language()) enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale) params = six.moves.urllib.parse.urlencode({'course_closed': enrollment_end_date}) return redirect('{0}?{1}'.format(reverse('dashboard'), params)) # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible # for university credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging # for the verified track. has_credit_upsell = any( CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False) ) course_id = text_type(course_key) context = { "course_modes_choose_url": reverse( "course_modes_choose", kwargs={'course_id': course_id} ), "modes": modes, "has_credit_upsell": has_credit_upsell, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "responsive": True, "nav_hidden": True, "content_gating_enabled": ContentTypeGatingConfig.enabled_for_enrollment( user=request.user, course_key=course_key ), "course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment( user=request.user, course_key=course_key ), } context.update( get_experiment_user_metadata_context( course, request.user, ) ) title_content = '' if enrollment_mode: title_content = _("Congratulations! You are now enrolled in {course_name}").format( course_name=course.display_name_with_default ) context["title_content"] = title_content if "verified" in modes: verified_mode = modes["verified"] context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in verified_mode.suggested_prices.split(",") if x.strip() ] price_before_discount = verified_mode.min_price course_price = price_before_discount enterprise_customer = enterprise_customer_for_request(request) if enterprise_customer and verified_mode.sku: course_price = get_course_final_price(request.user, verified_mode.sku, price_before_discount) context["currency"] = verified_mode.currency.upper() context["currency_symbol"] = get_currency_symbol(verified_mode.currency.upper()) context["min_price"] = course_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description # if course_price is equal to price_before_discount then user doesn't entitle to any discount. if course_price != price_before_discount: context["price_before_discount"] = price_before_discount if verified_mode.sku: context["use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(request.user) context["ecommerce_payment_page"] = ecommerce_service.payment_page_url() context["sku"] = verified_mode.sku context["bulk_sku"] = verified_mode.bulk_sku context['currency_data'] = [] if waffle.switch_is_active('local_currency'): if 'edx-price-l10n' not in request.COOKIES: currency_data = get_currency_data() try: context['currency_data'] = json.dumps(currency_data) except TypeError: pass return render_to_response("course_modes/choose.html", context)
def test_enterprise_customer_for_request( self, mock_registry, mock_partial, mock_enterprise_customer_model, mock_get_enterprise_learner_data, ): def mock_get_enterprise_customer(**kwargs): uuid = kwargs.get('enterprise_customer_identity_provider__provider_id') if uuid: return mock.MagicMock(uuid=uuid, user=self.user) raise Exception dummy_request = mock.MagicMock(session={}, user=self.user) mock_enterprise_customer_model.objects.get.side_effect = mock_get_enterprise_customer mock_enterprise_customer_model.DoesNotExist = Exception mock_partial.return_value = True mock_registry.get_from_pipeline.return_value.provider_id = 'real-ent-uuid' # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user. self.mock_get_enterprise_customer('real-ent-uuid', {'real': 'enterprisecustomer'}, 200) enterprise_customer = enterprise_customer_for_request(dummy_request) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) httpretty.reset() # Verify that the method `enterprise_customer_for_request` returns no # enterprise customer if the enterprise customer API throws 404. self.mock_get_enterprise_customer('real-ent-uuid', {'detail': 'Not found.'}, 404) enterprise_customer = enterprise_customer_for_request(dummy_request) self.assertIsNone(enterprise_customer) httpretty.reset() # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user even if # the third-party auth pipeline has no `provider_id`. mock_registry.get_from_pipeline.return_value.provider_id = None self.mock_get_enterprise_customer('real-ent-uuid', {'real': 'enterprisecustomer'}, 200) enterprise_customer = enterprise_customer_for_request( mock.MagicMock(GET={'enterprise_customer': 'real-ent-uuid'}, user=self.user) ) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user even if # the third-party auth pipeline has no `provider_id` but there is # enterprise customer UUID in the cookie. enterprise_customer = enterprise_customer_for_request( mock.MagicMock(GET={}, COOKIES={settings.ENTERPRISE_CUSTOMER_COOKIE_NAME: 'real-ent-uuid'}, user=self.user) ) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) # Verify that we can still get enterprise customer from enterprise # learner API even if we are unable to get it from preferred sources, # e.g. url query parameters, third-party auth pipeline, enterprise # cookie. mock_get_enterprise_learner_data.return_value = [{'enterprise_customer': {'uuid': 'real-ent-uuid'}}] enterprise_customer = enterprise_customer_for_request( mock.MagicMock(GET={}, COOKIES={}, user=self.user, site=1) ) self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'})
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(six.text_type(year), six.text_type(year)) for year in UserProfile.VALID_YEARS] try: user_orders = get_user_orders(user) except: # pylint: disable=bare-except log.exception('Error fetching order history from Otto.') # Return empty order list as account settings page expect a list and # it will be broken if exception raised user_orders = [] beta_language = {} dark_lang_config = DarkLangConfig.current() if dark_lang_config.enable_beta_languages: user_preferences = get_user_preferences(user) pref_language = user_preferences.get('pref-lang') if pref_language in dark_lang_config.beta_languages_list: beta_language['code'] = pref_language beta_language['name'] = settings.LANGUAGE_DICT.get(pref_language) context = { 'auth': {}, 'duplicate_provider': None, 'nav_hidden': True, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': all_languages(), }, 'time_zone': { 'options': TIME_ZONE_CHOICES, } }, 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'password_reset_support_link': configuration_helpers.get_value('PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_dashboard_tabs': True, 'order_history': user_orders, 'disable_order_history_tab': should_redirect_to_order_history_microfrontend(), 'enable_account_deletion': configuration_helpers.get_value( 'ENABLE_ACCOUNT_DELETION', settings.FEATURES.get('ENABLE_ACCOUNT_DELETION', False)), 'extended_profile_fields': _get_extended_profile_fields(), 'beta_language': beta_language, } enterprise_customer = enterprise_customer_for_request(request) update_account_settings_context_for_enterprise(context, enterprise_customer, user) if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider( messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [ { 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state. has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), # We only want to include providers if they are either currently available to be logged # in with, or if the user is already authenticated with them. } for state in auth_states if state.provider.display_for_login or state.has_account ] return context
def _third_party_auth_context(request, redirect_to, tpa_hint=None): """Context for third party auth providers and the currently running pipeline. Arguments: request (HttpRequest): The request, used to determine if a pipeline is currently running. redirect_to: The URL to send the user to following successful authentication. tpa_hint (string): An override flag that will return a matching provider as long as its configuration has been enabled Returns: dict """ context = { "currentProvider": None, "providers": [], "secondaryProviders": [], "finishAuthUrl": None, "errorMessage": None, "registerFormSubmitButtonText": _("Create Account"), } if third_party_auth.is_enabled(): enterprise_customer = enterprise_customer_for_request(request) if not enterprise_customer: for enabled in third_party_auth.provider.Registry.displayed_for_login( tpa_hint=tpa_hint): info = { "id": enabled.provider_id, "name": enabled.name, "iconClass": enabled.icon_class or None, "iconImage": enabled.icon_image.url if enabled.icon_image else None, "loginUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to, ), "registerUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to, ), } context["providers" if not enabled. secondary else "secondaryProviders"].append(info) running_pipeline = pipeline.get(request) if running_pipeline is not None: current_provider = third_party_auth.provider.Registry.get_from_pipeline( running_pipeline) if current_provider is not None: context["currentProvider"] = current_provider.name context["finishAuthUrl"] = pipeline.get_complete_url( current_provider.backend_name) if current_provider.skip_registration_form: # For enterprise (and later for everyone), we need to get explicit consent to the # Terms of service instead of auto submitting the registration form outright. if not enterprise_customer: # As a reliable way of "skipping" the registration form, we just submit it automatically context["autoSubmitRegForm"] = True else: context["autoRegisterWelcomeMessage"] = ( 'Thank you for joining {}. ' 'Just a couple steps before you start learning!' ).format( configuration_helpers.get_value( 'PLATFORM_NAME', settings.PLATFORM_NAME)) context["registerFormSubmitButtonText"] = _("Continue") # Check for any error messages we may want to display: for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": # msg may or may not be translated. Try translating [again] in case we are able to: context['errorMessage'] = _(unicode(msg)) # pylint: disable=translation-of-non-string break return context
def login_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard if request.user.is_authenticated: return redirect(redirect_to) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: try: next_args = urlparse.parse_qs(urlparse.urlparse(redirect_to).query) provider_id = next_args['tpa_hint'][0] tpa_hint_provider = third_party_auth.provider.Registry.get(provider_id=provider_id) if tpa_hint_provider: if tpa_hint_provider.skip_hinted_login_dialog: # Forward the user directly to the provider's login URL when the provider is configured # to skip the dialog. if initial_mode == "register": auth_entry = pipeline.AUTH_ENTRY_REGISTER else: auth_entry = pipeline.AUTH_ENTRY_LOGIN return redirect( pipeline.get_login_url(provider_id, auth_entry, redirect_url=redirect_to) ) third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError) as ex: log.error("Unknown tpa_hint provider: %s", ex) # If this is a themed site, revert to the old login/registration pages. # We need to do this for now to support existing themes. # Themed sites can use the new logistration page by setting # 'ENABLE_COMBINED_LOGIN_REGISTRATION' in their # configuration settings. if is_request_in_themed_site() and not configuration_helpers.get_value('ENABLE_COMBINED_LOGIN_REGISTRATION', False): if initial_mode == "login": return old_login_view(request) elif initial_mode == "register": return old_register_view(request) # Allow external auth to intercept and handle the request ext_auth_response = _external_auth_intercept(request, initial_mode) if ext_auth_response is not None: return ext_auth_response # Account activation message account_activation_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-activation' in message.tags ] # Otherwise, render the combined login/registration page context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': _third_party_auth_context(request, redirect_to, third_party_auth_hint), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK, 'account_activation_messages': account_activation_messages, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), 'account_creation_allowed': configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)) }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'combined_login_and_register': True, 'disable_footer': not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER', settings.FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER'] ), } enterprise_customer = enterprise_customer_for_request(request) update_logistration_context_for_enterprise(request, context, enterprise_customer) response = render_to_response('student_account/login_and_register.html', context) handle_enterprise_cookies_for_logistration(request, response, context) return response