diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py index 709009f06b78..330b5f4721db 100644 --- a/common/djangoapps/student/helpers.py +++ b/common/djangoapps/student/helpers.py @@ -15,15 +15,19 @@ from django.contrib.auth import load_backend from django.contrib.auth.models import User from django.db import IntegrityError, transaction +from django.db.models import Subquery from django.utils import http from django.utils.translation import ugettext as _ from oauth2_provider.models import AccessToken as dot_access_token from oauth2_provider.models import RefreshToken as dot_refresh_token +from provider.constants import CONFIDENTIAL from provider.oauth2.models import AccessToken as dop_access_token +from provider.oauth2.models import Client from provider.oauth2.models import RefreshToken as dop_refresh_token from pytz import UTC from six import iteritems, text_type import third_party_auth +from edx_oauth2_provider.models import TrustedClient from course_modes.models import CourseMode from lms.djangoapps.certificates.api import ( get_certificate_url, @@ -376,8 +380,22 @@ def destroy_oauth_tokens(user): """ Destroys ALL OAuth access and refresh tokens for the given user. """ - dop_access_token.objects.filter(user=user.id).delete() - dop_refresh_token.objects.filter(user=user.id).delete() + dop_access_query = dop_access_token.objects.filter(user=user.id) + dop_refresh_query = dop_refresh_token.objects.filter(user=user.id) + + if settings.FEATURES.get('KEEP_TRUSTED_CONFIDENTIAL_CLIENT_TOKENS', False): + # Appsembler: Avoid deleting the trusted confidential clients such as the Appsembler Management Console + trusted_clients = Client.objects.filter( + client_type=CONFIDENTIAL, + pk__in=Subquery(TrustedClient.objects.all().values('id')), + ) + + dop_access_query = dop_access_query.exclude(client__in=trusted_clients) + dop_refresh_query = dop_refresh_query.exclude(client__in=trusted_clients) + + dop_access_query.delete() + dop_refresh_query.delete() + dot_access_token.objects.filter(user=user.id).delete() dot_refresh_token.objects.filter(user=user.id).delete() diff --git a/common/djangoapps/student/tests/test_helpers.py b/common/djangoapps/student/tests/test_helpers.py index 3a47dcebbdaf..d52996ca806f 100644 --- a/common/djangoapps/student/tests/test_helpers.py +++ b/common/djangoapps/student/tests/test_helpers.py @@ -13,7 +13,18 @@ from mock import patch from testfixtures import LogCapture -from student.helpers import get_next_url_for_login_page +from student.helpers import destroy_oauth_tokens, get_next_url_for_login_page +from student.tests.factories import UserFactory +from edx_oauth2_provider.models import TrustedClient +from edx_oauth2_provider.tests.factories import ( + TrustedClientFactory, + AccessTokenFactory, + ClientFactory, + RefreshTokenFactory, +) +from provider.constants import CONFIDENTIAL, PUBLIC +from provider.oauth2.models import AccessToken, RefreshToken + from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context LOGGER_NAME = "student.helpers" @@ -110,3 +121,66 @@ def validate_login(): with with_site_configuration_context(configuration=dict(THIRD_PARTY_AUTH_HINT=tpa_hint)): validate_login() + + +@patch.dict(settings.FEATURES, {'KEEP_TRUSTED_CONFIDENTIAL_CLIENT_TOKENS': True}) +class TestDestroyOAuthTokensHelper(TestCase): + def setUp(self): + super(TestDestroyOAuthTokensHelper, self).setUp() + self.user = UserFactory.create() + self.client = ClientFactory(logout_uri='https://amc.example.com/logout/', client_type=CONFIDENTIAL) + access_token = AccessTokenFactory.create(user=self.user, client=self.client) + RefreshTokenFactory.create(user=self.user, client=self.client, access_token=access_token) + + def assert_destroy_behaviour(self, should_be_kept, message): + """ + Helper to test the `destroy_oauth_tokens` behaviour. + """ + assert AccessToken.objects.count() # Sanity check + assert RefreshToken.objects.count() # Sanity check + destroy_oauth_tokens(self.user) + assert should_be_kept == AccessToken.objects.count(), message + assert should_be_kept == RefreshToken.objects.count(), message + + @patch.dict(settings.FEATURES, {'KEEP_TRUSTED_CONFIDENTIAL_CLIENT_TOKENS': False}) + def test_confidential_trusted_client_feature_disabled(self): + """ + Tokens that have a confidential TrustedClient should be removed if the feature is disabled + """ + TrustedClientFactory.create(client=self.client) + self.assert_destroy_behaviour( + should_be_kept=False, + message='Tokens of a trusted confidential client should be deleted if the feature is disabled', + ) + + def test_confidential_trusted_client(self): + """ + Tokens that have a confidential TrustedClient shouldn't be removed. + """ + TrustedClientFactory.create(client=self.client) + self.assert_destroy_behaviour( + should_be_kept=True, + message='Tokens of a trusted confidential client should be kept', + ) + + def test_no_trusted_client(self): + """ + Only tokens that don't have a TrustedClient should be removed. + """ + assert not TrustedClient.objects.count() # 'Sanity check, there should not be a client' + self.assert_destroy_behaviour( + should_be_kept=False, + message='Tokens of an untrusted client should be deleted', + ) + + def test_public_trusted_client(self): + """ + Tokens for public clients are removed, even if they're trusted. + """ + self.client.client_type = PUBLIC + self.client.save() + TrustedClientFactory.create(client=self.client) + self.assert_destroy_behaviour( + should_be_kept=False, + message='Tokens of a public trusted client should be deleted', + )