Skip to content
Snippets Groups Projects
Unverified Commit d4a338d4 authored by sanfordstudent's avatar sanfordstudent Committed by GitHub
Browse files

Merge pull request #18113 from edx/sofiya/ed-2802-2

EDUCATOR-2802 Different LMS api endpoint to finish GDPR retirement for the user
parents 572b8a0b cd182061
No related branches found
Tags release-2018-05-22-13.11
No related merge requests found
......@@ -966,6 +966,7 @@ INSTALLED_APPS = [
# Standard apps
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.humanize',
'django.contrib.redirects',
'django.contrib.sessions',
'django.contrib.sites',
......@@ -980,6 +981,9 @@ INSTALLED_APPS = [
# Common views
'openedx.core.djangoapps.common_views',
# API access administration
'openedx.core.djangoapps.api_admin',
# History tables
'simple_history',
......@@ -1045,7 +1049,13 @@ INSTALLED_APPS = [
# Dark-launching languages
'openedx.core.djangoapps.dark_lang',
#
# User preferences
'wiki',
'django_notify',
'course_wiki', # Our customizations
'mptt',
'sekizai',
'openedx.core.djangoapps.user_api',
'django_openid_auth',
......
......@@ -317,9 +317,6 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
INSTALLED_APPS.append('openedx.core.djangoapps.ccxcon.apps.CCXConnectorConfig')
FEATURES['CUSTOM_COURSES_EDX'] = True
# API access management -- needed for simple-history to run.
INSTALLED_APPS.append('openedx.core.djangoapps.api_admin')
########################## VIDEO IMAGE STORAGE ############################
VIDEO_IMAGE_SETTINGS = dict(
VIDEO_IMAGE_MAX_BYTES=2 * 1024 * 1024, # 2 MB
......
......@@ -12,6 +12,7 @@ from consent.models import DataSharingConsent
import ddt
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.test import TestCase
......@@ -35,10 +36,18 @@ from rest_framework import status
from rest_framework.test import APIClient, APITestCase
from six import text_type
from social_django.models import UserSocialAuth
from wiki.models import ArticleRevision, Article
from wiki.models.pluginbase import RevisionPluginRevision, RevisionPlugin
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from entitlements.models import CourseEntitlementSupportDetail
from entitlements.tests.factories import CourseEntitlementFactory
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
from openedx.core.djangoapps.credit.models import (
CreditRequirementStatus, CreditRequest, CreditCourse, CreditProvider, CreditRequirement
)
from openedx.core.djangoapps.course_groups.models import CourseUserGroup, UnregisteredLearnerCohortAssignments
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.user_api.accounts import ACCOUNT_VISIBILITY_PREF_KEY
......@@ -47,9 +56,14 @@ from openedx.core.djangoapps.user_api.models import RetirementState, UserRetirem
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
from openedx.core.lib.token_utils import JwtBuilder
from survey.models import SurveyAnswer
from student.models import (
CourseEnrollment,
CourseEnrollmentAllowed,
ManualEnrollmentAudit,
PasswordHistory,
PendingEmailChange,
PendingNameChange,
Registration,
SocialLink,
UserProfile,
......@@ -1918,3 +1932,124 @@ class TestAccountRetirementPost(RetirementTestCase):
self.assertEqual(self.test_user, self.photo_verification.user)
for field in ('name', 'face_image_url', 'photo_id_image_url', 'photo_id_key'):
self.assertEqual('', getattr(self.photo_verification, field))
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Account APIs are only supported in LMS')
class TestLMSAccountRetirementPost(RetirementTestCase, ModuleStoreTestCase):
"""
Tests the LMS account retirement (GDPR P2) endpoint.
"""
def setUp(self):
super(TestLMSAccountRetirementPost, self).setUp()
self.pii_standin = 'PII here'
self.course = CourseFactory()
self.test_user = UserFactory()
self.test_superuser = SuperuserFactory()
self.original_username = self.test_user.username
self.original_email = self.test_user.email
self.retired_username = get_retired_username_by_username(self.original_username)
self.retired_email = get_retired_email_by_email(self.original_email)
retirement_state = RetirementState.objects.get(state_name='RETIRING_LMS')
self.retirement_status = UserRetirementStatus.create_retirement(self.test_user)
self.retirement_status.current_state = retirement_state
self.retirement_status.last_state = retirement_state
self.retirement_status.save()
# wiki data setup
rp = RevisionPlugin.objects.create(article_id=0)
RevisionPluginRevision.objects.create(
revision_number=1,
ip_address="ipaddresss",
plugin=rp,
user=self.test_user,
)
article = Article.objects.create()
ArticleRevision.objects.create(ip_address="ipaddresss", user=self.test_user, article=article)
# ManualEnrollmentAudit setup
course_enrollment = CourseEnrollment.enroll(user=self.test_user, course_key=self.course.id)
ManualEnrollmentAudit.objects.create(
enrollment=course_enrollment, reason=self.pii_standin, enrolled_email=self.pii_standin
)
# CreditRequest and CreditRequirementStatus setup
provider = CreditProvider.objects.create(provider_id="Hogwarts")
credit_course = CreditCourse.objects.create(course_key=self.course.id)
CreditRequest.objects.create(
username=self.test_user.username,
course=credit_course,
provider_id=provider.id,
parameters={self.pii_standin},
)
req = CreditRequirement.objects.create(course_id=credit_course.id)
CreditRequirementStatus.objects.create(username=self.test_user.username, requirement=req)
# ApiAccessRequest setup
site = Site.objects.create()
ApiAccessRequest.objects.create(
user=self.test_user,
site=site,
website=self.pii_standin,
company_address=self.pii_standin,
company_name=self.pii_standin,
reason=self.pii_standin,
)
# SurveyAnswer setup
SurveyAnswer.objects.create(user=self.test_user, field_value=self.pii_standin, form_id=0)
# other setup
PendingNameChange.objects.create(user=self.test_user, new_name=self.pii_standin, rationale=self.pii_standin)
PasswordHistory.objects.create(user=self.test_user, password=self.pii_standin)
# setup for doing POST from test client
self.headers = self.build_jwt_headers(self.test_superuser)
self.headers['content_type'] = "application/json"
self.url = reverse('accounts_retire_misc')
def post_and_assert_status(self, data, expected_status=status.HTTP_204_NO_CONTENT):
"""
Helper function for making a request to the retire subscriptions endpoint, and asserting the status.
"""
response = self.client.post(self.url, json.dumps(data), **self.headers)
self.assertEqual(response.status_code, expected_status)
return response
def test_retire_user(self):
# check that rows that will not exist after retirement exist now
self.assertTrue(CreditRequest.objects.filter(username=self.test_user.username).exists())
self.assertTrue(CreditRequirementStatus.objects.filter(username=self.test_user.username).exists())
self.assertTrue(PendingNameChange.objects.filter(user=self.test_user).exists())
retirement = UserRetirementStatus.get_retirement_for_retirement_action(self.test_user.username)
data = {'username': self.original_username}
self.post_and_assert_status(data)
self.test_user.refresh_from_db()
self.test_user.profile.refresh_from_db() # pylint: disable=no-member
self.assertEqual(RevisionPluginRevision.objects.get(user=self.test_user).ip_address, None)
self.assertEqual(ArticleRevision.objects.get(user=self.test_user).ip_address, None)
self.assertFalse(PendingNameChange.objects.filter(user=self.test_user).exists())
self.assertEqual(PasswordHistory.objects.get(user=self.test_user).password, '')
self.assertEqual(
ManualEnrollmentAudit.objects.get(
enrollment=CourseEnrollment.objects.get(user=self.test_user)
).enrolled_email,
retirement.retired_email
)
self.assertFalse(CreditRequest.objects.filter(username=self.test_user.username).exists())
self.assertTrue(CreditRequest.objects.filter(username=retirement.retired_username).exists())
self.assertEqual(CreditRequest.objects.get(username=retirement.retired_username).parameters, {})
self.assertFalse(CreditRequirementStatus.objects.filter(username=self.test_user.username).exists())
self.assertTrue(CreditRequirementStatus.objects.filter(username=retirement.retired_username).exists())
self.assertEqual(CreditRequirementStatus.objects.get(username=retirement.retired_username).reason, {})
retired_api_access_request = ApiAccessRequest.objects.get(user=self.test_user)
self.assertEqual(retired_api_access_request.website, '')
self.assertEqual(retired_api_access_request.company_address, '')
self.assertEqual(retired_api_access_request.company_name, '')
self.assertEqual(retired_api_access_request.reason, '')
self.assertEqual(SurveyAnswer.objects.get(user=self.test_user).field_value, '')
......@@ -16,6 +16,7 @@ from django.db import transaction
from django.utils.translation import ugettext as _
from edx_rest_framework_extensions.authentication import JwtAuthentication
from enterprise.models import EnterpriseCourseEnrollment, EnterpriseCustomerUser, PendingEnterpriseCustomerUser
from integrated_channels.degreed.models import DegreedLearnerDataTransmissionAudit
from integrated_channels.sap_success_factors.models import SapSuccessFactorsLearnerDataTransmissionAudit
from rest_framework import permissions, status
from rest_framework.authentication import SessionAuthentication
......@@ -25,20 +26,29 @@ from rest_framework.views import APIView
from rest_framework.viewsets import ViewSet
from six import text_type
from social_django.models import UserSocialAuth
from wiki.models import ArticleRevision
from wiki.models.pluginbase import RevisionPluginRevision
from entitlements.models import CourseEntitlement
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
from openedx.core.djangoapps.credit.models import CreditRequirementStatus, CreditRequest
from openedx.core.djangoapps.course_groups.models import UnregisteredLearnerCohortAssignments
from openedx.core.djangoapps.profile_images.images import remove_profile_images
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_names, set_has_profile_image
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
from openedx.core.djangolib.oauth2_retirement_utils import retire_dop_oauth2_models, retire_dot_oauth2_models
from openedx.core.djangolib.oauth2_retirement_utils import retire_dot_oauth2_models, retire_dop_oauth2_models
from openedx.core.lib.api.authentication import (
OAuth2AuthenticationAllowInactiveUser,
SessionAuthenticationAllowInactiveUser
)
from openedx.core.lib.api.parsers import MergePatchParser
from survey.models import SurveyAnswer
from student.models import (
CourseEnrollment,
ManualEnrollmentAudit,
PasswordHistory,
PendingNameChange,
CourseEnrollmentAllowed,
PendingEmailChange,
Registration,
......@@ -581,6 +591,54 @@ class AccountRetirementStatusView(ViewSet):
return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class LMSAccountRetirementView(ViewSet):
"""
Provides an API endpoint for retiring a user in the LMS.
"""
authentication_classes = (JwtAuthentication,)
permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
parser_classes = (JSONParser,)
@request_requires_username
def post(self, request):
"""
POST /api/user/v1/accounts/retire_misc/
{
'username': 'user_to_retire'
}
Retires the user with the given username in the LMS.
"""
username = request.data['username']
if is_username_retired(username):
return Response(status=status.HTTP_404_NOT_FOUND)
try:
retirement = UserRetirementStatus.get_retirement_for_retirement_action(username)
RevisionPluginRevision.retire_user(retirement.user)
ArticleRevision.retire_user(retirement.user)
PendingNameChange.delete_by_user_value(retirement.user, field='user')
PasswordHistory.retire_user(retirement.user.id)
course_enrollments = CourseEnrollment.objects.filter(user=retirement.user)
ManualEnrollmentAudit.retire_manual_enrollments(course_enrollments, retirement.retired_email)
CreditRequest.retire_user(retirement.original_username, retirement.retired_username)
ApiAccessRequest.retire_user(retirement.user)
CreditRequirementStatus.retire_user(retirement.user.username)
SurveyAnswer.retire_user(retirement.user.id)
except UserRetirementStatus.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
except RetirementStateError as exc:
return Response(text_type(exc), status=status.HTTP_400_BAD_REQUEST)
except Exception as exc: # pylint: disable=broad-except
return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(status=status.HTTP_204_NO_CONTENT)
class AccountRetirementView(ViewSet):
"""
Provides API endpoint for retiring a user.
......@@ -621,6 +679,7 @@ class AccountRetirementView(ViewSet):
# Retire data from Enterprise models
self.retire_users_data_sharing_consent(username, retired_username)
self.retire_sapsf_data_transmission(user)
self.retire_degreed_data_transmission(user)
self.retire_user_from_pending_enterprise_customer_user(user, retired_email)
self.retire_entitlement_support_detail(user)
......@@ -688,6 +747,17 @@ class AccountRetirementView(ViewSet):
)
audits.update(sapsf_user_id='')
@staticmethod
def retire_degreed_data_transmission(user):
for ent_user in EnterpriseCustomerUser.objects.filter(user_id=user.id):
for enrollment in EnterpriseCourseEnrollment.objects.filter(
enterprise_customer_user=ent_user
):
audits = DegreedLearnerDataTransmissionAudit.objects.filter(
enterprise_course_enrollment_id=enrollment.id
)
audits.update(degreed_user_email='')
@staticmethod
def retire_user_from_pending_enterprise_customer_user(user, retired_email):
PendingEnterpriseCustomerUser.objects.filter(user_email=user.email).update(user_email=retired_email)
......
......@@ -12,7 +12,8 @@ from .accounts.views import (
AccountRetirementStatusView,
AccountRetirementView,
AccountViewSet,
DeactivateLogoutView
DeactivateLogoutView,
LMSAccountRetirementView
)
from .preferences.views import PreferencesDetailView, PreferencesView
from .verification_api.views import IDVerificationStatusView
......@@ -47,6 +48,9 @@ RETIREMENT_POST = AccountRetirementView.as_view({
'post': 'post',
})
RETIREMENT_LMS_POST = LMSAccountRetirementView.as_view({
'post': 'post',
})
urlpatterns = [
url(
......@@ -104,6 +108,11 @@ urlpatterns = [
RETIREMENT_POST,
name='accounts_retire'
),
url(
r'^v1/accounts/retire_misc/$',
RETIREMENT_LMS_POST,
name='accounts_retire_misc'
),
url(
r'^v1/accounts/update_retirement_status/$',
RETIREMENT_UPDATE,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment