Commit 5e732ea2 by Will Daly

Country Access: Block enrollment from third party auth

Users who are blocked from enrolling in a course
due to an embargo are redirected to the blocked
message page after authentication completes.
parent 6ba6afce
...@@ -61,6 +61,7 @@ import random ...@@ -61,6 +61,7 @@ import random
import string # pylint: disable-msg=deprecated-module import string # pylint: disable-msg=deprecated-module
from collections import OrderedDict from collections import OrderedDict
import urllib import urllib
from ipware.ip import get_ip
import analytics import analytics
from eventtracking import tracker from eventtracking import tracker
...@@ -73,6 +74,7 @@ from social.exceptions import AuthException ...@@ -73,6 +74,7 @@ from social.exceptions import AuthException
from social.pipeline import partial from social.pipeline import partial
import student import student
from embargo import api as embargo_api
from shoppingcart.models import Order, PaidCourseRegistration # pylint: disable=import-error from shoppingcart.models import Order, PaidCourseRegistration # pylint: disable=import-error
from shoppingcart.exceptions import ( # pylint: disable=import-error from shoppingcart.exceptions import ( # pylint: disable=import-error
CourseDoesNotExistException, CourseDoesNotExistException,
...@@ -634,7 +636,7 @@ def login_analytics(strategy, *args, **kwargs): ...@@ -634,7 +636,7 @@ def login_analytics(strategy, *args, **kwargs):
@partial.partial @partial.partial
def change_enrollment(strategy, user=None, *args, **kwargs): def change_enrollment(strategy, user=None, is_dashboard=False, *args, **kwargs):
"""Enroll a user in a course. """Enroll a user in a course.
If a user entered the authentication flow when trying to enroll If a user entered the authentication flow when trying to enroll
...@@ -653,17 +655,55 @@ def change_enrollment(strategy, user=None, *args, **kwargs): ...@@ -653,17 +655,55 @@ def change_enrollment(strategy, user=None, *args, **kwargs):
upon completion of the authentication pipeline upon completion of the authentication pipeline
(configured using the ?next parameter to the third party auth login url). (configured using the ?next parameter to the third party auth login url).
Keyword Arguments:
user (User): The user being authenticated.
is_dashboard (boolean): Whether the user entered the authentication
pipeline from the "link account" button on the student dashboard.
""" """
# We skip enrollment if the user entered the flow from the "link account"
# button on the student dashboard. At this point, either:
#
# 1) The user already had a linked account when they started the enrollment flow,
# in which case they would have been enrolled during the normal authentication process.
#
# 2) The user did NOT have a linked account, in which case they would have
# needed to go through the login/register page. Since we preserve the querystring
# args when sending users to this page, successfully authenticating through this page
# would also enroll the student in the course.
enroll_course_id = strategy.session_get('enroll_course_id') enroll_course_id = strategy.session_get('enroll_course_id')
if enroll_course_id: if enroll_course_id and not is_dashboard:
course_id = CourseKey.from_string(enroll_course_id) course_id = CourseKey.from_string(enroll_course_id)
modes = CourseMode.modes_for_course_dict(course_id) modes = CourseMode.modes_for_course_dict(course_id)
# If the email opt in parameter is found, set the preference. # If the email opt in parameter is found, set the preference.
email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY) email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
if email_opt_in: if email_opt_in:
opt_in = email_opt_in.lower() == 'true' opt_in = email_opt_in.lower() == 'true'
profile.update_email_opt_in(user.username, course_id.org, opt_in) profile.update_email_opt_in(user.username, course_id.org, opt_in)
if CourseMode.can_auto_enroll(course_id, modes_dict=modes):
# Check whether we're blocked from enrolling by a
# country access rule.
# Note: We skip checking the user's profile setting
# for country here because the "redirect URL" pointing
# to the blocked message page is set when the user
# *enters* the pipeline, at which point they're
# not authenticated. If they end up being blocked
# from the courseware, it's better to let them
# enroll and then show the message when they
# enter the course than to skip enrollment
# altogether.
is_blocked = not embargo_api.check_course_access(
course_id, ip_address=get_ip(strategy.request),
url=strategy.request.path
)
if is_blocked:
# If we're blocked, skip enrollment.
# A redirect URL should have been set so the user
# ends up on the embargo page when enrollment completes.
pass
elif CourseMode.can_auto_enroll(course_id, modes_dict=modes):
try: try:
CourseEnrollment.enroll(user, course_id, check_access=True) CourseEnrollment.enroll(user, course_id, check_access=True)
except CourseEnrollmentException: except CourseEnrollmentException:
......
...@@ -4,20 +4,22 @@ from collections import namedtuple ...@@ -4,20 +4,22 @@ from collections import namedtuple
import datetime import datetime
import unittest import unittest
from mock import patch
import ddt import ddt
import pytz import pytz
from util.testing import UrlResetMixin
from third_party_auth import pipeline from third_party_auth import pipeline
from shoppingcart.models import Order, PaidCourseRegistration # pylint: disable=import-error from shoppingcart.models import Order, PaidCourseRegistration # pylint: disable=import-error
from social.apps.django_app import utils as social_utils from social.apps.django_app import utils as social_utils
from django.conf import settings from django.conf import settings
from django.contrib.sessions.backends import cache from django.contrib.sessions.backends import cache
from django.test import RequestFactory from django.test import RequestFactory
from django.test.utils import override_settings
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, CourseModeFactory from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from openedx.core.djangoapps.user_api.models import UserOrgTag from openedx.core.djangoapps.user_api.models import UserOrgTag
from embargo.test_utils import restrict_course
THIRD_PARTY_AUTH_CONFIGURED = ( THIRD_PARTY_AUTH_CONFIGURED = (
...@@ -27,15 +29,17 @@ THIRD_PARTY_AUTH_CONFIGURED = ( ...@@ -27,15 +29,17 @@ THIRD_PARTY_AUTH_CONFIGURED = (
@unittest.skipUnless(THIRD_PARTY_AUTH_CONFIGURED, "Third party auth must be configured") @unittest.skipUnless(THIRD_PARTY_AUTH_CONFIGURED, "Third party auth must be configured")
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
@ddt.ddt @ddt.ddt
class PipelineEnrollmentTest(ModuleStoreTestCase): class PipelineEnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
"""Test that the pipeline auto-enrolls students upon successful authentication. """ """Test that the pipeline auto-enrolls students upon successful authentication. """
BACKEND_NAME = "google-oauth2" BACKEND_NAME = "google-oauth2"
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def setUp(self): def setUp(self):
"""Create a test course and user. """ """Create a test course and user. """
super(PipelineEnrollmentTest, self).setUp() super(PipelineEnrollmentTest, self).setUp('embargo')
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.user = UserFactory.create() self.user = UserFactory.create()
...@@ -125,6 +129,30 @@ class PipelineEnrollmentTest(ModuleStoreTestCase): ...@@ -125,6 +129,30 @@ class PipelineEnrollmentTest(ModuleStoreTestCase):
self.assertEqual(result, {}) self.assertEqual(result, {})
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id)) self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def test_blocked_by_embargo(self):
strategy = self._fake_strategy()
strategy.session_set('enroll_course_id', unicode(self.course.id))
with restrict_course(self.course.id):
result = pipeline.change_enrollment(strategy, 1, user=self.user) # pylint: disable=assignment-from-no-return,redundant-keyword-arg
# Verify that we were NOT enrolled
self.assertEqual(result, {})
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
def test_skip_enroll_from_dashboard(self):
strategy = self._fake_strategy()
strategy.session_set('enroll_course_id', unicode(self.course.id))
# Simulate completing the pipeline from the student dashboard's
# "link account" button.
result = pipeline.change_enrollment(strategy, 1, user=self.user, is_dashboard=True) # pylint: disable=assignment-from-no-return,redundant-keyword-arg
# Verify that we were NOT enrolled
self.assertEqual(result, {})
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
def test_url_creation(self): def test_url_creation(self):
strategy = self._fake_strategy() strategy = self._fake_strategy()
strategy.session_set('enroll_course_id', unicode(self.course.id)) strategy.session_set('enroll_course_id', unicode(self.course.id))
......
...@@ -17,6 +17,7 @@ from django.test.utils import override_settings ...@@ -17,6 +17,7 @@ from django.test.utils import override_settings
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from third_party_auth.tests.testutil import simulate_running_pipeline from third_party_auth.tests.testutil import simulate_running_pipeline
from embargo.test_utils import restrict_course
from openedx.core.djangoapps.user_api.api import account as account_api from openedx.core.djangoapps.user_api.api import account as account_api
from openedx.core.djangoapps.user_api.api import profile as profile_api from openedx.core.djangoapps.user_api.api import profile as profile_api
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
...@@ -374,13 +375,17 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase): ...@@ -374,13 +375,17 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
@ddt.ddt @ddt.ddt
class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase): class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase):
""" Tests for the student account views that update the user's account information. """ """ Tests for the student account views that update the user's account information. """
USERNAME = "bob" USERNAME = "bob"
EMAIL = "bob@example.com" EMAIL = "bob@example.com"
PASSWORD = "password" PASSWORD = "password"
@mock.patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def setUp(self):
super(StudentAccountLoginAndRegistrationTest, self).setUp('embargo')
@ddt.data( @ddt.data(
("account_login", "login"), ("account_login", "login"),
("account_register", "register"), ("account_register", "register"),
...@@ -546,6 +551,49 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase): ...@@ -546,6 +551,49 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase):
response = self.client.get(reverse("account_login"), {"course_id": unicode(course.id)}) response = self.client.get(reverse("account_login"), {"course_id": unicode(course.id)})
self._assert_third_party_auth_data(response, None, expected_providers) self._assert_third_party_auth_data(response, None, expected_providers)
@mock.patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def test_third_party_auth_enrollment_embargo(self):
course = CourseFactory.create()
# Start the pipeline attempting to enroll in a restricted course
with restrict_course(course.id) as redirect_url:
response = self.client.get(reverse("account_login"), {"course_id": unicode(course.id)})
# Expect that the course ID has been removed from the
# login URLs (so the user won't be enrolled) and
# the ?next param sends users to the blocked message.
expected_providers = [
{
"name": "Facebook",
"iconClass": "fa-facebook",
"loginUrl": self._third_party_login_url(
"facebook", "login",
course_id=unicode(course.id),
redirect_url=redirect_url
),
"registerUrl": self._third_party_login_url(
"facebook", "register",
course_id=unicode(course.id),
redirect_url=redirect_url
)
},
{
"name": "Google",
"iconClass": "fa-google-plus",
"loginUrl": self._third_party_login_url(
"google-oauth2", "login",
course_id=unicode(course.id),
redirect_url=redirect_url
),
"registerUrl": self._third_party_login_url(
"google-oauth2", "register",
course_id=unicode(course.id),
redirect_url=redirect_url
)
}
]
self._assert_third_party_auth_data(response, None, expected_providers)
@override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME) @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME)
def test_microsite_uses_old_login_page(self): def test_microsite_uses_old_login_page(self):
# Retrieve the login page from a microsite domain # Retrieve the login page from a microsite domain
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
import logging import logging
import json import json
from ipware.ip import get_ip
from django.conf import settings from django.conf import settings
from django.http import ( from django.http import (
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
...@@ -14,8 +16,12 @@ from django.utils.translation import ugettext as _ ...@@ -14,8 +16,12 @@ from django.utils.translation import ugettext as _
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from opaque_keys.edx.keys import CourseKey
from opaque_keys import InvalidKeyError
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_response, render_to_string
from microsite_configuration import microsite from microsite_configuration import microsite
from embargo import api as embargo_api
import third_party_auth import third_party_auth
from external_auth.login_and_register import ( from external_auth.login_and_register import (
login as external_auth_login, login as external_auth_login,
...@@ -321,6 +327,36 @@ def _third_party_auth_context(request): ...@@ -321,6 +327,36 @@ def _third_party_auth_context(request):
course_id = request.GET.get("course_id") course_id = request.GET.get("course_id")
email_opt_in = request.GET.get('email_opt_in') email_opt_in = request.GET.get('email_opt_in')
redirect_to = request.GET.get("next") redirect_to = request.GET.get("next")
# Check if the user is trying to enroll in a course
# that they don't have access to based on country
# access rules.
#
# If so, set the redirect URL to the blocked page.
# We need to set it here, rather than redirecting
# from within the pipeline, because a redirect
# from the pipeline can prevent users
# from completing the authentication process.
#
# Note that we can't check the user's country
# profile at this point, since the user hasn't
# authenticated. If the user ends up being blocked
# by their country preference, we let them enroll;
# they'll still be blocked when they try to access
# the courseware.
if course_id:
try:
course_key = CourseKey.from_string(course_id)
redirect_url = embargo_api.redirect_if_blocked(
course_key,
ip_address=get_ip(request),
url=request.path
)
if redirect_url:
redirect_to = embargo_api.message_url_path(course_key, "enrollment")
except InvalidKeyError:
pass
login_urls = auth_pipeline_urls( login_urls = auth_pipeline_urls(
third_party_auth.pipeline.AUTH_ENTRY_LOGIN, third_party_auth.pipeline.AUTH_ENTRY_LOGIN,
course_id=course_id, course_id=course_id,
...@@ -330,7 +366,8 @@ def _third_party_auth_context(request): ...@@ -330,7 +366,8 @@ def _third_party_auth_context(request):
register_urls = auth_pipeline_urls( register_urls = auth_pipeline_urls(
third_party_auth.pipeline.AUTH_ENTRY_REGISTER, third_party_auth.pipeline.AUTH_ENTRY_REGISTER,
course_id=course_id, course_id=course_id,
email_opt_in=email_opt_in email_opt_in=email_opt_in,
redirect_url=redirect_to
) )
if third_party_auth.is_enabled(): if third_party_auth.is_enabled():
......
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
${_("Unlink")} ${_("Unlink")}
</a> </a>
% else: % else:
<a href="${pipeline.get_login_url(state.provider.NAME, pipeline.AUTH_ENTRY_DASHBOARD)}"> <a href="${pipeline.get_login_url(state.provider.NAME, pipeline.AUTH_ENTRY_DASHBOARD, redirect_url='/')}">
## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn). ## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
${_("Link")} ${_("Link")}
</a> </a>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment