EDUCATOR-2802: P2 gdpr endpoint
This commit is contained in:
committed by
Sanford Student
parent
5dc13d067d
commit
cd18206167
@@ -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,
|
||||
|
||||
@@ -16,7 +16,7 @@ git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f3
|
||||
git+https://github.com/edx/django-oauth-plus.git@01ec2a161dfc3465f9d35b9211ae790177418316#egg=django-oauth-plus==2.2.9.edx-1
|
||||
git+https://github.com/edx/django-openid-auth.git@0.15.1#egg=django-openid-auth==0.15.1
|
||||
git+https://github.com/jazzband/django-pipeline.git@d068a019169c9de5ee20ece041a6dea236422852#egg=django-pipeline==1.5.3
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.17#egg=django-wiki
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.18#egg=django-wiki
|
||||
git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1
|
||||
git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3
|
||||
-e common/lib/dogstats
|
||||
|
||||
@@ -18,7 +18,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c
|
||||
git+https://github.com/edx/django-oauth-plus.git@01ec2a161dfc3465f9d35b9211ae790177418316#egg=django-oauth-plus==2.2.9.edx-1
|
||||
git+https://github.com/edx/django-openid-auth.git@0.15.1#egg=django-openid-auth==0.15.1
|
||||
git+https://github.com/jazzband/django-pipeline.git@d068a019169c9de5ee20ece041a6dea236422852#egg=django-pipeline==1.5.3
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.17#egg=django-wiki
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.18#egg=django-wiki
|
||||
git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1
|
||||
git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3
|
||||
-e common/lib/dogstats
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
# Third-party:
|
||||
-e git+https://github.com/jazzband/django-pipeline.git@d068a019169c9de5ee20ece041a6dea236422852#egg=django-pipeline==1.5.3
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.17#egg=django-wiki
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.18#egg=django-wiki
|
||||
-e git+https://github.com/edx/django-openid-auth.git@0.15.1#egg=django-openid-auth==0.15.1
|
||||
-e git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=MongoDBProxy==0.1.0
|
||||
-e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
|
||||
|
||||
@@ -16,7 +16,7 @@ git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f3
|
||||
git+https://github.com/edx/django-oauth-plus.git@01ec2a161dfc3465f9d35b9211ae790177418316#egg=django-oauth-plus==2.2.9.edx-1
|
||||
git+https://github.com/edx/django-openid-auth.git@0.15.1#egg=django-openid-auth==0.15.1
|
||||
git+https://github.com/jazzband/django-pipeline.git@d068a019169c9de5ee20ece041a6dea236422852#egg=django-pipeline==1.5.3
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.17#egg=django-wiki
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.18#egg=django-wiki
|
||||
git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1
|
||||
git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3
|
||||
-e common/lib/dogstats
|
||||
|
||||
Reference in New Issue
Block a user