Commit ff4d9e43 by Clinton Blackburn

Using shared secret for JWTs sent to Credentials API

This change brings the Credentials API calls in line with those of other services. The change also makes it easier for the future switch to an asymmetric signing key.

LEARNER-629
parent f4e72c80
...@@ -338,7 +338,7 @@ def get_user_orders(user): ...@@ -338,7 +338,7 @@ def get_user_orders(user):
cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None
api = ecommerce_api_client(user) api = ecommerce_api_client(user)
commerce_user_orders = get_edx_api_data( commerce_user_orders = get_edx_api_data(
commerce_configuration, user, 'orders', api=api, querystring=user_query, cache_key=cache_key commerce_configuration, 'orders', api=api, querystring=user_query, cache_key=cache_key
) )
for order in commerce_user_orders: for order in commerce_user_orders:
......
...@@ -198,7 +198,7 @@ class TestGetCourseRuns(CatalogIntegrationMixin, TestCase): ...@@ -198,7 +198,7 @@ class TestGetCourseRuns(CatalogIntegrationMixin, TestCase):
""" """
args, kwargs = call_args args, kwargs = call_args
for arg in (self.catalog_integration, self.user, 'course_runs'): for arg in (self.catalog_integration, 'course_runs'):
self.assertIn(arg, args) self.assertIn(arg, args)
self.assertEqual(kwargs['api']._store['base_url'], self.catalog_integration.internal_api_url) # pylint: disable=protected-access self.assertEqual(kwargs['api']._store['base_url'], self.catalog_integration.internal_api_url) # pylint: disable=protected-access
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import copy import copy
import logging import logging
import waffle
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
...@@ -11,7 +10,6 @@ from openedx.core.djangoapps.catalog.models import CatalogIntegration ...@@ -11,7 +10,6 @@ from openedx.core.djangoapps.catalog.models import CatalogIntegration
from openedx.core.lib.edx_api_utils import get_edx_api_data from openedx.core.lib.edx_api_utils import get_edx_api_data
from openedx.core.lib.token_utils import JwtBuilder from openedx.core.lib.token_utils import JwtBuilder
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
User = get_user_model() # pylint: disable=invalid-name User = get_user_model() # pylint: disable=invalid-name
...@@ -67,11 +65,10 @@ def get_programs(uuid=None, types=None): # pylint: disable=redefined-builtin ...@@ -67,11 +65,10 @@ def get_programs(uuid=None, types=None): # pylint: disable=redefined-builtin
return get_edx_api_data( return get_edx_api_data(
catalog_integration, catalog_integration,
user,
'programs', 'programs',
api=api,
resource_id=uuid, resource_id=uuid,
cache_key=cache_key if catalog_integration.is_cache_enabled else None, cache_key=cache_key if catalog_integration.is_cache_enabled else None,
api=api,
querystring=querystring, querystring=querystring,
) )
else: else:
...@@ -98,13 +95,8 @@ def get_program_types(name=None): ...@@ -98,13 +95,8 @@ def get_program_types(name=None):
api = create_catalog_api_client(user, catalog_integration) api = create_catalog_api_client(user, catalog_integration)
cache_key = '{base}.program_types'.format(base=catalog_integration.CACHE_KEY) cache_key = '{base}.program_types'.format(base=catalog_integration.CACHE_KEY)
data = get_edx_api_data( data = get_edx_api_data(catalog_integration, 'program_types', api=api,
catalog_integration, cache_key=cache_key if catalog_integration.is_cache_enabled else None)
user,
'program_types',
cache_key=cache_key if catalog_integration.is_cache_enabled else None,
api=api
)
# Filter by name if a name was provided # Filter by name if a name was provided
if name: if name:
...@@ -169,12 +161,6 @@ def get_course_runs(): ...@@ -169,12 +161,6 @@ def get_course_runs():
'exclude_utm': 1, 'exclude_utm': 1,
} }
course_runs = get_edx_api_data( course_runs = get_edx_api_data(catalog_integration, 'course_runs', api=api, querystring=querystring)
catalog_integration,
user,
'course_runs',
api=api,
querystring=querystring,
)
return course_runs return course_runs
"""Helper functions for working with Credentials.""" """Helper functions for working with Credentials."""
from __future__ import unicode_literals from __future__ import unicode_literals
import logging import logging
from django.conf import settings
from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.djangoapps.catalog.utils import get_programs from openedx.core.djangoapps.catalog.utils import get_programs
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.lib.edx_api_utils import get_edx_api_data from openedx.core.lib.edx_api_utils import get_edx_api_data
from openedx.core.lib.token_utils import JwtBuilder
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_credentials_api_client(user):
""" Returns an authenticated Credentials API client. """
scopes = ['email', 'profile']
expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
jwt = JwtBuilder(user).build_token(scopes, expires_in)
return EdxRestApiClient(CredentialsApiConfig.current().internal_api_url, jwt=jwt)
def get_credentials(user, program_uuid=None): def get_credentials(user, program_uuid=None):
""" """
Given a user, get credentials earned from the credentials service. Given a user, get credentials earned from the credentials service.
...@@ -35,9 +48,10 @@ def get_credentials(user, program_uuid=None): ...@@ -35,9 +48,10 @@ def get_credentials(user, program_uuid=None):
# want to see them displayed immediately. # want to see them displayed immediately.
use_cache = credential_configuration.is_cache_enabled and not user.is_staff use_cache = credential_configuration.is_cache_enabled and not user.is_staff
cache_key = credential_configuration.CACHE_KEY + '.' + user.username if use_cache else None cache_key = credential_configuration.CACHE_KEY + '.' + user.username if use_cache else None
api = get_credentials_api_client(user)
return get_edx_api_data( return get_edx_api_data(
credential_configuration, user, 'credentials', querystring=querystring, cache_key=cache_key credential_configuration, 'credentials', api=api, querystring=querystring, cache_key=cache_key
) )
......
...@@ -14,15 +14,14 @@ from openedx.core.lib.token_utils import JwtBuilder ...@@ -14,15 +14,14 @@ from openedx.core.lib.token_utils import JwtBuilder
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_edx_api_data(api_config, user, resource, api=None, resource_id=None, def get_edx_api_data(api_config, resource, api, resource_id=None, querystring=None, cache_key=None, many=True,
querystring=None, cache_key=None, many=True, traverse_pagination=True): traverse_pagination=True):
"""GET data from an edX REST API. """GET data from an edX REST API.
DRY utility for handling caching and pagination. DRY utility for handling caching and pagination.
Arguments: Arguments:
api_config (ConfigurationModel): The configuration model governing interaction with the API. api_config (ConfigurationModel): The configuration model governing interaction with the API.
user (User): The user to authenticate as when requesting data.
resource (str): Name of the API resource being requested. resource (str): Name of the API resource being requested.
Keyword Arguments: Keyword Arguments:
...@@ -53,27 +52,6 @@ def get_edx_api_data(api_config, user, resource, api=None, resource_id=None, ...@@ -53,27 +52,6 @@ def get_edx_api_data(api_config, user, resource, api=None, resource_id=None,
return cached return cached
try: try:
if not api:
# TODO: Use the system's JWT_AUDIENCE and JWT_SECRET_KEY instead of client ID and name.
client_name = api_config.OAUTH2_CLIENT_NAME
try:
client = Client.objects.get(name=client_name)
except Client.DoesNotExist:
raise ImproperlyConfigured(
'OAuth2 Client with name [{}] does not exist.'.format(client_name)
)
scopes = ['email', 'profile']
expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
jwt = JwtBuilder(user, secret=client.client_secret).build_token(scopes, expires_in, aud=client.client_id)
api = EdxRestApiClient(api_config.internal_api_url, jwt=jwt)
except: # pylint: disable=bare-except
log.exception('Failed to initialize the %s API client.', api_config.API_NAME)
return no_data
try:
endpoint = getattr(api, resource) endpoint = getattr(api, resource)
querystring = querystring if querystring else {} querystring = querystring if querystring else {}
response = endpoint(resource_id).get(**querystring) response = endpoint(resource_id).get(**querystring)
......
...@@ -65,7 +65,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -65,7 +65,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
) )
with mock.patch('openedx.core.lib.edx_api_utils.EdxRestApiClient.__init__') as mock_init: with mock.patch('openedx.core.lib.edx_api_utils.EdxRestApiClient.__init__') as mock_init:
actual_collection = get_edx_api_data(catalog_integration, self.user, 'programs', api=api) actual_collection = get_edx_api_data(catalog_integration, 'programs', api=api)
# Verify that the helper function didn't initialize its own client. # Verify that the helper function didn't initialize its own client.
self.assertFalse(mock_init.called) self.assertFalse(mock_init.called)
...@@ -96,7 +96,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -96,7 +96,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
self._mock_catalog_api(responses) self._mock_catalog_api(responses)
actual_collection = get_edx_api_data(catalog_integration, self.user, 'programs', api=api) actual_collection = get_edx_api_data(catalog_integration, 'programs', api=api)
self.assertEqual(actual_collection, expected_collection) self.assertEqual(actual_collection, expected_collection)
self._assert_num_requests(len(expected_collection)) self._assert_num_requests(len(expected_collection))
...@@ -125,9 +125,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -125,9 +125,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
[httpretty.Response(body=json.dumps(body), content_type='application/json') for body in responses] [httpretty.Response(body=json.dumps(body), content_type='application/json') for body in responses]
) )
actual_collection = get_edx_api_data( actual_collection = get_edx_api_data(catalog_integration, 'programs', api=api, traverse_pagination=False)
catalog_integration, self.user, 'programs', api=api, traverse_pagination=False,
)
self.assertEqual(actual_collection, expected_response) self.assertEqual(actual_collection, expected_response)
self._assert_num_requests(1) self._assert_num_requests(1)
...@@ -149,7 +147,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -149,7 +147,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
url=url url=url
) )
actual_resource = get_edx_api_data(catalog_integration, self.user, 'programs', api=api, resource_id=resource_id) actual_resource = get_edx_api_data(catalog_integration, 'programs', api=api, resource_id=resource_id)
self.assertEqual(actual_resource, expected_resource) self.assertEqual(actual_resource, expected_resource)
self._assert_num_requests(1) self._assert_num_requests(1)
...@@ -178,7 +176,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -178,7 +176,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
url=url url=url
) )
actual_resource = get_edx_api_data(catalog_integration, self.user, 'programs', api=api, resource_id=resource_id) actual_resource = get_edx_api_data(catalog_integration, 'programs', api=api, resource_id=resource_id)
self.assertEqual(actual_resource, expected_resource) self.assertEqual(actual_resource, expected_resource)
self._assert_num_requests(1) self._assert_num_requests(1)
...@@ -214,17 +212,15 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -214,17 +212,15 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
cache_key = CatalogIntegration.current().CACHE_KEY cache_key = CatalogIntegration.current().CACHE_KEY
# Warm up the cache. # Warm up the cache.
get_edx_api_data(catalog_integration, self.user, 'programs', api=api, cache_key=cache_key) get_edx_api_data(catalog_integration, 'programs', api=api, cache_key=cache_key)
get_edx_api_data( get_edx_api_data(catalog_integration, 'programs', api=api, resource_id=resource_id, cache_key=cache_key)
catalog_integration, self.user, 'programs', api=api, resource_id=resource_id, cache_key=cache_key
)
# Hit the cache. # Hit the cache.
actual_collection = get_edx_api_data(catalog_integration, self.user, 'programs', api=api, cache_key=cache_key) actual_collection = get_edx_api_data(catalog_integration, 'programs', api=api, cache_key=cache_key)
self.assertEqual(actual_collection, expected_collection) self.assertEqual(actual_collection, expected_collection)
actual_resource = get_edx_api_data( actual_resource = get_edx_api_data(
catalog_integration, self.user, 'programs', api=api, resource_id=resource_id, cache_key=cache_key catalog_integration, 'programs', api=api, resource_id=resource_id, cache_key=cache_key
) )
self.assertEqual(actual_resource, expected_resource) self.assertEqual(actual_resource, expected_resource)
...@@ -236,24 +232,11 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -236,24 +232,11 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
"""Verify that no data is retrieved if the provided config model is disabled.""" """Verify that no data is retrieved if the provided config model is disabled."""
catalog_integration = self.create_catalog_integration(enabled=False) catalog_integration = self.create_catalog_integration(enabled=False)
actual = get_edx_api_data(catalog_integration, self.user, 'programs') actual = get_edx_api_data(catalog_integration, 'programs', api=None)
self.assertTrue(mock_warning.called) self.assertTrue(mock_warning.called)
self.assertEqual(actual, []) self.assertEqual(actual, [])
@mock.patch('edx_rest_api_client.client.EdxRestApiClient.__init__')
@mock.patch(UTILITY_MODULE + '.log.exception')
def test_client_initialization_failure(self, mock_exception, mock_init):
"""Verify that an exception is logged when the API client fails to initialize."""
mock_init.side_effect = Exception
catalog_integration = self.create_catalog_integration()
actual = get_edx_api_data(catalog_integration, self.user, 'programs')
self.assertTrue(mock_exception.called)
self.assertEqual(actual, [])
@mock.patch(UTILITY_MODULE + '.log.exception') @mock.patch(UTILITY_MODULE + '.log.exception')
def test_data_retrieval_failure(self, mock_exception): def test_data_retrieval_failure(self, mock_exception):
"""Verify that an exception is logged when data can't be retrieved.""" """Verify that an exception is logged when data can't be retrieved."""
...@@ -264,7 +247,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -264,7 +247,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
[httpretty.Response(body='clunk', content_type='application/json', status_code=500)] [httpretty.Response(body='clunk', content_type='application/json', status_code=500)]
) )
actual = get_edx_api_data(catalog_integration, self.user, 'programs', api=api) actual = get_edx_api_data(catalog_integration, 'programs', api=api)
self.assertTrue(mock_exception.called) self.assertTrue(mock_exception.called)
self.assertEqual(actual, []) self.assertEqual(actual, [])
...@@ -276,33 +259,15 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -276,33 +259,15 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
actual = get_edx_api_data( actual = get_edx_api_data(
catalog_integration, catalog_integration,
self.user,
'programs', 'programs',
api=None,
resource_id=100, resource_id=100,
many=False, many=False
) )
self.assertTrue(mock_warning.called) self.assertTrue(mock_warning.called)
self.assertEqual(actual, {}) self.assertEqual(actual, {})
@mock.patch('edx_rest_api_client.client.EdxRestApiClient.__init__')
@mock.patch(UTILITY_MODULE + '.log.exception')
def test_client_initialization_failure_with_id(self, mock_exception, mock_init):
"""Verify that an exception is logged when the API client fails to initialize."""
mock_init.side_effect = Exception
catalog_integration = self.create_catalog_integration()
actual = get_edx_api_data(
catalog_integration,
self.user,
'programs',
resource_id=100,
many=False,
)
self.assertTrue(mock_exception.called)
self.assertEqual(actual, {})
@mock.patch(UTILITY_MODULE + '.log.exception') @mock.patch(UTILITY_MODULE + '.log.exception')
def test_data_retrieval_failure_with_id(self, mock_exception): def test_data_retrieval_failure_with_id(self, mock_exception):
"""Verify that an exception is logged when data can't be retrieved.""" """Verify that an exception is logged when data can't be retrieved."""
...@@ -315,21 +280,10 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -315,21 +280,10 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
actual = get_edx_api_data( actual = get_edx_api_data(
catalog_integration, catalog_integration,
self.user,
'programs', 'programs',
api=api, api=api,
resource_id=100, resource_id=100,
many=False, many=False
) )
self.assertTrue(mock_exception.called) self.assertTrue(mock_exception.called)
self.assertEqual(actual, {}) self.assertEqual(actual, {})
def test_api_client_not_provided(self):
"""Verify that an API client doesn't need to be provided."""
ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
credentials_api_config = self.create_credentials_config()
with mock.patch('openedx.core.lib.edx_api_utils.EdxRestApiClient.__init__') as mock_init:
get_edx_api_data(credentials_api_config, self.user, 'credentials')
self.assertTrue(mock_init.called)
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