Repository: deis/deis

Issue:
Implement account cancellation
There is no way to cancel an account currently.  We need to implement this over the API.



Discussion:

Resolving patch:
diff --git a/api/urls.py b/api/urls.py
--- a/api/urls.py
+++ b/api/urls.py
@@ -289,6 +289,10 @@
 
   Create a new :class:`~django.contrib.auth.models.User`.
 
+.. http:delete:: /api/auth/register/
+
+  Destroy the logged-in :class:`~django.contrib.auth.models.User`.
+
 .. http:post:: /api/auth/login
 
   Authenticate for the REST framework.
@@ -407,6 +411,8 @@
     # authn / authz
     url(r'^auth/register/?',
         views.UserRegistrationView.as_view({'post': 'create'})),
+    url(r'^auth/cancel/?',
+        views.UserCancellationView.as_view({'delete': 'destroy'})),
     url(r'^auth/',
         include('rest_framework.urls', namespace='rest_framework')),
     url(r'^generate-api-key/',
diff --git a/api/views.py b/api/views.py
--- a/api/views.py
+++ b/api/views.py
@@ -84,6 +84,18 @@ def pre_save(self, obj):
         obj.set_password(obj.password)
 
 
+class UserCancellationView(viewsets.GenericViewSet,
+                           viewsets.mixins.DestroyModelMixin):
+    model = User
+
+    permission_classes = (permissions.IsAuthenticated,)
+
+    def destroy(self, request, *args, **kwargs):
+        obj = self.request.user
+        obj.delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+
 class OwnerViewSet(viewsets.ModelViewSet):
     """Scope views to an `owner` attribute."""
 
diff --git a/client/deis.py b/client/deis.py
--- a/client/deis.py
+++ b/client/deis.py
@@ -604,6 +604,30 @@ def auth_register(self, args):
             print('Registration failed', response.content)
             return False
 
+    def auth_cancel(self, args):
+        """
+        Cancel and remove the current account.
+
+        Usage: deis auth:cancel
+        """
+        controller = self._settings.get('controller')
+        if not controller:
+            print('Not logged in to a Deis controller')
+            return False
+        print('Please log in again in order to cancel this account')
+        username = self.auth_login({'<controller>': controller})
+        if username:
+            confirm = raw_input("Cancel account \"{}\" at {}? (y/n) ".format(username, controller))
+            if confirm == 'y':
+                self._dispatch('delete', '/api/auth/cancel')
+                self._session.cookies.clear()
+                self._session.cookies.save()
+                self._settings['controller'] = None
+                self._settings.save()
+                print('Account cancelled')
+            else:
+                print('Accont not changed')
+
     def auth_login(self, args):
         """
         Login by authenticating against a controller
@@ -630,7 +654,7 @@ def auth_login(self, args):
             self._settings['controller'] = controller
             self._settings.save()
             print("Logged in as {}".format(username))
-            return True
+            return username
         else:
             print('Login failed')
             self._session.cookies.clear()


Test patch:
diff --git a/api/tests/auth.py b/api/tests/auth.py
--- a/api/tests/auth.py
+++ b/api/tests/auth.py
@@ -78,3 +78,36 @@ def test_auth(self):
         url = '/api/providers'
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
+
+    def test_cancel(self):
+        """Test that a registered user can cancel her account."""
+        # test registration workflow
+        username, password = 'newuser', 'password'
+        first_name, last_name = 'Otto', 'Test'
+        email = 'autotest@deis.io'
+        submit = {
+            'username': username,
+            'password': password,
+            'first_name': first_name,
+            'last_name': last_name,
+            'email': email,
+            # try to abuse superuser/staff level perms
+            'is_superuser': True,
+            'is_staff': True,
+        }
+        url = '/api/auth/register'
+        response = self.client.post(url, json.dumps(submit), content_type='application/json')
+        self.assertEqual(response.status_code, 201)
+        self.assertTrue(
+            self.client.login(username=username, password=password))
+        # test for default objects
+        url = '/api/providers'
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.data['count'], len(settings.PROVIDER_MODULES))
+        # cancel the account
+        url = '/api/auth/cancel'
+        response = self.client.delete(url)
+        self.assertEqual(response.status_code, 204)
+        self.assertFalse(
+            self.client.login(username=username, password=password))