Merge lp:~rbanffy/maas/1510210-cant-delete-user into lp:~maas-committers/maas/trunk

Proposed by Ricardo Bánffy
Status: Merged
Approved by: Ricardo Bánffy
Approved revision: no longer in the source branch.
Merged at revision: 4424
Proposed branch: lp:~rbanffy/maas/1510210-cant-delete-user
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 269 lines (+131/-6)
7 files modified
src/maasserver/api/tests/test_user.py (+92/-0)
src/maasserver/api/users.py (+26/-1)
src/maasserver/models/filestorage.py (+3/-1)
src/maasserver/models/node.py (+3/-1)
src/maasserver/models/sshkey.py (+2/-1)
src/maasserver/models/sslkey.py (+2/-1)
src/maasserver/models/staticipaddress.py (+3/-1)
To merge this branch: bzr merge lp:~rbanffy/maas/1510210-cant-delete-user
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+275789@code.launchpad.net

Commit message

Allows administrators to delete a user that has no attached nodes, IP address reservation or SSH/SSL keys. Prevent self-deletion.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks generally good, but I think your to strict on the checking. We should not prevent the user from being deleted if they have ssh keys or ssl keys. Those are directly related to the user, we can just delete those when the user is deleted. Actually you can set those to CASCADE and they will be deleted for you, I believe.

review: Needs Fixing
Revision history for this message
Ricardo Bánffy (rbanffy) wrote :

> Looks generally good, but I think your to strict on the checking. We should
> not prevent the user from being deleted if they have ssh keys or ssl keys.
> Those are directly related to the user, we can just delete those when the user
> is deleted. Actually you can set those to CASCADE and they will be deleted for
> you, I believe.

Yes. That's an option. Just need to remove the extra checks and change the tests to reflect the new behavior.

Revision history for this message
Ricardo Bánffy (rbanffy) wrote :

Done

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good. But you need to update two test names.

review: Approve
Revision history for this message
Ricardo Bánffy (rbanffy) wrote :

> Looks good. But you need to update two test names.

/me slaps forehead

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (61.4 KiB)

The attempt to merge lp:~rbanffy/maas/1510210-cant-delete-user into lp:maas failed. Below is the output from the failed tests.

Get:1 http://security.ubuntu.com trusty-security InRelease [64.4 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:2 http://nova.clouds.archive.ubuntu.com trusty-updates InRelease [64.4 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:3 http://security.ubuntu.com trusty-security/main Sources [97.7 kB]
Get:4 http://security.ubuntu.com trusty-security/universe Sources [31.0 kB]
Get:5 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [241 kB]
Get:6 http://security.ubuntu.com trusty-security/main amd64 Packages [356 kB]
Get:7 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [140 kB]
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [117 kB]
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [636 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [324 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 2,071 kB in 4s (516 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm pep8 phantomjs postgresql pyflakes python-apt python-bson python-bzrlib python-convoy python-coverage python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lxml python-mock python-netaddr python-netifaces python-nose python-oauth python-openssl python-paramiko python-pexpect python-pip python-pocket-lint python-psycopg2 python-pyinotify python-pyparsing python-seamicroclient python-simplejson python-simplestreams python-sphinx python-subunit python-tempita python-testresources python-testscenarios ...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api/tests/test_user.py'
--- src/maasserver/api/tests/test_user.py 2014-09-10 16:20:31 +0000
+++ src/maasserver/api/tests/test_user.py 2015-10-28 10:52:32 +0000
@@ -19,6 +19,14 @@
1919
20from django.contrib.auth.models import User20from django.contrib.auth.models import User
21from django.core.urlresolvers import reverse21from django.core.urlresolvers import reverse
22from maasserver.enum import (
23 IPADDRESS_TYPE,
24 NODE_STATUS,
25)
26from maasserver.models import (
27 SSHKey,
28 SSLKey,
29)
22from maasserver.testing.api import APITestCase30from maasserver.testing.api import APITestCase
23from maasserver.testing.factory import factory31from maasserver.testing.factory import factory
24from testtools.matchers import ContainsAll32from testtools.matchers import ContainsAll
@@ -164,3 +172,87 @@
164 self.assertEqual(172 self.assertEqual(
165 httplib.NOT_FOUND, response.status_code, response.status_code)173 httplib.NOT_FOUND, response.status_code, response.status_code)
166 self.assertItemsEqual([], User.objects.filter(username=nonuser))174 self.assertItemsEqual([], User.objects.filter(username=nonuser))
175
176 def test_DELETE_requires_admin_privileges(self):
177 user = factory.make_User()
178 response = self.client.delete(
179 reverse('user_handler', args=[user.username]))
180 self.assertEqual(
181 httplib.FORBIDDEN, response.status_code, response.status_code)
182 self.assertTrue(User.objects.filter(username=user.username).exists())
183
184 def test_DELETE_requires_admin_privileges_with_invalid_user(self):
185 """If the user has no admin privileges, it doesn't matter if the user
186 being deleted exists or not, we will return a 403."""
187 nonuser = factory.make_name('nonuser')
188 response = self.client.delete(reverse('user_handler', args=[nonuser]))
189 self.assertEqual(
190 httplib.FORBIDDEN, response.status_code, response.status_code)
191
192 def test_DELETE_keeps_quiet_if_user_not_found(self):
193 self.become_admin()
194 nonuser = factory.make_name('nonuser')
195 response = self.client.delete(reverse('user_handler', args=[nonuser]))
196 self.assertEqual(
197 httplib.NO_CONTENT, response.status_code, response.status_code)
198
199 def test_DELETE_admin_cannot_delete_self(self):
200 self.become_admin()
201 user = self.logged_in_user
202 response = self.client.delete(
203 reverse('user_handler', args=[user.username]))
204 self.assertEqual(
205 httplib.BAD_REQUEST, response.status_code, response.status_code)
206 self.assertTrue(User.objects.filter(username=user.username).exists())
207 self.assertIn('cannot self-delete', response.content)
208
209 def test_DELETE_deletes_user(self):
210 self.become_admin()
211 user = factory.make_User()
212 response = self.client.delete(
213 reverse('user_handler', args=[user.username]))
214 self.assertEqual(
215 httplib.NO_CONTENT, response.status_code, response.status_code)
216 self.assertItemsEqual([], User.objects.filter(username=user.username))
217
218 def test_DELETE_user_with_node_fails(self):
219 self.become_admin()
220 user = factory.make_User()
221 factory.make_Node(owner=user, status=NODE_STATUS.DEPLOYED)
222 response = self.client.delete(
223 reverse('user_handler', args=[user.username]))
224 self.assertEqual(
225 httplib.BAD_REQUEST, response.status_code, response.status_code)
226 self.assertIn('assigned nodes cannot be deleted', response.content)
227
228 def test_DELETE_user_with_staticaddress_fails(self):
229 self.become_admin()
230 user = factory.make_User()
231 factory.make_StaticIPAddress(
232 user=user, alloc_type=IPADDRESS_TYPE.USER_RESERVED)
233 response = self.client.delete(
234 reverse('user_handler', args=[user.username]))
235 self.assertEqual(
236 httplib.BAD_REQUEST, response.status_code, response.status_code)
237 self.assertIn(
238 'with reserved IP addresses cannot be deleted', response.content)
239
240 def test_DELETE_user_with_sslkey_deletes_key(self):
241 self.become_admin()
242 user = factory.make_User()
243 key_id = factory.make_SSLKey(user=user).id
244 response = self.client.delete(
245 reverse('user_handler', args=[user.username]))
246 self.assertEqual(
247 httplib.NO_CONTENT, response.status_code, response.status_code)
248 self.assertFalse(SSLKey.objects.filter(id=key_id).exists())
249
250 def test_DELETE_user_with_sshkey_deletes_key(self):
251 self.become_admin()
252 user = factory.make_User()
253 key_id = factory.make_SSHKey(user=user).id
254 response = self.client.delete(
255 reverse('user_handler', args=[user.username]))
256 self.assertEqual(
257 httplib.NO_CONTENT, response.status_code, response.status_code)
258 self.assertFalse(SSHKey.objects.filter(id=key_id).exists())
167259
=== modified file 'src/maasserver/api/users.py'
--- src/maasserver/api/users.py 2015-05-07 18:14:38 +0000
+++ src/maasserver/api/users.py 2015-10-28 10:52:32 +0000
@@ -18,6 +18,7 @@
18 ]18 ]
1919
2020
21from django.core.exceptions import ValidationError
21from django.shortcuts import get_object_or_40422from django.shortcuts import get_object_or_404
22from maasserver.api.support import (23from maasserver.api.support import (
23 admin_method,24 admin_method,
@@ -28,6 +29,9 @@
28 get_mandatory_param,29 get_mandatory_param,
29)30)
30from maasserver.models import User31from maasserver.models import User
32from maasserver.utils.orm import get_one
33from piston.models import Consumer
34from piston.utils import rc
3135
3236
33class UsersHandler(OperationsHandler):37class UsersHandler(OperationsHandler):
@@ -79,7 +83,7 @@
79class UserHandler(OperationsHandler):83class UserHandler(OperationsHandler):
80 """Manage a user account."""84 """Manage a user account."""
81 api_doc_section_name = "User"85 api_doc_section_name = "User"
82 create = update = delete = None86 create = update = None
8387
84 model = User88 model = User
85 fields = (89 fields = (
@@ -90,3 +94,24 @@
9094
91 def read(self, request, username):95 def read(self, request, username):
92 return get_object_or_404(User, username=username)96 return get_object_or_404(User, username=username)
97
98 @admin_method
99 def delete(self, request, username):
100 """Deletes a user"""
101 if request.user.username == username:
102 raise ValidationError("An administrator cannot self-delete.")
103
104 user = get_one(User.objects.filter(username=username))
105
106 if user is not None:
107 if user.node_set.exists():
108 raise ValidationError(
109 "A user with assigned nodes cannot be deleted.")
110 elif user.staticipaddress_set.exists():
111 raise ValidationError(
112 "A user with reserved IP addresses cannot be deleted.")
113 else:
114 Consumer.objects.filter(user=user).delete()
115 user.delete()
116
117 return rc.DELETED
93118
=== modified file 'src/maasserver/models/filestorage.py'
--- src/maasserver/models/filestorage.py 2015-05-07 18:14:38 +0000
+++ src/maasserver/models/filestorage.py 2015-10-28 10:52:32 +0000
@@ -26,6 +26,7 @@
26 ForeignKey,26 ForeignKey,
27 Manager,27 Manager,
28 Model,28 Model,
29 PROTECT,
29)30)
30from django.utils.http import urlencode31from django.utils.http import urlencode
31from maasserver import DefaultMeta32from maasserver import DefaultMeta
@@ -89,7 +90,8 @@
89 # owner can be None: this is to support upgrading existing90 # owner can be None: this is to support upgrading existing
90 # installations where the files were not linked to users yet.91 # installations where the files were not linked to users yet.
91 owner = ForeignKey(92 owner = ForeignKey(
92 User, default=None, blank=True, null=True, editable=False)93 User, default=None, blank=True, null=True, editable=False,
94 on_delete=PROTECT)
93 key = CharField(95 key = CharField(
94 max_length=36, unique=True, default=generate_filestorage_key,96 max_length=36, unique=True, default=generate_filestorage_key,
95 editable=False)97 editable=False)
9698
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2015-10-28 02:02:45 +0000
+++ src/maasserver/models/node.py 2015-10-28 10:52:32 +0000
@@ -44,6 +44,7 @@
44 IntegerField,44 IntegerField,
45 Manager,45 Manager,
46 ManyToManyField,46 ManyToManyField,
47 PROTECT,
47 Q,48 Q,
48 SET_DEFAULT,49 SET_DEFAULT,
49 SET_NULL,50 SET_NULL,
@@ -447,7 +448,8 @@
447 default=NODE_STATUS.DEFAULT)448 default=NODE_STATUS.DEFAULT)
448449
449 owner = ForeignKey(450 owner = ForeignKey(
450 User, default=None, blank=True, null=True, editable=False)451 User, default=None, blank=True, null=True, editable=False,
452 on_delete=PROTECT)
451453
452 bios_boot_method = CharField(max_length=31, blank=True, null=True)454 bios_boot_method = CharField(max_length=31, blank=True, null=True)
453455
454456
=== modified file 'src/maasserver/models/sshkey.py'
--- src/maasserver/models/sshkey.py 2015-05-07 18:14:38 +0000
+++ src/maasserver/models/sshkey.py 2015-10-28 10:52:32 +0000
@@ -22,6 +22,7 @@
22from django.contrib.auth.models import User22from django.contrib.auth.models import User
23from django.core.exceptions import ValidationError23from django.core.exceptions import ValidationError
24from django.db.models import (24from django.db.models import (
25 CASCADE,
25 ForeignKey,26 ForeignKey,
26 Manager,27 Manager,
27 TextField,28 TextField,
@@ -124,7 +125,7 @@
124125
125 objects = SSHKeyManager()126 objects = SSHKeyManager()
126127
127 user = ForeignKey(User, null=False, editable=False)128 user = ForeignKey(User, null=False, editable=False, on_delete=CASCADE)
128129
129 key = TextField(130 key = TextField(
130 null=False, editable=True, validators=[validate_ssh_public_key])131 null=False, editable=True, validators=[validate_ssh_public_key])
131132
=== modified file 'src/maasserver/models/sslkey.py'
--- src/maasserver/models/sslkey.py 2015-09-24 16:22:12 +0000
+++ src/maasserver/models/sslkey.py 2015-10-28 10:52:32 +0000
@@ -23,6 +23,7 @@
23from django.contrib.auth.models import User23from django.contrib.auth.models import User
24from django.core.exceptions import ValidationError24from django.core.exceptions import ValidationError
25from django.db.models import (25from django.db.models import (
26 CASCADE,
26 ForeignKey,27 ForeignKey,
27 Manager,28 Manager,
28 TextField,29 TextField,
@@ -93,7 +94,7 @@
93 """94 """
94 objects = SSLKeyManager()95 objects = SSLKeyManager()
9596
96 user = ForeignKey(User, null=False, editable=False)97 user = ForeignKey(User, null=False, editable=False, on_delete=CASCADE)
97 key = TextField(98 key = TextField(
98 null=False, blank=False, editable=True, validators=[validate_ssl_key])99 null=False, blank=False, editable=True, validators=[validate_ssl_key])
99100
100101
=== modified file 'src/maasserver/models/staticipaddress.py'
--- src/maasserver/models/staticipaddress.py 2015-09-30 18:42:27 +0000
+++ src/maasserver/models/staticipaddress.py 2015-10-28 10:52:32 +0000
@@ -36,6 +36,7 @@
36 ForeignKey,36 ForeignKey,
37 IntegerField,37 IntegerField,
38 Manager,38 Manager,
39 PROTECT,
39)40)
40from maasserver import (41from maasserver import (
41 DefaultMeta,42 DefaultMeta,
@@ -594,7 +595,8 @@
594 validators=[validate_hostname])595 validators=[validate_hostname])
595596
596 user = ForeignKey(597 user = ForeignKey(
597 User, default=None, blank=True, null=True, editable=False)598 User, default=None, blank=True, null=True, editable=False,
599 on_delete=PROTECT)
598600
599 objects = StaticIPAddressManager()601 objects = StaticIPAddressManager()
600602