diff --git a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py index abe7d89b6a..9f0122a246 100644 --- a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py +++ b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py @@ -94,11 +94,13 @@ class EdxOAuth2Validator(OAuth2Validator): def get_default_scopes(self, client_id, request, *args, **kwargs): """ If the request payload does not have `scopes` attribute for a grant_type of - client credentials, it should use available scopes as default. + client credentials, it should add `user_id` in the default scopes. """ + default_scopes = super().get_default_scopes(client_id, request, *args, **kwargs) if request.grant_type == 'client_credentials' and not request.scopes: - return get_scopes_backend().get_available_scopes(application=request.client, request=request) - return super().get_default_scopes(client_id, request, *args, **kwargs) + if get_scopes_backend().has_user_id_in_application_scopes(application=request.client): + default_scopes.append('user_id') + return default_scopes def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): """ diff --git a/openedx/core/djangoapps/oauth_dispatch/scopes.py b/openedx/core/djangoapps/oauth_dispatch/scopes.py index b9070ddc51..bcaf6df91f 100644 --- a/openedx/core/djangoapps/oauth_dispatch/scopes.py +++ b/openedx/core/djangoapps/oauth_dispatch/scopes.py @@ -22,3 +22,15 @@ class ApplicationModelScopes(SettingsScopes): default_scopes = self.get_default_scopes() all_scopes = list(self.get_all_scopes().keys()) return set(application_scopes + default_scopes).intersection(all_scopes) + + def has_user_id_in_application_scopes(self, application): + """ + Returns the user id associated with the given application. + """ + try: + application_scopes = ApplicationAccess.get_scopes(application) + if 'user_id' in application_scopes: + return True + except ApplicationAccess.DoesNotExist: + return False + return False diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/test_dot_overrides.py b/openedx/core/djangoapps/oauth_dispatch/tests/test_dot_overrides.py index dc38b18c6e..2bc4e92c1f 100644 --- a/openedx/core/djangoapps/oauth_dispatch/tests/test_dot_overrides.py +++ b/openedx/core/djangoapps/oauth_dispatch/tests/test_dot_overrides.py @@ -5,6 +5,7 @@ Test of custom django-oauth-toolkit behavior # pylint: disable=protected-access import datetime +from unittest import mock from django.conf import settings from django.test import RequestFactory, TestCase @@ -77,6 +78,40 @@ class CustomValidationTestCase(TestCase): request = self.request_factory.get('/') assert self.validator.validate_user('darkhelmet', self.TEST_PASSWORD, client=None, request=request) + @mock.patch( + 'openedx.core.djangoapps.oauth_dispatch.scopes.ApplicationModelScopes.has_user_id_in_application_scopes' + ) + @mock.patch('oauth2_provider.oauth2_validators.OAuth2Validator.get_default_scopes') + def test_get_updated_default_scopes(self, mock_get_default_scopes, mock_has_user_id_in_application_scopes): + """ + Test that get_default_scopes returns the default scopes plus the user_id scope if it's available. + """ + default_scopes = ['profile', 'email'] + mock_get_default_scopes.return_value = default_scopes.copy() + mock_has_user_id_in_application_scopes.return_value = True + + request = mock.Mock(grant_type='client_credentials', client=None, scopes=None) + overriden_default_scopes = self.validator.get_default_scopes(request=request, client_id='client_id') + + self.assertEqual(overriden_default_scopes, default_scopes + ['user_id']) + + @mock.patch( + 'openedx.core.djangoapps.oauth_dispatch.scopes.ApplicationModelScopes.has_user_id_in_application_scopes' + ) + @mock.patch('oauth2_provider.oauth2_validators.OAuth2Validator.get_default_scopes') + def test_get_default_scopes(self, mock_get_default_scopes, mock_has_user_id_in_application_scopes): + """ + Test that get_default_scopes returns the default scopes if user_id scope is not available. + """ + default_scopes = ['profile', 'email'] + mock_get_default_scopes.return_value = default_scopes.copy() + mock_has_user_id_in_application_scopes.return_value = False + + request = mock.Mock(grant_type='client_credentials', client=None, scopes=None) + overriden_default_scopes = self.validator.get_default_scopes(request=request, client_id='client_id') + + self.assertEqual(overriden_default_scopes, default_scopes) + @skip_unless_lms class CustomAuthorizationViewTestCase(TestCase): diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/test_scopes.py b/openedx/core/djangoapps/oauth_dispatch/tests/test_scopes.py index 5b68f6f14c..d4545758c1 100644 --- a/openedx/core/djangoapps/oauth_dispatch/tests/test_scopes.py +++ b/openedx/core/djangoapps/oauth_dispatch/tests/test_scopes.py @@ -30,4 +30,13 @@ class ApplicationModelScopesTestCase(TestCase): application_access = ApplicationAccessFactory(scopes=application_scopes) scopes = ApplicationModelScopes() assert set(scopes.get_available_scopes(application_access.application)) == \ - set(list(settings.OAUTH2_DEFAULT_SCOPES.keys()) + expected_additional_scopes) + set(list(settings.OAUTH2_DEFAULT_SCOPES.keys()) + expected_additional_scopes) + + def test_has_user_id_in_application_scopes(self): + """ Verify the settings backend correctly identifies whether the user_id scope is available. """ + application_access = ApplicationAccessFactory(scopes=['user_id']) + scopes = ApplicationModelScopes() + assert scopes.has_user_id_in_application_scopes(application_access.application) + application_access.scopes = [] + application_access.save() + assert not scopes.has_user_id_in_application_scopes(application_access.application)