diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py index 8bd64cf177..1c41506a3e 100644 --- a/common/djangoapps/enrollment/views.py +++ b/common/djangoapps/enrollment/views.py @@ -30,8 +30,8 @@ from openedx.core.lib.api.permissions import ApiKeyHeaderPermission, ApiKeyHeade from openedx.core.lib.exceptions import CourseNotFoundError from openedx.core.lib.log_utils import audit_log from openedx.features.enterprise_support.api import ( - ConsentApiServiceClient, - EnterpriseApiServiceClient, + ConsentApiClient, + EnterpriseApiClient, EnterpriseApiException, enterprise_enabled ) @@ -598,8 +598,8 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): enterprise_course_consent = request.data.get('enterprise_course_consent') explicit_linked_enterprise = request.data.get('linked_enterprise_customer') if (enterprise_course_consent or explicit_linked_enterprise) and has_api_key_permissions and enterprise_enabled(): - enterprise_api_client = EnterpriseApiServiceClient() - consent_client = ConsentApiServiceClient() + enterprise_api_client = EnterpriseApiClient() + consent_client = ConsentApiClient() # We received an explicitly-linked EnterpriseCustomer for the enrollment if explicit_linked_enterprise is not None: try: diff --git a/openedx/features/enterprise_support/api.py b/openedx/features/enterprise_support/api.py index 1f85e9bdef..d9548cb2f3 100644 --- a/openedx/features/enterprise_support/api.py +++ b/openedx/features/enterprise_support/api.py @@ -1,19 +1,25 @@ """ APIs providing support for enterprise functionality. """ +import hashlib import logging from functools import wraps +import six from django.conf import settings from django.contrib.auth.models import User +from django.core.cache import cache from django.core.urlresolvers import reverse from django.shortcuts import redirect from django.template.loader import render_to_string from django.utils.http import urlencode from django.utils.translation import ugettext as _ from edx_rest_api_client.client import EdxRestApiClient +from requests.exceptions import ConnectionError, Timeout from slumber.exceptions import HttpClientError, HttpNotFoundError, HttpServerError, SlumberBaseException +from openedx.core.djangoapps.catalog.models import CatalogIntegration +from openedx.core.djangoapps.catalog.utils import create_catalog_api_client from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.lib.token_utils import JwtBuilder from third_party_auth.pipeline import get as get_partial_pipeline @@ -24,7 +30,6 @@ try: except ImportError: pass - CONSENT_FAILED_PARAMETER = 'consent_failed' LOGGER = logging.getLogger("edx.enterprise_helpers") @@ -41,12 +46,12 @@ class ConsentApiClient(object): Class for producing an Enterprise Consent service API client """ - def __init__(self, user): + def __init__(self): """ - Initialize an authenticated Consent service API client by using the - provided user. + Initialize a consent service API client, authenticated using the Enterprise worker username. """ - jwt = JwtBuilder(user).build_token([]) + self.user = User.objects.get(username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME) + jwt = JwtBuilder(self.user).build_token([]) url = configuration_helpers.get_value('ENTERPRISE_CONSENT_API_URL', settings.ENTERPRISE_CONSENT_API_URL) self.client = EdxRestApiClient( url, @@ -92,38 +97,17 @@ class ConsentApiClient(object): return response['consent_required'] -class EnterpriseServiceClientMixin(object): - """ - Class for initializing an Enterprise API clients with service user. - """ - - def __init__(self): - """ - Initialize an authenticated Enterprise API client by using the - Enterprise worker user by default. - """ - user = User.objects.get(username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME) - super(EnterpriseServiceClientMixin, self).__init__(user) - - -class ConsentApiServiceClient(EnterpriseServiceClientMixin, ConsentApiClient): - """ - Class for producing an Enterprise Consent API client with service user. - """ - pass - - class EnterpriseApiClient(object): """ Class for producing an Enterprise service API client. """ - def __init__(self, user): + def __init__(self): """ - Initialize an authenticated Enterprise service API client by using the - provided user. + Initialize an Enterprise service API client, authenticated using the Enterprise worker username. """ - jwt = JwtBuilder(user).build_token([]) + self.user = User.objects.get(username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME) + jwt = JwtBuilder(self.user).build_token([]) self.client = EdxRestApiClient( configuration_helpers.get_value('ENTERPRISE_API_URL', settings.ENTERPRISE_API_URL), jwt=jwt @@ -257,13 +241,6 @@ class EnterpriseApiClient(object): return response -class EnterpriseApiServiceClient(EnterpriseServiceClientMixin, EnterpriseApiClient): - """ - Class for producing an Enterprise service API client with service user. - """ - pass - - def data_sharing_consent_required(view_func): """ Decorator which makes a view method redirect to the Data Sharing Consent form if: @@ -317,7 +294,7 @@ def enterprise_customer_for_request(request): if not enterprise_enabled(): return None - enterprise_customer = None + ec = None sso_provider_id = request.GET.get('tpa_hint') running_pipeline = get_partial_pipeline(request) @@ -334,38 +311,34 @@ def enterprise_customer_for_request(request): # Check if there's an Enterprise Customer such that the linked SSO provider # has an ID equal to the ID we got from the running pipeline or from the # request tpa_hint URL parameter. - enterprise_customer_uuid = EnterpriseCustomer.objects.get( + ec_uuid = EnterpriseCustomer.objects.get( enterprise_customer_identity_provider__provider_id=sso_provider_id ).uuid except EnterpriseCustomer.DoesNotExist: # If there is not an EnterpriseCustomer linked to this SSO provider, set # the UUID variable to be null. - enterprise_customer_uuid = None + ec_uuid = None else: # Check if we got an Enterprise UUID passed directly as either a query # parameter, or as a value in the Enterprise cookie. - enterprise_customer_uuid = request.GET.get('enterprise_customer') or request.COOKIES.get( - settings.ENTERPRISE_CUSTOMER_COOKIE_NAME - ) + ec_uuid = request.GET.get('enterprise_customer') or request.COOKIES.get(settings.ENTERPRISE_CUSTOMER_COOKIE_NAME) - if not enterprise_customer_uuid and request.user.is_authenticated(): + if not ec_uuid and request.user.is_authenticated(): # If there's no way to get an Enterprise UUID for the request, check to see # if there's already an Enterprise attached to the requesting user on the backend. learner_data = get_enterprise_learner_data(request.site, request.user) if learner_data: - enterprise_customer_uuid = learner_data[0]['enterprise_customer']['uuid'] - if enterprise_customer_uuid: + ec_uuid = learner_data[0]['enterprise_customer']['uuid'] + if ec_uuid: # If we were able to obtain an EnterpriseCustomer UUID, go ahead # and use it to attempt to retrieve EnterpriseCustomer details # from the EnterpriseCustomer API. try: - enterprise_customer = EnterpriseApiClient(user=request.user).get_enterprise_customer( - enterprise_customer_uuid - ) + ec = EnterpriseApiClient().get_enterprise_customer(ec_uuid) except HttpNotFoundError: - enterprise_customer = None + ec = None - return enterprise_customer + return ec def consent_needed_for_course(request, user, course_id, enrollment_exists=False): @@ -385,7 +358,7 @@ def consent_needed_for_course(request, user, course_id, enrollment_exists=False) if not enterprise_learner_details: consent_needed = False else: - client = ConsentApiClient(user=request.user) + client = ConsentApiClient() consent_needed = any( client.consent_required( username=user.username, @@ -453,7 +426,7 @@ def get_enterprise_learner_data(site, user): if not enterprise_enabled(): return None - enterprise_learner_data = EnterpriseApiClient(user=user).fetch_enterprise_learner_data(site=site, user=user) + enterprise_learner_data = EnterpriseApiClient().fetch_enterprise_learner_data(site=site, user=user) if enterprise_learner_data: return enterprise_learner_data['results'] @@ -488,7 +461,7 @@ def get_dashboard_consent_notification(request, user, course_enrollments): enrollment = course_enrollment break - client = ConsentApiClient(user=request.user) + client = ConsentApiClient() consent_needed = client.consent_required( enterprise_customer_uuid=enterprise_customer['uuid'], username=user.username, diff --git a/openedx/features/enterprise_support/tests/test_api.py b/openedx/features/enterprise_support/tests/test_api.py index b53115afc8..7d46e0c4f8 100644 --- a/openedx/features/enterprise_support/tests/test_api.py +++ b/openedx/features/enterprise_support/tests/test_api.py @@ -8,20 +8,16 @@ import ddt import httpretty import mock from django.conf import settings -from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect from django.test import TestCase from django.test.utils import override_settings from openedx.features.enterprise_support.api import ( - ConsentApiClient, - ConsentApiServiceClient, consent_needed_for_course, data_sharing_consent_required, - EnterpriseApiClient, - EnterpriseApiServiceClient, enterprise_customer_for_request, + enterprise_enabled, get_dashboard_consent_notification, get_enterprise_consent_url, ) @@ -48,86 +44,21 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, TestCase): """ @classmethod def setUpTestData(cls): - cls.user = UserFactory.create( - username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME, + UserFactory.create( + username='enterprise_worker', email='ent_worker@example.com', password='password123', ) super(TestEnterpriseApi, cls).setUpTestData() - def _assert_api_service_client(self, api_client, mocked_jwt_builder): - """ - Verify that the provided api client uses the enterprise service user to generate - JWT token for auth. - """ - mocked_jwt_builder.return_value.build_token.return_value = 'test-token' - enterprise_service_user = User.objects.get(username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME) - enterprise_api_service_client = api_client() - - mocked_jwt_builder.assert_called_once_with(enterprise_service_user) - # pylint: disable=protected-access - self.assertEqual(enterprise_api_service_client.client._store['session'].auth.token, 'test-token') - - def _assert_api_client_with_user(self, api_client, mocked_jwt_builder): - """ - Verify that the provided api client uses the expected user to generate - JWT token for auth. - """ - mocked_jwt_builder.return_value.build_token.return_value = 'test-token' - dummy_enterprise_user = UserFactory.create( - username='dummy-enterprise-user', - email='dummy-enterprise-user@example.com', - password='password123', - ) - enterprise_api_service_client = api_client(dummy_enterprise_user) - - mocked_jwt_builder.assert_called_once_with(dummy_enterprise_user) - # pylint: disable=protected-access - self.assertEqual(enterprise_api_service_client.client._store['session'].auth.token, 'test-token') - - @httpretty.activate - @mock.patch('openedx.features.enterprise_support.api.JwtBuilder') - def test_enterprise_api_client_with_service_user(self, mock_jwt_builder): - """ - Verify that enterprise API service client uses enterprise service user - by default to authenticate and access enterprise API. - """ - self._assert_api_service_client(EnterpriseApiServiceClient, mock_jwt_builder) - - @httpretty.activate - @mock.patch('openedx.features.enterprise_support.api.JwtBuilder') - def test_enterprise_api_client_with_user(self, mock_jwt_builder): - """ - Verify that enterprise API client uses the provided user to - authenticate and access enterprise API. - """ - self._assert_api_client_with_user(EnterpriseApiClient, mock_jwt_builder) - - @httpretty.activate - @mock.patch('openedx.features.enterprise_support.api.JwtBuilder') - def test_enterprise_consent_api_client_with_service_user(self, mock_jwt_builder): - """ - Verify that enterprise API consent service client uses enterprise - service user by default to authenticate and access enterprise API. - """ - self._assert_api_service_client(ConsentApiServiceClient, mock_jwt_builder) - - @httpretty.activate - @mock.patch('openedx.features.enterprise_support.api.JwtBuilder') - def test_enterprise_consent_api_client_with_user(self, mock_jwt_builder): - """ - Verify that enterprise API consent service client uses the provided - user to authenticate and access enterprise API. - """ - self._assert_api_client_with_user(ConsentApiClient, mock_jwt_builder) - @httpretty.activate + @override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker') def test_consent_needed_for_course(self): user = mock.MagicMock( username='janedoe', is_authenticated=lambda: True, ) - request = mock.MagicMock(session={}, user=user) + request = mock.MagicMock(session={}) self.mock_enterprise_learner_api() self.mock_consent_missing(user.username, 'fake-course', 'cf246b88-d5f6-4908-a522-fc307e0b0c59') self.assertTrue(consent_needed_for_course(request, user, 'fake-course')) @@ -143,65 +74,63 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, TestCase): @mock.patch('openedx.features.enterprise_support.api.EnterpriseCustomer') @mock.patch('openedx.features.enterprise_support.api.get_partial_pipeline') @mock.patch('openedx.features.enterprise_support.api.Registry') + @override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker') def test_enterprise_customer_for_request( self, mock_registry, mock_partial, - mock_enterprise_customer_model, + mock_ec_model, mock_get_el_data ): - def mock_get_enterprise_customer(**kwargs): + def mock_get_ec(**kwargs): uuid = kwargs.get('enterprise_customer_identity_provider__provider_id') if uuid: - return mock.MagicMock(uuid=uuid, user=self.user) + return mock.MagicMock(uuid=uuid) 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_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) + self.mock_get_enterprise_customer('real-ent-uuid', {"real": "enterprisecustomer"}, 200) - enterprise_customer = enterprise_customer_for_request(dummy_request) + ec = enterprise_customer_for_request(mock.MagicMock()) - self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) + self.assertEqual(ec, {"real": "enterprisecustomer"}) httpretty.reset() - self.mock_get_enterprise_customer('real-ent-uuid', {'detail': 'Not found.'}, 404) + self.mock_get_enterprise_customer('real-ent-uuid', {"detail": "Not found."}, 404) - enterprise_customer = enterprise_customer_for_request(dummy_request) + ec = enterprise_customer_for_request(mock.MagicMock()) - self.assertIsNone(enterprise_customer) + 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) + 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) + 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(enterprise_customer, {'real': 'enterprisecustomer'}) - - 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'}) + self.assertEqual(ec, {"real": "enterprisecustomer"}) mock_get_el_data.return_value = [{'enterprise_customer': {'uuid': 'real-ent-uuid'}}] - enterprise_customer = enterprise_customer_for_request( - mock.MagicMock(GET={}, COOKIES={}, user=self.user, site=1) + ec = enterprise_customer_for_request( + mock.MagicMock(GET={}, COOKIES={}, user=mock.MagicMock(is_authenticated=lambda: True), site=1) ) - self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) + self.assertEqual(ec, {"real": "enterprisecustomer"}) def check_data_sharing_consent(self, consent_required=False, consent_url=None): """