Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3172,3 +3172,14 @@ def _should_send_certificate_events(settings):
SSL_AUTH_DN_FORMAT_STRING = (
"/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}"
)

OPEN_EDX_FILTERS_CONFIG = {
"org.openedx.learning.logistration.context.requested.v1": {
"fail_silently": True,
"pipeline": ["enterprise.filters.logistration.LogistrationContextEnricher"],
},
"org.openedx.learning.auth.post_login.redirect_url.requested.v1": {
"fail_silently": True,
"pipeline": ["enterprise.filters.logistration.PostLoginEnterpriseRedirect"],
},
}
42 changes: 7 additions & 35 deletions openedx/core/djangoapps/user_authn/views/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import hashlib
import json
import logging
import re
import urllib

from django.conf import settings
from django.contrib.auth import authenticate, get_user_model
Expand All @@ -29,7 +27,7 @@
from eventtracking import tracker
from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
from openedx_filters.learning.filters import StudentLoginRequested
from openedx_filters.learning.filters import PostLoginRedirectURLRequested, StudentLoginRequested
from rest_framework import status
from rest_framework.views import APIView

Expand All @@ -56,11 +54,10 @@
)
from openedx.core.djangoapps.user_authn.views.login_form import get_login_session_form
from openedx.core.djangoapps.user_authn.views.password_reset import send_password_reset_email_for_user
from openedx.core.djangoapps.user_authn.views.utils import API_V1, ENTERPRISE_ENROLLMENT_URL_REGEX, UUID4_REGEX
from openedx.core.djangoapps.user_authn.views.utils import API_V1
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.lib.api.view_utils import require_post_params # lint-amnesty, pylint: disable=unused-import
from openedx.features.enterprise_support.api import activate_learner_enterprise, get_enterprise_learner_data_from_api

log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
Expand Down Expand Up @@ -478,35 +475,6 @@ def finish_auth(request):
)


def enterprise_selection_page(request, user, next_url):
"""
Updates redirect url to enterprise selection page if user is associated
with multiple enterprises otherwise return the next url.

param:
next_url(string): The URL to redirect to after multiple enterprise selection or in case
the selection page is bypassed e.g when dealing with direct enrolment urls.
"""
redirect_url = next_url

response = get_enterprise_learner_data_from_api(user)
if response and len(response) > 1:
redirect_url = reverse("enterprise_select_active") + "/?success_url=" + urllib.parse.quote(next_url)

# Check to see if next url has an enterprise in it. In this case if user is associated with
# that enterprise, activate that enterprise and bypass the selection page.
if re.match(ENTERPRISE_ENROLLMENT_URL_REGEX, urllib.parse.unquote(next_url)):
enterprise_in_url = re.search(UUID4_REGEX, next_url).group(0)
for enterprise in response:
if enterprise_in_url == str(enterprise["enterprise_customer"]["uuid"]):
is_activated_successfully = activate_learner_enterprise(request, user, enterprise_in_url)
if is_activated_successfully:
redirect_url = next_url
break

return redirect_url


@ensure_csrf_cookie
@require_http_methods(["POST"])
@ratelimit(
Expand Down Expand Up @@ -649,7 +617,11 @@ def login_user(request, api_version="v1"): # pylint: disable=too-many-statement
elif should_redirect_to_authn_microfrontend():
next_url, root_url = get_next_url_for_login_page(request, include_host=True)
redirect_url = get_redirect_url_with_host(
root_url, enterprise_selection_page(request, possibly_authenticated_user, finish_auth_url or next_url)
root_url, PostLoginRedirectURLRequested.run_filter(
redirect_url='',
user=possibly_authenticated_user,
next_url=finish_auth_url or next_url,
) or finish_auth_url or next_url
)

if (
Expand Down
53 changes: 13 additions & 40 deletions openedx/core/djangoapps/user_authn/views/login_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,28 @@
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
from django_ratelimit.decorators import ratelimit
from openedx_filters.learning.filters import LogistrationContextRequested

from common.djangoapps import third_party_auth
from common.djangoapps.edxmako.shortcuts import render_to_response
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.decorators import xframe_allow_whitelisted
from common.djangoapps.util.password_policy_validators import DEFAULT_MAX_PASSWORD_LENGTH
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api import accounts
from openedx.core.djangoapps.user_api.accounts.utils import (
is_secondary_email_feature_enabled
)
from openedx.core.djangoapps.user_api.helpers import FormDescription
from openedx.core.djangoapps.user_authn.cookies import set_logged_in_cookies
from openedx.core.djangoapps.user_authn.toggles import should_redirect_to_authn_microfrontend
from openedx.core.djangoapps.user_authn.toggles import (
is_require_third_party_auth_enabled,
should_redirect_to_authn_microfrontend,
)
from openedx.core.djangoapps.user_authn.views.password_reset import get_password_reset_form
from openedx.core.djangoapps.user_authn.views.registration_form import RegistrationFormFactory
from openedx.core.djangoapps.user_authn.views.utils import third_party_auth_context
from openedx.core.djangoapps.user_authn.toggles import is_require_third_party_auth_enabled
from openedx.features.enterprise_support.api import enterprise_customer_for_request, enterprise_enabled
from openedx.features.enterprise_support.utils import (
get_enterprise_slug_login_url,
handle_enterprise_cookies_for_logistration,
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.decorators import xframe_allow_whitelisted
from common.djangoapps.util.password_policy_validators import DEFAULT_MAX_PASSWORD_LENGTH

log = logging.getLogger(__name__)

Expand All @@ -56,22 +53,8 @@ def _apply_third_party_auth_overrides(request, form_desc):
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,
}
)
if current_provider:
pass # SSO-specific form overrides are handled by filter pipeline steps.


def get_login_session_form(request):
Expand Down Expand Up @@ -200,12 +183,7 @@ def login_and_registration_form(request, initial_mode="login"):
running_pipeline.get('backend'), running_pipeline.get('kwargs')
)

enterprise_customer = enterprise_customer_for_request(request)

if should_redirect_to_authn_microfrontend() and \
not enterprise_customer and \
not tpa_hint_provider and \
not saml_provider:
if should_redirect_to_authn_microfrontend() and not tpa_hint_provider 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
Expand Down Expand Up @@ -260,8 +238,6 @@ def login_and_registration_form(request, initial_mode="login"):
'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)),
'register_links_allowed': settings.FEATURES.get('SHOW_REGISTRATION_LINKS', True),
'is_account_recovery_feature_enabled': is_secondary_email_feature_enabled(),
'enterprise_slug_login_url': get_enterprise_slug_login_url(),
'is_enterprise_enable': enterprise_enabled(),
'is_require_third_party_auth_enabled': is_require_third_party_auth_enabled(),
'enable_coppa_compliance': settings.ENABLE_COPPA_COMPLIANCE,
'edx_user_info_cookie_name': settings.EDXMKTG_USER_INFO_COOKIE_NAME,
Expand All @@ -277,11 +253,8 @@ def login_and_registration_form(request, initial_mode="login"):
),
}

update_logistration_context_for_enterprise(request, context, enterprise_customer)

context, _ = LogistrationContextRequested.run_filter(context=context, request=request)
response = render_to_response('student_account/login_and_register.html', context)
handle_enterprise_cookies_for_logistration(request, response, context)

return response


Expand Down
109 changes: 0 additions & 109 deletions openedx/core/djangoapps/user_authn/views/tests/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import hashlib
import json
import unicodedata
import urllib.parse
from unittest.mock import Mock, patch

import ddt
Expand Down Expand Up @@ -41,7 +40,6 @@
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.core.lib.api.test_utils import ApiTestCase
from openedx.features.enterprise_support.tests.factories import EnterpriseCustomerUserFactory
from common.djangoapps.student.models import LoginFailures
from common.djangoapps.util.password_policy_validators import DEFAULT_MAX_PASSWORD_LENGTH
from common.test.utils import assert_dict_contains_subset
Expand Down Expand Up @@ -218,113 +216,6 @@ def test_login_success_with_redirect(self, next_url, course_id, expected_redirec
self._assert_response(response, success=True)
self._assert_redirect_url(response, expected_redirect)

@ddt.data(('/dashboard', False), ('/enterprise/select/active/?success_url=/dashboard', True))
@ddt.unpack
@patch.dict(settings.FEATURES, {'ENABLE_AUTHN_MICROFRONTEND': True, 'ENABLE_ENTERPRISE_INTEGRATION': True})
@override_settings(LOGIN_REDIRECT_WHITELIST=['openedx.service'])
@patch('openedx.features.enterprise_support.api.EnterpriseApiClient')
@patch('openedx.core.djangoapps.user_authn.views.login.reverse')
@skip_unless_lms
def test_login_success_for_multiple_enterprises(
self, expected_redirect, user_has_multiple_enterprises, reverse_mock, mock_api_client_class
):
"""
Test that if multiple enterprise feature is enabled, user is redirected
to correct page
"""
api_response = {'results': []}
enterprise = EnterpriseCustomerUserFactory(user_id=self.user.id).enterprise_customer
api_response['results'].append(
{
"enterprise_customer": {
"uuid": enterprise.uuid,
"name": enterprise.name,
"active": enterprise.active,
}
}
)

if user_has_multiple_enterprises:
enterprise = EnterpriseCustomerUserFactory(user_id=self.user.id).enterprise_customer
api_response['results'].append(
{
"enterprise_customer": {
"uuid": enterprise.uuid,
"name": enterprise.name,
"active": enterprise.active,
}
}
)

mock_client = mock_api_client_class.return_value
mock_client.fetch_enterprise_learner_data.return_value = api_response
reverse_mock.return_value = '/enterprise/select/active'

response, _ = self._login_response(
self.user.email,
self.password,
HTTP_ACCEPT='*/*',
)
self._assert_response(response, success=True)
self._assert_redirect_url(response, settings.LMS_ROOT_URL + expected_redirect)

@ddt.data(('', True), ('/enterprise/select/active/?success_url=', False))
@ddt.unpack
@patch.dict(settings.FEATURES, {'ENABLE_AUTHN_MICROFRONTEND': True, 'ENABLE_ENTERPRISE_INTEGRATION': True})
@patch('openedx.features.enterprise_support.api.EnterpriseApiClient')
@patch('openedx.core.djangoapps.user_authn.views.login.activate_learner_enterprise')
@patch('openedx.core.djangoapps.user_authn.views.login.reverse')
@skip_unless_lms
def test_enterprise_in_url(
self, expected_redirect, is_activated, reverse_mock, mock_activate_learner_enterprise, mock_api_client_class
):
"""
If user has multiple enterprises and the enterprise is present in url,
activate that url
"""
api_response = {}
enterprise_1 = EnterpriseCustomerUserFactory(user_id=self.user.id).enterprise_customer
enterprise_2 = EnterpriseCustomerUserFactory(user_id=self.user.id).enterprise_customer
api_response['results'] = [
{
"enterprise_customer": {
"uuid": enterprise_1.uuid,
"name": enterprise_1.name,
"active": enterprise_1.active,
}
},
{
"enterprise_customer": {
"uuid": enterprise_2.uuid,
"name": enterprise_2.name,
"active": enterprise_2.active,
}
}
]

next_url = '/enterprise/{}/course/{}/enroll/?catalog=catalog_uuid&utm_medium=enterprise'.format(
enterprise_1.uuid,
'course-v1:testX+test101+2T2020'
)

mock_client = mock_api_client_class.return_value
mock_client.fetch_enterprise_learner_data.return_value = api_response
mock_activate_learner_enterprise.return_value = is_activated
reverse_mock.return_value = '/enterprise/select/active'

response, _ = self._login_response(
self.user.email,
self.password,
extra_post_params={'next': next_url},
HTTP_ACCEPT='*/*',
)

if not is_activated:
next_url = urllib.parse.quote(next_url)

self._assert_response(response, success=True)
self._assert_redirect_url(response, settings.LMS_ROOT_URL + expected_redirect + next_url)

@patch.dict("django.conf.settings.FEATURES", {'SQUELCH_PII_IN_LOGS': True})
def test_login_success_no_pii(self):
response, mock_audit_log = self._login_response(
Expand Down
Loading
Loading