command to remove social auth users (#21280)

remove social auth users command
This commit is contained in:
Zachary Hancock
2019-08-06 15:48:19 -04:00
committed by GitHub
parent 1f028e0be3
commit 90c9103a89
4 changed files with 189 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
"""
Management command to remove social auth users. Intended for use in masters
integration sandboxes to allow partners reset users and enrollment data.
"""
from __future__ import absolute_import
import logging
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from six.moves import input
from third_party_auth.models import SAMLProviderConfig
log = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Management command to remove all social auth entries AND the corresponding edX
users for a given IDP.
Usage:
manage.py remove_social_auth_users gtx
"""
confirmation_prompt = "Type 'confirm' to continue with deletion\n"
def add_arguments(self, parser):
parser.add_argument('IDP', help='slug for the idp to remove all users from')
parser.add_argument(
'--force',
action='store_true',
help='Skip manual confirmation step before deleting objects',
)
@transaction.atomic
def handle(self, *args, **options):
slug = options['IDP']
if not settings.FEATURES.get('ENABLE_ENROLLMENT_RESET'):
raise CommandError('ENABLE_ENROLLMENT_RESET feature not enabled on this enviroment')
try:
SAMLProviderConfig.objects.current_set().get(slug=slug)
except SAMLProviderConfig.DoesNotExist:
raise CommandError(u'No SAML provider found for slug {}'.format(slug))
users = User.objects.filter(social_auth__provider=slug)
user_count = len(users)
count, models = users.delete()
log.info(
u'\n%s users and their related models will be deleted:\n%s\n',
user_count,
models,
)
if not options['force']:
confirmation = input(self.confirmation_prompt)
if confirmation != 'confirm':
raise CommandError('User confirmation required. No records have been modified')
log.info(u'Deleting %s records...', count)

View File

@@ -0,0 +1,119 @@
"""
Tests for `remove_social_auth_users` management command
"""
from __future__ import absolute_import
import sys
import unittest
from contextlib import contextmanager
from StringIO import StringIO
from uuid import uuid4
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import TestCase, override_settings
from social_django.models import UserSocialAuth
from student.models import User
from student.tests.factories import UserFactory
from third_party_auth.management.commands import remove_social_auth_users
from third_party_auth.tests.factories import SAMLProviderConfigFactory
FEATURES_WITH_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_ENABLED['ENABLE_ENROLLMENT_RESET'] = True
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestRemoveSocialAuthUsersCommand(TestCase):
"""
Test django management command
"""
@classmethod
def setUpClass(cls):
super(TestRemoveSocialAuthUsersCommand, cls).setUpClass()
cls.command = remove_social_auth_users.Command()
def setUp(self):
super(TestRemoveSocialAuthUsersCommand, self).setUp()
self.provider_hogwarts = SAMLProviderConfigFactory.create(slug='hogwarts')
self.provider_durmstrang = SAMLProviderConfigFactory.create(slug='durmstrang')
self.user_fleur = UserFactory(username='fleur') # no social auth
self.user_harry = UserFactory(username='harry') # social auth for Hogwarts
self.user_viktor = UserFactory(username='viktor') # social auth for Durmstrang
self.create_social_auth_entry(self.user_harry, self.provider_hogwarts)
self.create_social_auth_entry(self.user_viktor, self.provider_durmstrang)
@contextmanager
def _replace_stdin(self, text):
orig = sys.stdin
sys.stdin = StringIO(text)
yield
sys.stdin = orig
def create_social_auth_entry(self, user, provider):
external_id = uuid4()
UserSocialAuth.objects.create(
user=user,
uid='{0}:{1}'.format(provider.slug, external_id),
provider=provider.slug,
)
def find_user_social_auth_entry(self, username):
UserSocialAuth.objects.get(user__username=username)
@override_settings(FEATURES=FEATURES_WITH_ENABLED)
def test_remove_users(self):
call_command(self.command, self.provider_hogwarts.slug, force=True)
# user with input idp is removed, along with social auth entries
with self.assertRaises(User.DoesNotExist):
User.objects.get(username='harry')
with self.assertRaises(UserSocialAuth.DoesNotExist):
self.find_user_social_auth_entry('harry')
# other users intact
self.user_fleur.refresh_from_db()
self.user_viktor.refresh_from_db()
self.assertIsNotNone(self.user_fleur)
self.assertIsNotNone(self.user_viktor)
# other social auth intact
self.find_user_social_auth_entry(self.user_viktor.username)
@override_settings(FEATURES=FEATURES_WITH_ENABLED)
def test_invalid_idp(self):
invalid_slug = 'jedi-academy'
err_string = u'No SAML provider found for slug {}'.format(invalid_slug)
with self.assertRaisesRegexp(CommandError, err_string):
call_command(self.command, invalid_slug)
@override_settings(FEATURES=FEATURES_WITH_ENABLED)
def test_confirmation_required(self):
""" By default this command will require user input to confirm """
with self._replace_stdin('confirm'):
call_command(self.command, self.provider_hogwarts.slug)
with self.assertRaises(User.DoesNotExist):
User.objects.get(username='harry')
with self.assertRaises(UserSocialAuth.DoesNotExist):
self.find_user_social_auth_entry('harry')
@override_settings(FEATURES=FEATURES_WITH_ENABLED)
def test_confirmation_failure(self):
err_string = 'User confirmation required. No records have been modified'
with self.assertRaisesRegexp(CommandError, err_string):
with self._replace_stdin('no'):
call_command(self.command, self.provider_hogwarts.slug)
# no users should be removed
self.assertEqual(len(User.objects.all()), 3)
self.assertEqual(len(UserSocialAuth.objects.all()), 2)
def test_feature_default_disabled(self):
""" By default this command should not be enabled """
err_string = 'ENABLE_ENROLLMENT_RESET feature not enabled on this enviroment'
with self.assertRaisesRegexp(CommandError, err_string):
call_command(self.command, self.provider_hogwarts.slug, force=True)

View File

@@ -420,6 +420,9 @@ FEATURES = {
# Whether to display the account deletion section the account settings page
'ENABLE_ACCOUNT_DELETION': True,
# Enable feature to remove enrollments and users. Used to reset state of master's integration environments
'ENABLE_ENROLLMENT_RESET': False,
}
# Settings for the course reviews tool template and identification key, set either to None to disable course reviews

View File

@@ -222,6 +222,9 @@ FEATURES['STORE_BILLING_INFO'] = True
FEATURES['ENABLE_PAID_COURSE_REGISTRATION'] = True
FEATURES['ENABLE_COSMETIC_DISPLAY_PRICE'] = True
######################### Program Enrollments #####################
FEATURES['ENABLE_ENROLLMENT_RESET'] = True
########################## Third Party Auth #######################
if FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and 'third_party_auth.dummy.DummyBackend' not in AUTHENTICATION_BACKENDS: