From 30d8c1a5600c8f0e2f80a1ac06a3580d20a5a951 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Thu, 26 Oct 2023 20:48:50 +0530 Subject: [PATCH 01/24] feat: renamed DeprecatedRestApiClient from EdxRestApiClient --- lms/djangoapps/commerce/tests/__init__.py | 2 +- openedx/core/djangoapps/commerce/utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index 367cefae2cd8..d8bda2ab4eb8 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -25,7 +25,7 @@ } -class EdxRestApiClientTest(TestCase): +class DeprecatedRestApiClientTest(TestCase): """ Tests to ensure the client is initialized properly. """ diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index 5bb908043a0f..fa11c2eeaff2 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -4,7 +4,7 @@ import requests from django.conf import settings from edx_rest_api_client.auth import SuppliedJwtAuth -from edx_rest_api_client.client import EdxRestApiClient +from edx_rest_api_client.client import DeprecatedRestApiClient from eventtracking import tracker from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user @@ -53,7 +53,7 @@ def ecommerce_api_client(user, session=None): ] jwt = create_jwt_for_user(user, additional_claims=claims, scopes=scopes) - return EdxRestApiClient( + return DeprecatedRestApiClient( configuration_helpers.get_value('ECOMMERCE_API_URL', settings.ECOMMERCE_API_URL), jwt=jwt, session=session From c907655bd71ecb13519e97007fed100be59a811e Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 1 Nov 2023 16:56:11 +0530 Subject: [PATCH 02/24] feat: renamed DeprecatedRestApiClient --- openedx/core/djangoapps/commerce/utils.py | 69 ++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index fa11c2eeaff2..0411f18111b5 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -2,14 +2,17 @@ import requests +import slumber from django.conf import settings -from edx_rest_api_client.auth import SuppliedJwtAuth -from edx_rest_api_client.client import DeprecatedRestApiClient +from edx_rest_api_client.auth import BearerAuth, JwtAuth, SuppliedJwtAuth from eventtracking import tracker from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from edx_django_utils.monitoring import set_custom_attribute +REQUEST_CONNECT_TIMEOUT = 3.05 +REQUEST_READ_TIMEOUT = 5 ECOMMERCE_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' @@ -76,3 +79,65 @@ def get_ecommerce_api_client(user): client.auth = SuppliedJwtAuth(jwt) return client + + +class DeprecatedRestApiClient(slumber.API): + """ + API client for edX REST API. + + (deprecated) See docs/decisions/0002-oauth-api-client-replacement.rst. + """ + + @classmethod + def user_agent(cls): + return USER_AGENT + + @classmethod + def get_oauth_access_token(cls, url, client_id, client_secret, token_type='bearer', + timeout=(REQUEST_CONNECT_TIMEOUT, REQUEST_READ_TIMEOUT)): + # 'To help transition to OAuthAPIClient, use DeprecatedRestApiClient.get_and_cache_jwt_oauth_access_token instead' + # 'of DeprecatedRestApiClient.get_oauth_access_token to share cached jwt token used by OAuthAPIClient.' + return get_oauth_access_token(url, client_id, client_secret, token_type=token_type, timeout=timeout) + + @classmethod + def get_and_cache_jwt_oauth_access_token(cls, url, client_id, client_secret, + timeout=(REQUEST_CONNECT_TIMEOUT, REQUEST_READ_TIMEOUT)): + return get_and_cache_oauth_access_token(url, client_id, client_secret, token_type="jwt", timeout=timeout) + + def __init__(self, url, signing_key=None, username=None, full_name=None, email=None, + timeout=5, issuer=None, expires_in=30, tracking_context=None, oauth_access_token=None, + session=None, jwt=None, **kwargs): + """ + DeprecatedRestApiClient is deprecated. Use OAuthAPIClient instead. + + Instantiate a new client. You can pass extra kwargs to Slumber like + 'append_slash'. + + Raises: + ValueError: If a URL is not provided. + + """ + set_custom_attribute('api_client', 'DeprecatedRestApiClient') + if not url: + raise ValueError('An API url must be supplied!') + + if jwt: + auth = SuppliedJwtAuth(jwt) + elif oauth_access_token: + auth = BearerAuth(oauth_access_token) + elif signing_key and username: + auth = JwtAuth(username, full_name, email, signing_key, + issuer=issuer, expires_in=expires_in, tracking_context=tracking_context) + else: + auth = None + + session = session or requests.Session() + session.headers['User-Agent'] = self.user_agent() + + session.timeout = timeout + super().__init__( + url, + session=session, + auth=auth, + **kwargs + ) From 933c9223a2de320923a2a3181b467bd54a3c6924 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 1 Nov 2023 18:13:01 +0530 Subject: [PATCH 03/24] feat: add undefined-variable --- openedx/core/djangoapps/commerce/utils.py | 158 ++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index 0411f18111b5..4f8d6c708c2f 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -5,6 +5,7 @@ import slumber from django.conf import settings from edx_rest_api_client.auth import BearerAuth, JwtAuth, SuppliedJwtAuth +from edx_rest_api_client.__version__ import __version__ from eventtracking import tracker from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user @@ -16,6 +17,163 @@ ECOMMERCE_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' +def user_agent(): + """ + Return a User-Agent that identifies this client. + + Example: + python-requests/2.9.1 edx-rest-api-client/1.7.2 ecommerce + + The last item in the list will be the application name, taken from the + OS environment variable EDX_REST_API_CLIENT_NAME. If that environment + variable is not set, it will default to the hostname. + """ + client_name = 'unknown_client_name' + try: + client_name = os.environ.get("EDX_REST_API_CLIENT_NAME") or socket.gethostbyname(socket.gethostname()) + except: # pylint: disable=bare-except + pass # using 'unknown_client_name' is good enough. no need to log. + return "{} edx-rest-api-client/{} {}".format( + requests.utils.default_user_agent(), # e.g. "python-requests/2.9.1" + __version__, # version of this client + client_name + ) + + +USER_AGENT = user_agent() + + +def _get_oauth_url(url): + """ + Returns the complete url for the oauth2 endpoint. + + Args: + url (str): base url of the LMS oauth endpoint, which can optionally include some or all of the path + ``/oauth2/access_token``. Common example settings that would work for ``url`` would include: + LMS_BASE_URL = 'http://edx.devstack.lms:18000' + BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL = 'http://edx.devstack.lms:18000/oauth2' + + """ + stripped_url = url.rstrip('/') + if stripped_url.endswith('/access_token'): + return url + + if stripped_url.endswith('/oauth2'): + return stripped_url + '/access_token' + + return stripped_url + '/oauth2/access_token' + + +def get_oauth_access_token(url, client_id, client_secret, token_type='jwt', grant_type='client_credentials', + refresh_token=None, + timeout=(REQUEST_CONNECT_TIMEOUT, REQUEST_READ_TIMEOUT)): + """ + Retrieves OAuth 2.0 access token using the given grant type. + + Args: + url (str): Oauth2 access token endpoint, optionally including part of the path. + client_id (str): client ID + client_secret (str): client secret + Kwargs: + token_type (str): Type of token to return. Options include bearer and jwt. + grant_type (str): One of 'client_credentials' or 'refresh_token' + refresh_token (str): The previous access token (for grant_type=refresh_token) + + Raises: + requests.RequestException if there is a problem retrieving the access token. + + Returns: + tuple: Tuple containing (access token string, expiration datetime). + + """ + now = datetime.datetime.utcnow() + data = { + 'grant_type': grant_type, + 'client_id': client_id, + 'client_secret': client_secret, + 'token_type': token_type, + } + if refresh_token: + data['refresh_token'] = refresh_token + else: + assert grant_type != 'refresh_token', "refresh_token parameter required" + + response = requests.post( + _get_oauth_url(url), + data=data, + headers={ + 'User-Agent': USER_AGENT, + }, + timeout=timeout + ) + + response.raise_for_status() # Raise an exception for bad status codes. + try: + data = response.json() + access_token = data['access_token'] + expires_in = data['expires_in'] + except (KeyError, json.decoder.JSONDecodeError) as json_error: + raise requests.RequestException(response=response) from json_error + + expires_at = now + datetime.timedelta(seconds=expires_in) + + return access_token, expires_at + + +def get_and_cache_oauth_access_token(url, client_id, client_secret, token_type='jwt', grant_type='client_credentials', + refresh_token=None, + timeout=(REQUEST_CONNECT_TIMEOUT, REQUEST_READ_TIMEOUT)): + """ + Retrieves a possibly cached OAuth 2.0 access token using the given grant type. + + See ``get_oauth_access_token`` for usage details. + + First retrieves the access token from the cache and ensures it has not expired. If + the access token either wasn't found in the cache, or was expired, retrieves a new + access token and caches it for the lifetime of the token. + + Note: Consider tokens to be expired ACCESS_TOKEN_EXPIRED_THRESHOLD_SECONDS early + to ensure the token won't expire while it is in use. + + Returns: + tuple: Tuple containing (access token string, expiration datetime). + + """ + oauth_url = _get_oauth_url(url) + cache_key = 'edx_rest_api_client.access_token.{}.{}.{}.{}'.format( + token_type, + grant_type, + client_id, + oauth_url, + ) + cached_response = TieredCache.get_cached_response(cache_key) + + # Attempt to get an unexpired cached access token + if cached_response.is_found: + _, expiration = cached_response.value + # Double-check the token hasn't already expired as a safety net. + adjusted_expiration = expiration - datetime.timedelta(seconds=ACCESS_TOKEN_EXPIRED_THRESHOLD_SECONDS) + if datetime.datetime.utcnow() < adjusted_expiration: + return cached_response.value + + # Get a new access token if no unexpired access token was found in the cache. + oauth_access_token_response = get_oauth_access_token( + oauth_url, + client_id, + client_secret, + grant_type=grant_type, + refresh_token=refresh_token, + timeout=timeout, + ) + + # Cache the new access token with an expiration matching the lifetime of the token. + _, expiration = oauth_access_token_response + expires_in = (expiration - datetime.datetime.utcnow()).seconds - ACCESS_TOKEN_EXPIRED_THRESHOLD_SECONDS + TieredCache.set_all_tiers(cache_key, oauth_access_token_response, expires_in) + + return oauth_access_token_response + + def create_tracking_context(user): """ Assembles attributes from user and request objects to be sent along in E-Commerce API calls for tracking purposes. """ From ea643df41a9de59776b6bdb14b7a00a70d50f2a3 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 1 Nov 2023 18:38:45 +0530 Subject: [PATCH 04/24] feat: add undefined-variable --- openedx/core/djangoapps/commerce/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index 4f8d6c708c2f..a0e619666689 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -3,17 +3,29 @@ import requests import slumber +import datetime +import json +import os +import socket from django.conf import settings from edx_rest_api_client.auth import BearerAuth, JwtAuth, SuppliedJwtAuth from edx_rest_api_client.__version__ import __version__ +from edx_django_utils.cache import TieredCache from eventtracking import tracker from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from edx_django_utils.monitoring import set_custom_attribute +# When caching tokens, use this value to err on expiring tokens a little early so they are +# sure to be valid at the time they are used. +ACCESS_TOKEN_EXPIRED_THRESHOLD_SECONDS = 5 + +# How long should we wait to connect to the auth service. +# https://requests.readthedocs.io/en/master/user/advanced/#timeouts REQUEST_CONNECT_TIMEOUT = 3.05 REQUEST_READ_TIMEOUT = 5 + ECOMMERCE_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' From a1b9bd8726a3bdc1ea325894aa0dc0af48c3a092 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Thu, 2 Nov 2023 00:19:33 +0530 Subject: [PATCH 05/24] feat: update utils.py --- openedx/core/djangoapps/commerce/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index a0e619666689..bac81263facb 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -265,8 +265,13 @@ def user_agent(cls): @classmethod def get_oauth_access_token(cls, url, client_id, client_secret, token_type='bearer', timeout=(REQUEST_CONNECT_TIMEOUT, REQUEST_READ_TIMEOUT)): - # 'To help transition to OAuthAPIClient, use DeprecatedRestApiClient.get_and_cache_jwt_oauth_access_token instead' - # 'of DeprecatedRestApiClient.get_oauth_access_token to share cached jwt token used by OAuthAPIClient.' + """ + To help transition to OAuthAPIClient, use DeprecatedRestApiClient. + get_and_cache_jwt_oauth_access_token instead' + + 'of DeprecatedRestApiClient.get_oauth_access_token to share cached jwt token used by OAuthAPIClient.' + + """ return get_oauth_access_token(url, client_id, client_secret, token_type=token_type, timeout=timeout) @classmethod From 2f0833e8a1857b7a633a6aab148664fff4a04693 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Thu, 2 Nov 2023 15:22:00 +0530 Subject: [PATCH 06/24] feat: add test file for DeprecatedRestApiClient --- lms/djangoapps/commerce/tests/__init__.py | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index d8bda2ab4eb8..610d862ffa89 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -5,9 +5,14 @@ from urllib.parse import urljoin import httpretty +import ddt +import requests from django.conf import settings from django.test import TestCase from freezegun import freeze_time +from edx_rest_api_client import __version__ +from edx_rest_api_client.auth import JwtAuth +from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client @@ -26,6 +31,83 @@ class DeprecatedRestApiClientTest(TestCase): + """ + Tests for the edX Rest API client. + """ + + @ddt.unpack + @ddt.data( + ({'url': URL, 'signing_key': SIGNING_KEY, 'username': USERNAME, + 'full_name': FULL_NAME, 'email': EMAIL}, JwtAuth), + ({'url': URL, 'signing_key': SIGNING_KEY, 'username': USERNAME, 'full_name': None, 'email': EMAIL}, JwtAuth), + ({'url': URL, 'signing_key': SIGNING_KEY, 'username': USERNAME, + 'full_name': FULL_NAME, 'email': None}, JwtAuth), + ({'url': URL, 'signing_key': SIGNING_KEY, 'username': USERNAME, 'full_name': None, 'email': None}, JwtAuth), + ({'url': URL, 'signing_key': SIGNING_KEY, 'username': USERNAME}, JwtAuth), + ({'url': URL, 'signing_key': None, 'username': USERNAME}, type(None)), + ({'url': URL, 'signing_key': SIGNING_KEY, 'username': None}, type(None)), + ({'url': URL, 'signing_key': None, 'username': None, 'oauth_access_token': None}, type(None)) + ) + def test_valid_configuration(self, kwargs, auth_type): + """ + The constructor should return successfully if all arguments are valid. + We also check that the auth type of the api is what we expect. + """ + api = DeprecatedRestApiClient(**kwargs) + self.assertIsInstance(api._store['session'].auth, auth_type) # pylint: disable=protected-access + + @ddt.data( + {'url': None, 'signing_key': SIGNING_KEY, 'username': USERNAME}, + {'url': None, 'signing_key': None, 'username': None, 'oauth_access_token': None}, + ) + def test_invalid_configuration(self, kwargs): + """ + If the constructor arguments are invalid, an InvalidConfigurationError should be raised. + """ + self.assertRaises(ValueError, DeprecatedRestApiClient, **kwargs) + + @mock.patch('edx_rest_api_client.auth.JwtAuth.__init__', return_value=None) + def test_tracking_context(self, mock_auth): + """ + Ensure the tracking context is included with API requests if specified. + """ + DeprecatedRestApiClient(URL, SIGNING_KEY, USERNAME, FULL_NAME, EMAIL, tracking_context=TRACKING_CONTEXT) + self.assertIn(TRACKING_CONTEXT, mock_auth.call_args[1].values()) + + def test_oauth2(self): + """ + Ensure OAuth2 authentication is used when an access token is supplied to the constructor. + """ + + with mock.patch('edx_rest_api_client.auth.BearerAuth.__init__', return_value=None) as mock_auth: + DeprecatedRestApiClient(URL, oauth_access_token=ACCESS_TOKEN) + mock_auth.assert_called_with(ACCESS_TOKEN) + + def test_supplied_jwt(self): + """Ensure JWT authentication is used when a JWT is supplied to the constructor.""" + with mock.patch('edx_rest_api_client.auth.SuppliedJwtAuth.__init__', return_value=None) as mock_auth: + DeprecatedRestApiClient(URL, jwt=JWT) + mock_auth.assert_called_with(JWT) + + def test_user_agent(self): + """Make sure our custom User-Agent is getting built correctly.""" + with mock.patch('socket.gethostbyname', return_value='test_hostname'): + default_user_agent = user_agent() + self.assertIn('python-requests', default_user_agent) + self.assertIn(f'edx-rest-api-client/{__version__}', default_user_agent) + self.assertIn('test_hostname', default_user_agent) + + with mock.patch('socket.gethostbyname') as mock_gethostbyname: + mock_gethostbyname.side_effect = ValueError() + default_user_agent = user_agent() + self.assertIn('unknown_client_name', default_user_agent) + + with mock.patch.dict(os.environ, {'EDX_REST_API_CLIENT_NAME': "awesome_app"}): + uagent = user_agent() + self.assertIn('awesome_app', uagent) + + self.assertEqual(user_agent(), DeprecatedRestApiClient.user_agent()) + """ Tests to ensure the client is initialized properly. """ From a99e8c1c50d0e8281071d7529e97dbea270ff4b5 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Thu, 2 Nov 2023 15:35:12 +0530 Subject: [PATCH 07/24] feat: update __init__.py --- lms/djangoapps/commerce/tests/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index 610d862ffa89..65bb02b8478c 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -18,6 +18,14 @@ from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user +URL = 'http://example.com/api/v2' +SIGNING_KEY = 'edx' +USERNAME = 'edx' +FULL_NAME = 'édx äpp' +EMAIL = 'edx@example.com' +TRACKING_CONTEXT = {'foo': 'bar'} +ACCESS_TOKEN = 'abc123' +JWT = 'abc.123.doremi' JSON = 'application/json' TEST_PUBLIC_URL_ROOT = 'http://www.example.com' TEST_API_URL = 'http://www-internal.example.com/api' From bb34690d30286c479c7b87c16d2287ea4a9c1e0a Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Thu, 2 Nov 2023 18:43:51 +0530 Subject: [PATCH 08/24] feat: update __init__.py and undefined-variable --- lms/djangoapps/commerce/tests/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index 65bb02b8478c..03a1d2dbde7c 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -12,7 +12,7 @@ from freezegun import freeze_time from edx_rest_api_client import __version__ from edx_rest_api_client.auth import JwtAuth -from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient +from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient, user_agent from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client @@ -116,9 +116,7 @@ def test_user_agent(self): self.assertEqual(user_agent(), DeprecatedRestApiClient.user_agent()) - """ - Tests to ensure the client is initialized properly. - """ + # Tests to ensure the client is initialized properly. SCOPES = [ 'user_id', 'email', From 4cd7f38cbac6d94faa51763d0207a9615a94a5fa Mon Sep 17 00:00:00 2001 From: Yagnesh Nayi <127923546+Yagnesh1998@users.noreply.github.com> Date: Thu, 2 Nov 2023 18:45:42 +0530 Subject: [PATCH 09/24] feat: Update __init__.py --- lms/djangoapps/commerce/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index 03a1d2dbde7c..910291ae7e07 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -12,7 +12,7 @@ from freezegun import freeze_time from edx_rest_api_client import __version__ from edx_rest_api_client.auth import JwtAuth -from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient, user_agent +from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient, user_agent from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client From f614c8cb98a0eb140ae237165399b189a048a64a Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Thu, 2 Nov 2023 19:55:59 +0530 Subject: [PATCH 10/24] feat: Renamed functions and add undefined-variable --- lms/djangoapps/commerce/tests/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index 910291ae7e07..122f71ee5dcb 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -6,6 +6,7 @@ import httpretty import ddt +import os import requests from django.conf import settings from django.test import TestCase @@ -75,7 +76,7 @@ def test_invalid_configuration(self, kwargs): self.assertRaises(ValueError, DeprecatedRestApiClient, **kwargs) @mock.patch('edx_rest_api_client.auth.JwtAuth.__init__', return_value=None) - def test_tracking_context(self, mock_auth): + def test_tracking_contexts(self, mock_auth): """ Ensure the tracking context is included with API requests if specified. """ From 9ec5bd2e38b3d8f82d8299a71bb5b00cfd49ffd6 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Thu, 2 Nov 2023 22:32:00 +0530 Subject: [PATCH 11/24] feat: add new test class --- lms/djangoapps/commerce/tests/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index 122f71ee5dcb..cafa9e68ffd7 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -39,7 +39,8 @@ } -class DeprecatedRestApiClientTest(TestCase): +@ddt.ddt +class DeprecatedRestApiClientTests(TestCase): """ Tests for the edX Rest API client. """ @@ -76,7 +77,7 @@ def test_invalid_configuration(self, kwargs): self.assertRaises(ValueError, DeprecatedRestApiClient, **kwargs) @mock.patch('edx_rest_api_client.auth.JwtAuth.__init__', return_value=None) - def test_tracking_contexts(self, mock_auth): + def test_tracking_context(self, mock_auth): """ Ensure the tracking context is included with API requests if specified. """ @@ -117,7 +118,11 @@ def test_user_agent(self): self.assertEqual(user_agent(), DeprecatedRestApiClient.user_agent()) - # Tests to ensure the client is initialized properly. + +class DeprecatedRestApiClientTest(TestCase): + """ + Tests to ensure the client is initialized properly. + """ SCOPES = [ 'user_id', 'email', From e149fb3c5d4bd1ebaa41685e26994d01d9989037 Mon Sep 17 00:00:00 2001 From: Yagnesh Nayi <127923546+Yagnesh1998@users.noreply.github.com> Date: Thu, 2 Nov 2023 22:35:52 +0530 Subject: [PATCH 12/24] feat: Update function name --- lms/djangoapps/commerce/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index cafa9e68ffd7..fc7cd9b49ee4 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -77,7 +77,7 @@ def test_invalid_configuration(self, kwargs): self.assertRaises(ValueError, DeprecatedRestApiClient, **kwargs) @mock.patch('edx_rest_api_client.auth.JwtAuth.__init__', return_value=None) - def test_tracking_context(self, mock_auth): + def test_tracking_contexts(self, mock_auth): """ Ensure the tracking context is included with API requests if specified. """ From 7b97592745d665fa7bb9beb8958023ee9631215e Mon Sep 17 00:00:00 2001 From: Yagnesh Nayi <127923546+Yagnesh1998@users.noreply.github.com> Date: Thu, 2 Nov 2023 22:59:18 +0530 Subject: [PATCH 13/24] feat: Update __init__.py --- lms/djangoapps/commerce/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index fc7cd9b49ee4..cafa9e68ffd7 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -77,7 +77,7 @@ def test_invalid_configuration(self, kwargs): self.assertRaises(ValueError, DeprecatedRestApiClient, **kwargs) @mock.patch('edx_rest_api_client.auth.JwtAuth.__init__', return_value=None) - def test_tracking_contexts(self, mock_auth): + def test_tracking_context(self, mock_auth): """ Ensure the tracking context is included with API requests if specified. """ From 6f29776126fb26921521b0795928543c1f584b25 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 8 Nov 2023 18:54:54 +0530 Subject: [PATCH 14/24] feat: add JwtAuth and BearerAuth --- openedx/core/djangoapps/commerce/utils.py | 74 ++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index bac81263facb..9549f749d5ad 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -8,12 +8,13 @@ import os import socket from django.conf import settings -from edx_rest_api_client.auth import BearerAuth, JwtAuth, SuppliedJwtAuth +from edx_rest_api_client.auth import SuppliedJwtAuth from edx_rest_api_client.__version__ import __version__ from edx_django_utils.cache import TieredCache from eventtracking import tracker from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user +from requests.auth import AuthBase from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from edx_django_utils.monitoring import set_custom_attribute @@ -29,6 +30,77 @@ ECOMMERCE_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' +# pylint: disable=line-too-long +class JwtAuth(AuthBase): + """ + Attaches JWT Authentication to the given Request object. + + Deprecated: + See https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0008-use-asymmetric-jwts.rst + + Todos: + * Remove pyjwt dependency from edx-rest-api-client when this class is + removed. + * This class is only used by ecomworker according to data supplied by + the `deprecated_jwt_signing` custom attribute. This class should be + moved to ecomworker and out of the shared library until it can be + removed completely. The new class should rename + `deprecated_jwt_signing` to `deprecated_ecomworker_jwt_signing` + to ensure the transition. + + """ + + def __init__(self, username, full_name, email, signing_key, issuer=None, expires_in=30, tracking_context=None): + self.issuer = issuer + self.expires_in = expires_in + self.username = username + self.email = email + self.full_name = full_name + self.signing_key = signing_key + self.tracking_context = tracking_context + + def __call__(self, r): + now = datetime.datetime.utcnow() + data = { + 'username': self.username, + 'full_name': self.full_name, + 'email': self.email, + 'iat': now, + } + + if self.issuer: + data['iss'] = self.issuer + + if self.expires_in: + data['exp'] = now + datetime.timedelta(seconds=self.expires_in) + + if self.tracking_context is not None: + data['tracking_context'] = self.tracking_context + + set_custom_attribute('deprecated_jwt_signing', 'JwtAuth') + r.headers['Authorization'] = 'JWT {jwt}'.format(jwt=jwt.encode(data, self.signing_key)) + return r + + +class BearerAuth(AuthBase): + """ + Attaches Bearer Authentication to the given Request object. + """ + + def __init__(self, token): + """ + Instantiate the auth class. + """ + self.token = token + + def __call__(self, r): + """ + Update the request headers. + """ + r.headers['Authorization'] = f'Bearer {self.token}' + return r + + def user_agent(): """ Return a User-Agent that identifies this client. From 2fe5b5b8ab005a030a0bf07807705c37862e71e8 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 8 Nov 2023 19:30:46 +0530 Subject: [PATCH 15/24] feat: update __init__.py --- lms/djangoapps/commerce/tests/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index cafa9e68ffd7..d82c3ad50b23 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -12,8 +12,7 @@ from django.test import TestCase from freezegun import freeze_time from edx_rest_api_client import __version__ -from edx_rest_api_client.auth import JwtAuth -from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient, user_agent +from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient, user_agent, JwtAuth from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client From ba94b3d5d7ebde13e16b8a2c842f74485d31ebb9 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 8 Nov 2023 19:34:34 +0530 Subject: [PATCH 16/24] feat: update utils.py --- openedx/core/djangoapps/commerce/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index 9549f749d5ad..20297fa74196 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -7,6 +7,7 @@ import json import os import socket +import jwt from django.conf import settings from edx_rest_api_client.auth import SuppliedJwtAuth from edx_rest_api_client.__version__ import __version__ From 55d6550a8c6aa0d9a614e2bc53971191dcb941ae Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 8 Nov 2023 20:08:14 +0530 Subject: [PATCH 17/24] feat: update utils.py --- openedx/core/djangoapps/commerce/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index 20297fa74196..9549f749d5ad 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -7,7 +7,6 @@ import json import os import socket -import jwt from django.conf import settings from edx_rest_api_client.auth import SuppliedJwtAuth from edx_rest_api_client.__version__ import __version__ From 9d0ceda32bcce091e37ef315be86d7ef8cbac1b1 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 8 Nov 2023 20:17:21 +0530 Subject: [PATCH 18/24] feat: update patch --- lms/djangoapps/commerce/tests/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index d82c3ad50b23..248fa8827c6b 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -75,7 +75,7 @@ def test_invalid_configuration(self, kwargs): """ self.assertRaises(ValueError, DeprecatedRestApiClient, **kwargs) - @mock.patch('edx_rest_api_client.auth.JwtAuth.__init__', return_value=None) + @mock.patch('openedx.core.djangoapps.commerce.utils.JwtAuth.__init__', return_value=None) def test_tracking_context(self, mock_auth): """ Ensure the tracking context is included with API requests if specified. @@ -88,7 +88,7 @@ def test_oauth2(self): Ensure OAuth2 authentication is used when an access token is supplied to the constructor. """ - with mock.patch('edx_rest_api_client.auth.BearerAuth.__init__', return_value=None) as mock_auth: + with mock.patch('openedx.core.djangoapps.commerce.utils.BearerAuth.__init__', return_value=None) as mock_auth: DeprecatedRestApiClient(URL, oauth_access_token=ACCESS_TOKEN) mock_auth.assert_called_with(ACCESS_TOKEN) From 059da11688c496ac3e34e25113fcc4a762fb26be Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 8 Nov 2023 20:54:06 +0530 Subject: [PATCH 19/24] feat: update changes --- lms/djangoapps/commerce/tests/__init__.py | 5 ++- openedx/core/djangoapps/commerce/utils.py | 54 +---------------------- 2 files changed, 4 insertions(+), 55 deletions(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index 248fa8827c6b..af18d74af69a 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -12,7 +12,8 @@ from django.test import TestCase from freezegun import freeze_time from edx_rest_api_client import __version__ -from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient, user_agent, JwtAuth +from edx_rest_api_client.auth import JwtAuth +from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient, user_agent from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client @@ -75,7 +76,7 @@ def test_invalid_configuration(self, kwargs): """ self.assertRaises(ValueError, DeprecatedRestApiClient, **kwargs) - @mock.patch('openedx.core.djangoapps.commerce.utils.JwtAuth.__init__', return_value=None) + @mock.patch('edx_rest_api_client.auth.JwtAuth.__init__', return_value=None) def test_tracking_context(self, mock_auth): """ Ensure the tracking context is included with API requests if specified. diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index 9549f749d5ad..84b63d339e60 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -8,7 +8,7 @@ import os import socket from django.conf import settings -from edx_rest_api_client.auth import SuppliedJwtAuth +from edx_rest_api_client.auth import SuppliedJwtAuth, JwtAuth from edx_rest_api_client.__version__ import __version__ from edx_django_utils.cache import TieredCache from eventtracking import tracker @@ -30,58 +30,6 @@ ECOMMERCE_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' -# pylint: disable=line-too-long -class JwtAuth(AuthBase): - """ - Attaches JWT Authentication to the given Request object. - - Deprecated: - See https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0008-use-asymmetric-jwts.rst - - Todos: - * Remove pyjwt dependency from edx-rest-api-client when this class is - removed. - * This class is only used by ecomworker according to data supplied by - the `deprecated_jwt_signing` custom attribute. This class should be - moved to ecomworker and out of the shared library until it can be - removed completely. The new class should rename - `deprecated_jwt_signing` to `deprecated_ecomworker_jwt_signing` - to ensure the transition. - - """ - - def __init__(self, username, full_name, email, signing_key, issuer=None, expires_in=30, tracking_context=None): - self.issuer = issuer - self.expires_in = expires_in - self.username = username - self.email = email - self.full_name = full_name - self.signing_key = signing_key - self.tracking_context = tracking_context - - def __call__(self, r): - now = datetime.datetime.utcnow() - data = { - 'username': self.username, - 'full_name': self.full_name, - 'email': self.email, - 'iat': now, - } - - if self.issuer: - data['iss'] = self.issuer - - if self.expires_in: - data['exp'] = now + datetime.timedelta(seconds=self.expires_in) - - if self.tracking_context is not None: - data['tracking_context'] = self.tracking_context - - set_custom_attribute('deprecated_jwt_signing', 'JwtAuth') - r.headers['Authorization'] = 'JWT {jwt}'.format(jwt=jwt.encode(data, self.signing_key)) - return r - - class BearerAuth(AuthBase): """ Attaches Bearer Authentication to the given Request object. From 03eb270b5e956ae8e49706749145816d81fd3b21 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Wed, 8 Nov 2023 21:12:40 +0530 Subject: [PATCH 20/24] feat: add version --- lms/djangoapps/commerce/tests/__init__.py | 2 +- openedx/core/djangoapps/commerce/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index af18d74af69a..f96852a74cc5 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -11,7 +11,6 @@ from django.conf import settings from django.test import TestCase from freezegun import freeze_time -from edx_rest_api_client import __version__ from edx_rest_api_client.auth import JwtAuth from openedx.core.djangoapps.commerce.utils import DeprecatedRestApiClient, user_agent @@ -19,6 +18,7 @@ from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user +__version__ = '5.6.1' URL = 'http://example.com/api/v2' SIGNING_KEY = 'edx' USERNAME = 'edx' diff --git a/openedx/core/djangoapps/commerce/utils.py b/openedx/core/djangoapps/commerce/utils.py index 84b63d339e60..1d0c5d0924a6 100644 --- a/openedx/core/djangoapps/commerce/utils.py +++ b/openedx/core/djangoapps/commerce/utils.py @@ -9,7 +9,6 @@ import socket from django.conf import settings from edx_rest_api_client.auth import SuppliedJwtAuth, JwtAuth -from edx_rest_api_client.__version__ import __version__ from edx_django_utils.cache import TieredCache from eventtracking import tracker @@ -25,6 +24,7 @@ # How long should we wait to connect to the auth service. # https://requests.readthedocs.io/en/master/user/advanced/#timeouts REQUEST_CONNECT_TIMEOUT = 3.05 +__version__ = '5.6.1' REQUEST_READ_TIMEOUT = 5 ECOMMERCE_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' From 9625159afdc0a6c2dc706ed25afd0dddcf152649 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Tue, 12 Dec 2023 22:42:52 +0530 Subject: [PATCH 21/24] feat: add slumber --- requirements/edx/kernel.in | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 1c727d2f9c2c..383af59c1268 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -159,3 +159,4 @@ webob web-fragments # Provides the ability to render fragments of web pages XBlock[django] # Courseware component architecture xss-utils # https://github.com/edx/edx-platform/pull/20633 Fix XSS via Translations +slumber \ No newline at end of file From d9ec06dc59578ce245dfa58797a72595d11a19fd Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Tue, 12 Dec 2023 22:46:00 +0530 Subject: [PATCH 22/24] feat: update kernel.in --- requirements/edx/kernel.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 383af59c1268..5321ac0eb7d7 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -159,4 +159,4 @@ webob web-fragments # Provides the ability to render fragments of web pages XBlock[django] # Courseware component architecture xss-utils # https://github.com/edx/edx-platform/pull/20633 Fix XSS via Translations -slumber \ No newline at end of file +slumber From a317fdbe8f3d7bccd50d5c3b9f9372c26f512477 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Tue, 12 Dec 2023 22:53:05 +0530 Subject: [PATCH 23/24] feat:update kernel.in --- requirements/edx/kernel.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 5321ac0eb7d7..1786d745f485 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -159,4 +159,4 @@ webob web-fragments # Provides the ability to render fragments of web pages XBlock[django] # Courseware component architecture xss-utils # https://github.com/edx/edx-platform/pull/20633 Fix XSS via Translations -slumber +slumber # The following dependency is unsupported and used by the DeprecatedRestApiClient From 001e3636240590ae29a9d80c0ecc98c71d8a7ab1 Mon Sep 17 00:00:00 2001 From: Yagnesh Date: Tue, 12 Dec 2023 23:15:15 +0530 Subject: [PATCH 24/24] feat: update alphabetical order --- requirements/edx/kernel.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 1786d745f485..744f1bd632ce 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -148,6 +148,7 @@ social-auth-core simplejson Shapely # Geometry library, used for image click regions in capa six # Utilities for supporting Python 2 & 3 in the same codebase +slumber # The following dependency is unsupported and used by the DeprecatedRestApiClient social-auth-app-django sorl-thumbnail sortedcontainers # Provides SortedKeyList, used for lists of XBlock assets @@ -159,4 +160,3 @@ webob web-fragments # Provides the ability to render fragments of web pages XBlock[django] # Courseware component architecture xss-utils # https://github.com/edx/edx-platform/pull/20633 Fix XSS via Translations -slumber # The following dependency is unsupported and used by the DeprecatedRestApiClient