Skip to content

Commit

Permalink
Merge pull request #8909 from Johnetordoff/admin-expose-delete-user
Browse files Browse the repository at this point in the history
[PLAT-1311] Expose GDPR user deletion for admin app
  • Loading branch information
sloria committed Jan 18, 2019
2 parents 1a06767 + e065766 commit 865e1a4
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 1 deletion.
17 changes: 17 additions & 0 deletions admin/templates/users/GDPR_delete_user.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<form class="well" method="post" action="{% url 'users:GDPR_delete' guid=guid %}">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Are you sure you want to <b>GDPR</b> delete this user? {{ guid }}</h3>
</div>
<div class="modal-body">
<h2><b>This is not reversible!</b></h2>
{% csrf_token %}
</div>
<div class="modal-footer">
<input class="btn btn-danger" type="submit" value="Confirm" />
<button type="button" class="btn btn-default"
data-dismiss="modal">
Cancel
</button>
</div>
</form>
19 changes: 18 additions & 1 deletion admin/templates/users/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
{% endblock title %}
{% block content %}
<div class="container-fluid">
<div class="row">
{% if messages %}
<ul>
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="row">
<div class="col-md-12">
<div class="btn-group" role="group">
Expand Down Expand Up @@ -39,6 +48,9 @@
<div class="row">
<div class="col-md-12">
<div class="btn-group padded" role="group">
{% if not user.deleted %}
<a href="{% url 'users:GDPR_delete' user.id %}" data-toggle="modal" data-target="#deleteModal" class="btn btn-danger">GDPR Delete Account</a>
{% endif %}
{% if not user.disabled %}
{% if user.requested_deactivation %}
<a href="{% url 'users:disable' user.id %}" data-toggle="modal" data-target="#disableModal" class="btn btn-danger">Requested account deactivation</a>
Expand All @@ -47,7 +59,7 @@
{% endif %}
<a href="{% url 'users:disable' user.id %}" data-toggle="modal" data-target="#disableModal" class="btn btn-danger">Force disable account</a>
{% elif 'deac_confirmed' not in user.system_tags %}
<form method="post" action="{% url 'users:reactivate' user.id %}">
<form method="post" action="{% url 'users:reactivate' user.id %}" style="display: inherit;">
{% csrf_token %}
<input class="btn btn-success" type="submit" value="Reactivate account"/>
</form>
Expand Down Expand Up @@ -111,6 +123,11 @@
<div class="modal-content"></div>
</div>
</div>
<div class="modal" id="deleteModal">
<div class="modal-dialog">
<div class="modal-content"></div>
</div>
</div>
<div class="modal" id="disableSpamModal">
<div class="modal-dialog">
<div class="modal-content"></div>
Expand Down
1 change: 1 addition & 0 deletions admin/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def serialize_user(user):
'last_login': user.date_last_login,
'confirmed': user.date_confirmed,
'registered': user.date_registered,
'deleted': user.deleted,
'disabled': user.date_disabled if user.is_disabled else False,
'two_factor': user.has_addon('twofactor'),
'osf_link': user.absolute_url,
Expand Down
1 change: 1 addition & 0 deletions admin/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
url(r'^(?P<guid>[a-z0-9]+)/$', views.UserView.as_view(), name='user'),
url(r'^search/(?P<name>.*)/$', views.UserSearchList.as_view(), name='search_list'),
url(r'^(?P<guid>[a-z0-9]+)/reset-password/$', views.ResetPasswordView.as_view(), name='reset_password'),
url(r'^(?P<guid>[a-z0-9]+)/gdpr_delete/$', views.UserGDPRDeleteView.as_view(), name='GDPR_delete'),
url(r'^(?P<guid>[a-z0-9]+)/disable/$', views.UserDeleteView.as_view(), name='disable'),
url(r'^(?P<guid>[a-z0-9]+)/disable_spam/$', views.SpamUserDeleteView.as_view(), name='spam_disable'),
url(r'^(?P<guid>[a-z0-9]+)/enable_ham/$', views.HamUserRestoreView.as_view(), name='ham_enable'),
Expand Down
50 changes: 50 additions & 0 deletions admin/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
from django.db.models import Q
from django.views.defaults import page_not_found
from django.views.generic import FormView, DeleteView, ListView, TemplateView
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.http import Http404, HttpResponse
from django.shortcuts import redirect

from osf.exceptions import UserStateError
from osf.models.base import Guid
from osf.models.user import OSFUser
from osf.models.node import Node, NodeLog
Expand All @@ -32,6 +34,7 @@
USER_EMAILED,
USER_REMOVED,
USER_RESTORED,
USER_GDPR_DELETED,
CONFIRM_SPAM,
REINDEX_ELASTIC,
)
Expand Down Expand Up @@ -103,6 +106,53 @@ def get_object(self, queryset=None):
return OSFUser.load(self.kwargs.get('guid'))


class UserGDPRDeleteView(PermissionRequiredMixin, DeleteView):
""" Allow authorised admin user to totally erase user data.
Interface with OSF database. No admin models.
"""
template_name = 'users/GDPR_delete_user.html'
context_object_name = 'user'
object = None
permission_required = 'osf.change_osfuser'
raise_exception = True

def delete(self, request, *args, **kwargs):
try:
user = self.get_object()
user.gdpr_delete()
user.save()
message = 'User {} was successfully GDPR deleted'.format(user._id)
messages.success(request, message)
update_admin_log(
user_id=self.request.user.id,
object_id=user.pk,
object_repr='User',
message=message,
action_flag=USER_GDPR_DELETED
)
except UserStateError as e:
messages.warning(request, str(e))

return redirect(reverse_user(self.kwargs.get('guid')))

def get_context_data(self, **kwargs):
context = {}
context.setdefault('guid', kwargs.get('object')._id)
return super(UserGDPRDeleteView, self).get_context_data(**context)

def get_object(self, queryset=None):
user = OSFUser.load(self.kwargs.get('guid'))
if user:
return user
else:
raise Http404(
'{} with id "{}" not found.'.format(
self.context_object_name.title(),
self.kwargs.get('guid')
))


class SpamUserDeleteView(UserDeleteView):
"""
Allow authorized admin user to delete a spam user and mark all their nodes as private
Expand Down
54 changes: 54 additions & 0 deletions admin_tests/users/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied
from django.contrib.auth.models import Permission
from django.contrib.messages.storage.fallback import FallbackStorage

from tests.base import AdminTestCase
from website import settings
Expand Down Expand Up @@ -134,6 +135,59 @@ def test_correct_view_permissions(self):
self.assertEqual(response.status_code, 200)


class TestDeleteUser(AdminTestCase):
def setUp(self):
self.user = UserFactory()
self.request = RequestFactory().post('/fake_path')
self.view = views.UserGDPRDeleteView
self.view = setup_log_view(self.view, self.request, guid=self.user._id)

def test_get_object(self):
obj = self.view().get_object()
nt.assert_is_instance(obj, OSFUser)

def test_gdpr_delete_user(self):
# django.contrib.messages has a bug which effects unittests
# more info here -> https://code.djangoproject.com/ticket/17971
setattr(self.request, 'session', 'session')
messages = FallbackStorage(self.request)
setattr(self.request, '_messages', messages)

count = AdminLogEntry.objects.count()
self.view().delete(self.request)
self.user.reload()
nt.assert_true(self.user.deleted)
nt.assert_equal(AdminLogEntry.objects.count(), count + 1)

def test_no_user(self):
view = setup_view(views.UserGDPRDeleteView(), self.request, guid='meh')
with nt.assert_raises(Http404):
view.delete(self.request)

def test_no_user_permissions_raises_error(self):
user = UserFactory()
guid = user._id
request = RequestFactory().get(reverse('users:GDPR_delete', kwargs={'guid': guid}))
request.user = user

with self.assertRaises(PermissionDenied):
self.view.as_view()(request, guid=guid)

def test_correct_view_permissions(self):
user = UserFactory()
guid = user._id

change_permission = Permission.objects.get(codename='change_osfuser')
user.user_permissions.add(change_permission)
user.save()

request = RequestFactory().get(reverse('users:GDPR_delete', kwargs={'guid': guid}))
request.user = user

response = self.view.as_view()(request, guid=guid)
self.assertEqual(response.status_code, 200)


class TestDisableUser(AdminTestCase):
def setUp(self):
self.user = UserFactory()
Expand Down
1 change: 1 addition & 0 deletions osf/models/admin_log_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
USER_RESTORED = 41
USER_2_FACTOR = 42
USER_EMAILED = 43
USER_GDPR_DELETED = 44

REINDEX_SHARE = 50
REINDEX_ELASTIC = 51
Expand Down

0 comments on commit 865e1a4

Please sign in to comment.