Commit fd07dea0 by Zubair Afzal Committed by GitHub

Merge pull request #16001 from edx/zub/ENT-624-access-enterprise-api-with-request-user

Initialize enterprise api client with provided user
parents d62e2498 16120efa
......@@ -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:
......
"""
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,
......
......@@ -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'}))
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'})
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={}, COOKIES={settings.ENTERPRISE_CUSTOMER_COOKIE_NAME: 'real-ent-uuid'}, user=self.user)
)
self.assertEqual(ec, {"real": "enterprisecustomer"})
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):
"""
......
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