EDUCATOR-2802: P2 gdpr endpoint

This commit is contained in:
Sofiya Semenova
2018-05-02 14:00:31 -04:00
committed by Sanford Student
parent 5dc13d067d
commit cd18206167
9 changed files with 230 additions and 9 deletions

View File

@@ -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',

View File

@@ -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

View File

@@ -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, '')

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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