From 16120efaeb13ac7898f7ee5f285018f944844598 Mon Sep 17 00:00:00 2001 From: zubair-arbi Date: Tue, 12 Sep 2017 14:32:59 +0500 Subject: [PATCH] Initialize enterprise api client with provided user ENT-624 --- common/djangoapps/enrollment/views.py | 8 +- openedx/features/enterprise_support/api.py | 81 +++++++---- .../enterprise_support/tests/test_api.py | 127 ++++++++++++++---- 3 files changed, 157 insertions(+), 59 deletions(-) diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py index 1c41506a3e..8bd64cf177 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 ( - ConsentApiClient, - EnterpriseApiClient, + ConsentApiServiceClient, + EnterpriseApiServiceClient, 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 = EnterpriseApiClient() - consent_client = ConsentApiClient() + enterprise_api_client = EnterpriseApiServiceClient() + consent_client = ConsentApiServiceClient() # 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 d9548cb2f3..1f85e9bdef 100644 --- a/openedx/features/enterprise_support/api.py +++ b/openedx/features/enterprise_support/api.py @@ -1,25 +1,19 @@ """ 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 @@ -30,6 +24,7 @@ try: except ImportError: pass + CONSENT_FAILED_PARAMETER = 'consent_failed' LOGGER = logging.getLogger("edx.enterprise_helpers") @@ -46,12 +41,12 @@ class ConsentApiClient(object): Class for producing an Enterprise Consent service API client """ - def __init__(self): + def __init__(self, user): """ - Initialize a consent service API client, authenticated using the Enterprise worker username. + Initialize an authenticated Consent service API client by using the + provided user. """ - self.user = User.objects.get(username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME) - jwt = JwtBuilder(self.user).build_token([]) + jwt = JwtBuilder(user).build_token([]) url = configuration_helpers.get_value('ENTERPRISE_CONSENT_API_URL', settings.ENTERPRISE_CONSENT_API_URL) self.client = EdxRestApiClient( url, @@ -97,17 +92,38 @@ 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): + def __init__(self, user): """ - Initialize an Enterprise service API client, authenticated using the Enterprise worker username. + Initialize an authenticated Enterprise service API client by using the + provided user. """ - self.user = User.objects.get(username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME) - jwt = JwtBuilder(self.user).build_token([]) + jwt = JwtBuilder(user).build_token([]) self.client = EdxRestApiClient( configuration_helpers.get_value('ENTERPRISE_API_URL', settings.ENTERPRISE_API_URL), jwt=jwt @@ -241,6 +257,13 @@ 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: @@ -294,7 +317,7 @@ def enterprise_customer_for_request(request): if not enterprise_enabled(): return None - ec = None + enterprise_customer = None sso_provider_id = request.GET.get('tpa_hint') running_pipeline = get_partial_pipeline(request) @@ -311,34 +334,38 @@ 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. - ec_uuid = EnterpriseCustomer.objects.get( + enterprise_customer_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. - ec_uuid = None + enterprise_customer_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. - ec_uuid = request.GET.get('enterprise_customer') or request.COOKIES.get(settings.ENTERPRISE_CUSTOMER_COOKIE_NAME) + enterprise_customer_uuid = request.GET.get('enterprise_customer') or request.COOKIES.get( + settings.ENTERPRISE_CUSTOMER_COOKIE_NAME + ) - if not ec_uuid and request.user.is_authenticated(): + if not enterprise_customer_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: - ec_uuid = learner_data[0]['enterprise_customer']['uuid'] - if ec_uuid: + enterprise_customer_uuid = learner_data[0]['enterprise_customer']['uuid'] + if enterprise_customer_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: - ec = EnterpriseApiClient().get_enterprise_customer(ec_uuid) + enterprise_customer = EnterpriseApiClient(user=request.user).get_enterprise_customer( + enterprise_customer_uuid + ) except HttpNotFoundError: - ec = None + enterprise_customer = None - return ec + return enterprise_customer def consent_needed_for_course(request, user, course_id, enrollment_exists=False): @@ -358,7 +385,7 @@ def consent_needed_for_course(request, user, course_id, enrollment_exists=False) if not enterprise_learner_details: consent_needed = False else: - client = ConsentApiClient() + client = ConsentApiClient(user=request.user) consent_needed = any( client.consent_required( username=user.username, @@ -426,7 +453,7 @@ def get_enterprise_learner_data(site, user): if not enterprise_enabled(): return None - enterprise_learner_data = EnterpriseApiClient().fetch_enterprise_learner_data(site=site, user=user) + enterprise_learner_data = EnterpriseApiClient(user=user).fetch_enterprise_learner_data(site=site, user=user) if enterprise_learner_data: return enterprise_learner_data['results'] @@ -461,7 +488,7 @@ def get_dashboard_consent_notification(request, user, course_enrollments): enrollment = course_enrollment break - client = ConsentApiClient() + client = ConsentApiClient(user=request.user) 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 7d46e0c4f8..b53115afc8 100644 --- a/openedx/features/enterprise_support/tests/test_api.py +++ b/openedx/features/enterprise_support/tests/test_api.py @@ -8,16 +8,20 @@ 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, ) @@ -44,21 +48,86 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, TestCase): """ @classmethod def setUpTestData(cls): - UserFactory.create( - username='enterprise_worker', + cls.user = UserFactory.create( + username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME, 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={}) + request = mock.MagicMock(session={}, user=user) 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')) @@ -74,63 +143,65 @@ 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_ec_model, + mock_enterprise_customer_model, mock_get_el_data ): - def mock_get_ec(**kwargs): + def mock_get_enterprise_customer(**kwargs): uuid = kwargs.get('enterprise_customer_identity_provider__provider_id') if uuid: - return mock.MagicMock(uuid=uuid) + return mock.MagicMock(uuid=uuid, user=self.user) raise Exception - mock_ec_model.objects.get.side_effect = mock_get_ec - mock_ec_model.DoesNotExist = 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' - self.mock_get_enterprise_customer('real-ent-uuid', {"real": "enterprisecustomer"}, 200) + self.mock_get_enterprise_customer('real-ent-uuid', {'real': 'enterprisecustomer'}, 200) - ec = enterprise_customer_for_request(mock.MagicMock()) + enterprise_customer = enterprise_customer_for_request(dummy_request) - self.assertEqual(ec, {"real": "enterprisecustomer"}) + self.assertEqual(enterprise_customer, {'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) - ec = enterprise_customer_for_request(mock.MagicMock()) + enterprise_customer = enterprise_customer_for_request(dummy_request) - self.assertIsNone(ec) + self.assertIsNone(enterprise_customer) 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) - 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'}) + enterprise_customer = enterprise_customer_for_request( + mock.MagicMock(GET={'enterprise_customer': 'real-ent-uuid'}, user=self.user) ) - self.assertEqual(ec, {"real": "enterprisecustomer"}) + 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'}) 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) + enterprise_customer = enterprise_customer_for_request( + mock.MagicMock(GET={}, COOKIES={}, user=self.user, site=1) ) - self.assertEqual(ec, {"real": "enterprisecustomer"}) + self.assertEqual(enterprise_customer, {'real': 'enterprisecustomer'}) def check_data_sharing_consent(self, consent_required=False, consent_url=None): """