command to remove social auth users (#21280)
remove social auth users command
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user