Merge lp:~rbanffy/maas/1510210-cant-delete-user into lp:~maas-committers/maas/trunk
- 1510210-cant-delete-user
- Merge into trunk
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 |
Related bugs: |
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.
Description of the change
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.
Ricardo Bánffy (rbanffy) wrote : | # |
Done
Blake Rouse (blake-rouse) wrote : | # |
Looks good. But you need to update two test names.
Ricardo Bánffy (rbanffy) wrote : | # |
> Looks good. But you need to update two test names.
/me slaps forehead
MAAS Lander (maas-lander) wrote : | # |
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://
Ign http://
Get:2 http://
Hit http://
Hit http://
Get:3 http://
Get:4 http://
Get:5 http://
Get:6 http://
Get:7 http://
Get:8 http://
Get:9 http://
Hit http://
Hit http://
Get:10 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 2,071 kB in 4s (516 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === modified file 'src/maasserver/api/tests/test_user.py' | |||
2 | --- src/maasserver/api/tests/test_user.py 2014-09-10 16:20:31 +0000 | |||
3 | +++ src/maasserver/api/tests/test_user.py 2015-10-28 10:52:32 +0000 | |||
4 | @@ -19,6 +19,14 @@ | |||
5 | 19 | 19 | ||
6 | 20 | from django.contrib.auth.models import User | 20 | from django.contrib.auth.models import User |
7 | 21 | from django.core.urlresolvers import reverse | 21 | from django.core.urlresolvers import reverse |
8 | 22 | from maasserver.enum import ( | ||
9 | 23 | IPADDRESS_TYPE, | ||
10 | 24 | NODE_STATUS, | ||
11 | 25 | ) | ||
12 | 26 | from maasserver.models import ( | ||
13 | 27 | SSHKey, | ||
14 | 28 | SSLKey, | ||
15 | 29 | ) | ||
16 | 22 | from maasserver.testing.api import APITestCase | 30 | from maasserver.testing.api import APITestCase |
17 | 23 | from maasserver.testing.factory import factory | 31 | from maasserver.testing.factory import factory |
18 | 24 | from testtools.matchers import ContainsAll | 32 | from testtools.matchers import ContainsAll |
19 | @@ -164,3 +172,87 @@ | |||
20 | 164 | self.assertEqual( | 172 | self.assertEqual( |
21 | 165 | httplib.NOT_FOUND, response.status_code, response.status_code) | 173 | httplib.NOT_FOUND, response.status_code, response.status_code) |
22 | 166 | self.assertItemsEqual([], User.objects.filter(username=nonuser)) | 174 | self.assertItemsEqual([], User.objects.filter(username=nonuser)) |
23 | 175 | |||
24 | 176 | def test_DELETE_requires_admin_privileges(self): | ||
25 | 177 | user = factory.make_User() | ||
26 | 178 | response = self.client.delete( | ||
27 | 179 | reverse('user_handler', args=[user.username])) | ||
28 | 180 | self.assertEqual( | ||
29 | 181 | httplib.FORBIDDEN, response.status_code, response.status_code) | ||
30 | 182 | self.assertTrue(User.objects.filter(username=user.username).exists()) | ||
31 | 183 | |||
32 | 184 | def test_DELETE_requires_admin_privileges_with_invalid_user(self): | ||
33 | 185 | """If the user has no admin privileges, it doesn't matter if the user | ||
34 | 186 | being deleted exists or not, we will return a 403.""" | ||
35 | 187 | nonuser = factory.make_name('nonuser') | ||
36 | 188 | response = self.client.delete(reverse('user_handler', args=[nonuser])) | ||
37 | 189 | self.assertEqual( | ||
38 | 190 | httplib.FORBIDDEN, response.status_code, response.status_code) | ||
39 | 191 | |||
40 | 192 | def test_DELETE_keeps_quiet_if_user_not_found(self): | ||
41 | 193 | self.become_admin() | ||
42 | 194 | nonuser = factory.make_name('nonuser') | ||
43 | 195 | response = self.client.delete(reverse('user_handler', args=[nonuser])) | ||
44 | 196 | self.assertEqual( | ||
45 | 197 | httplib.NO_CONTENT, response.status_code, response.status_code) | ||
46 | 198 | |||
47 | 199 | def test_DELETE_admin_cannot_delete_self(self): | ||
48 | 200 | self.become_admin() | ||
49 | 201 | user = self.logged_in_user | ||
50 | 202 | response = self.client.delete( | ||
51 | 203 | reverse('user_handler', args=[user.username])) | ||
52 | 204 | self.assertEqual( | ||
53 | 205 | httplib.BAD_REQUEST, response.status_code, response.status_code) | ||
54 | 206 | self.assertTrue(User.objects.filter(username=user.username).exists()) | ||
55 | 207 | self.assertIn('cannot self-delete', response.content) | ||
56 | 208 | |||
57 | 209 | def test_DELETE_deletes_user(self): | ||
58 | 210 | self.become_admin() | ||
59 | 211 | user = factory.make_User() | ||
60 | 212 | response = self.client.delete( | ||
61 | 213 | reverse('user_handler', args=[user.username])) | ||
62 | 214 | self.assertEqual( | ||
63 | 215 | httplib.NO_CONTENT, response.status_code, response.status_code) | ||
64 | 216 | self.assertItemsEqual([], User.objects.filter(username=user.username)) | ||
65 | 217 | |||
66 | 218 | def test_DELETE_user_with_node_fails(self): | ||
67 | 219 | self.become_admin() | ||
68 | 220 | user = factory.make_User() | ||
69 | 221 | factory.make_Node(owner=user, status=NODE_STATUS.DEPLOYED) | ||
70 | 222 | response = self.client.delete( | ||
71 | 223 | reverse('user_handler', args=[user.username])) | ||
72 | 224 | self.assertEqual( | ||
73 | 225 | httplib.BAD_REQUEST, response.status_code, response.status_code) | ||
74 | 226 | self.assertIn('assigned nodes cannot be deleted', response.content) | ||
75 | 227 | |||
76 | 228 | def test_DELETE_user_with_staticaddress_fails(self): | ||
77 | 229 | self.become_admin() | ||
78 | 230 | user = factory.make_User() | ||
79 | 231 | factory.make_StaticIPAddress( | ||
80 | 232 | user=user, alloc_type=IPADDRESS_TYPE.USER_RESERVED) | ||
81 | 233 | response = self.client.delete( | ||
82 | 234 | reverse('user_handler', args=[user.username])) | ||
83 | 235 | self.assertEqual( | ||
84 | 236 | httplib.BAD_REQUEST, response.status_code, response.status_code) | ||
85 | 237 | self.assertIn( | ||
86 | 238 | 'with reserved IP addresses cannot be deleted', response.content) | ||
87 | 239 | |||
88 | 240 | def test_DELETE_user_with_sslkey_deletes_key(self): | ||
89 | 241 | self.become_admin() | ||
90 | 242 | user = factory.make_User() | ||
91 | 243 | key_id = factory.make_SSLKey(user=user).id | ||
92 | 244 | response = self.client.delete( | ||
93 | 245 | reverse('user_handler', args=[user.username])) | ||
94 | 246 | self.assertEqual( | ||
95 | 247 | httplib.NO_CONTENT, response.status_code, response.status_code) | ||
96 | 248 | self.assertFalse(SSLKey.objects.filter(id=key_id).exists()) | ||
97 | 249 | |||
98 | 250 | def test_DELETE_user_with_sshkey_deletes_key(self): | ||
99 | 251 | self.become_admin() | ||
100 | 252 | user = factory.make_User() | ||
101 | 253 | key_id = factory.make_SSHKey(user=user).id | ||
102 | 254 | response = self.client.delete( | ||
103 | 255 | reverse('user_handler', args=[user.username])) | ||
104 | 256 | self.assertEqual( | ||
105 | 257 | httplib.NO_CONTENT, response.status_code, response.status_code) | ||
106 | 258 | self.assertFalse(SSHKey.objects.filter(id=key_id).exists()) | ||
107 | 167 | 259 | ||
108 | === modified file 'src/maasserver/api/users.py' | |||
109 | --- src/maasserver/api/users.py 2015-05-07 18:14:38 +0000 | |||
110 | +++ src/maasserver/api/users.py 2015-10-28 10:52:32 +0000 | |||
111 | @@ -18,6 +18,7 @@ | |||
112 | 18 | ] | 18 | ] |
113 | 19 | 19 | ||
114 | 20 | 20 | ||
115 | 21 | from django.core.exceptions import ValidationError | ||
116 | 21 | from django.shortcuts import get_object_or_404 | 22 | from django.shortcuts import get_object_or_404 |
117 | 22 | from maasserver.api.support import ( | 23 | from maasserver.api.support import ( |
118 | 23 | admin_method, | 24 | admin_method, |
119 | @@ -28,6 +29,9 @@ | |||
120 | 28 | get_mandatory_param, | 29 | get_mandatory_param, |
121 | 29 | ) | 30 | ) |
122 | 30 | from maasserver.models import User | 31 | from maasserver.models import User |
123 | 32 | from maasserver.utils.orm import get_one | ||
124 | 33 | from piston.models import Consumer | ||
125 | 34 | from piston.utils import rc | ||
126 | 31 | 35 | ||
127 | 32 | 36 | ||
128 | 33 | class UsersHandler(OperationsHandler): | 37 | class UsersHandler(OperationsHandler): |
129 | @@ -79,7 +83,7 @@ | |||
130 | 79 | class UserHandler(OperationsHandler): | 83 | class UserHandler(OperationsHandler): |
131 | 80 | """Manage a user account.""" | 84 | """Manage a user account.""" |
132 | 81 | api_doc_section_name = "User" | 85 | api_doc_section_name = "User" |
134 | 82 | create = update = delete = None | 86 | create = update = None |
135 | 83 | 87 | ||
136 | 84 | model = User | 88 | model = User |
137 | 85 | fields = ( | 89 | fields = ( |
138 | @@ -90,3 +94,24 @@ | |||
139 | 90 | 94 | ||
140 | 91 | def read(self, request, username): | 95 | def read(self, request, username): |
141 | 92 | return get_object_or_404(User, username=username) | 96 | return get_object_or_404(User, username=username) |
142 | 97 | |||
143 | 98 | @admin_method | ||
144 | 99 | def delete(self, request, username): | ||
145 | 100 | """Deletes a user""" | ||
146 | 101 | if request.user.username == username: | ||
147 | 102 | raise ValidationError("An administrator cannot self-delete.") | ||
148 | 103 | |||
149 | 104 | user = get_one(User.objects.filter(username=username)) | ||
150 | 105 | |||
151 | 106 | if user is not None: | ||
152 | 107 | if user.node_set.exists(): | ||
153 | 108 | raise ValidationError( | ||
154 | 109 | "A user with assigned nodes cannot be deleted.") | ||
155 | 110 | elif user.staticipaddress_set.exists(): | ||
156 | 111 | raise ValidationError( | ||
157 | 112 | "A user with reserved IP addresses cannot be deleted.") | ||
158 | 113 | else: | ||
159 | 114 | Consumer.objects.filter(user=user).delete() | ||
160 | 115 | user.delete() | ||
161 | 116 | |||
162 | 117 | return rc.DELETED | ||
163 | 93 | 118 | ||
164 | === modified file 'src/maasserver/models/filestorage.py' | |||
165 | --- src/maasserver/models/filestorage.py 2015-05-07 18:14:38 +0000 | |||
166 | +++ src/maasserver/models/filestorage.py 2015-10-28 10:52:32 +0000 | |||
167 | @@ -26,6 +26,7 @@ | |||
168 | 26 | ForeignKey, | 26 | ForeignKey, |
169 | 27 | Manager, | 27 | Manager, |
170 | 28 | Model, | 28 | Model, |
171 | 29 | PROTECT, | ||
172 | 29 | ) | 30 | ) |
173 | 30 | from django.utils.http import urlencode | 31 | from django.utils.http import urlencode |
174 | 31 | from maasserver import DefaultMeta | 32 | from maasserver import DefaultMeta |
175 | @@ -89,7 +90,8 @@ | |||
176 | 89 | # owner can be None: this is to support upgrading existing | 90 | # owner can be None: this is to support upgrading existing |
177 | 90 | # installations where the files were not linked to users yet. | 91 | # installations where the files were not linked to users yet. |
178 | 91 | owner = ForeignKey( | 92 | owner = ForeignKey( |
180 | 92 | User, default=None, blank=True, null=True, editable=False) | 93 | User, default=None, blank=True, null=True, editable=False, |
181 | 94 | on_delete=PROTECT) | ||
182 | 93 | key = CharField( | 95 | key = CharField( |
183 | 94 | max_length=36, unique=True, default=generate_filestorage_key, | 96 | max_length=36, unique=True, default=generate_filestorage_key, |
184 | 95 | editable=False) | 97 | editable=False) |
185 | 96 | 98 | ||
186 | === modified file 'src/maasserver/models/node.py' | |||
187 | --- src/maasserver/models/node.py 2015-10-28 02:02:45 +0000 | |||
188 | +++ src/maasserver/models/node.py 2015-10-28 10:52:32 +0000 | |||
189 | @@ -44,6 +44,7 @@ | |||
190 | 44 | IntegerField, | 44 | IntegerField, |
191 | 45 | Manager, | 45 | Manager, |
192 | 46 | ManyToManyField, | 46 | ManyToManyField, |
193 | 47 | PROTECT, | ||
194 | 47 | Q, | 48 | Q, |
195 | 48 | SET_DEFAULT, | 49 | SET_DEFAULT, |
196 | 49 | SET_NULL, | 50 | SET_NULL, |
197 | @@ -447,7 +448,8 @@ | |||
198 | 447 | default=NODE_STATUS.DEFAULT) | 448 | default=NODE_STATUS.DEFAULT) |
199 | 448 | 449 | ||
200 | 449 | owner = ForeignKey( | 450 | owner = ForeignKey( |
202 | 450 | User, default=None, blank=True, null=True, editable=False) | 451 | User, default=None, blank=True, null=True, editable=False, |
203 | 452 | on_delete=PROTECT) | ||
204 | 451 | 453 | ||
205 | 452 | bios_boot_method = CharField(max_length=31, blank=True, null=True) | 454 | bios_boot_method = CharField(max_length=31, blank=True, null=True) |
206 | 453 | 455 | ||
207 | 454 | 456 | ||
208 | === modified file 'src/maasserver/models/sshkey.py' | |||
209 | --- src/maasserver/models/sshkey.py 2015-05-07 18:14:38 +0000 | |||
210 | +++ src/maasserver/models/sshkey.py 2015-10-28 10:52:32 +0000 | |||
211 | @@ -22,6 +22,7 @@ | |||
212 | 22 | from django.contrib.auth.models import User | 22 | from django.contrib.auth.models import User |
213 | 23 | from django.core.exceptions import ValidationError | 23 | from django.core.exceptions import ValidationError |
214 | 24 | from django.db.models import ( | 24 | from django.db.models import ( |
215 | 25 | CASCADE, | ||
216 | 25 | ForeignKey, | 26 | ForeignKey, |
217 | 26 | Manager, | 27 | Manager, |
218 | 27 | TextField, | 28 | TextField, |
219 | @@ -124,7 +125,7 @@ | |||
220 | 124 | 125 | ||
221 | 125 | objects = SSHKeyManager() | 126 | objects = SSHKeyManager() |
222 | 126 | 127 | ||
224 | 127 | user = ForeignKey(User, null=False, editable=False) | 128 | user = ForeignKey(User, null=False, editable=False, on_delete=CASCADE) |
225 | 128 | 129 | ||
226 | 129 | key = TextField( | 130 | key = TextField( |
227 | 130 | null=False, editable=True, validators=[validate_ssh_public_key]) | 131 | null=False, editable=True, validators=[validate_ssh_public_key]) |
228 | 131 | 132 | ||
229 | === modified file 'src/maasserver/models/sslkey.py' | |||
230 | --- src/maasserver/models/sslkey.py 2015-09-24 16:22:12 +0000 | |||
231 | +++ src/maasserver/models/sslkey.py 2015-10-28 10:52:32 +0000 | |||
232 | @@ -23,6 +23,7 @@ | |||
233 | 23 | from django.contrib.auth.models import User | 23 | from django.contrib.auth.models import User |
234 | 24 | from django.core.exceptions import ValidationError | 24 | from django.core.exceptions import ValidationError |
235 | 25 | from django.db.models import ( | 25 | from django.db.models import ( |
236 | 26 | CASCADE, | ||
237 | 26 | ForeignKey, | 27 | ForeignKey, |
238 | 27 | Manager, | 28 | Manager, |
239 | 28 | TextField, | 29 | TextField, |
240 | @@ -93,7 +94,7 @@ | |||
241 | 93 | """ | 94 | """ |
242 | 94 | objects = SSLKeyManager() | 95 | objects = SSLKeyManager() |
243 | 95 | 96 | ||
245 | 96 | user = ForeignKey(User, null=False, editable=False) | 97 | user = ForeignKey(User, null=False, editable=False, on_delete=CASCADE) |
246 | 97 | key = TextField( | 98 | key = TextField( |
247 | 98 | null=False, blank=False, editable=True, validators=[validate_ssl_key]) | 99 | null=False, blank=False, editable=True, validators=[validate_ssl_key]) |
248 | 99 | 100 | ||
249 | 100 | 101 | ||
250 | === modified file 'src/maasserver/models/staticipaddress.py' | |||
251 | --- src/maasserver/models/staticipaddress.py 2015-09-30 18:42:27 +0000 | |||
252 | +++ src/maasserver/models/staticipaddress.py 2015-10-28 10:52:32 +0000 | |||
253 | @@ -36,6 +36,7 @@ | |||
254 | 36 | ForeignKey, | 36 | ForeignKey, |
255 | 37 | IntegerField, | 37 | IntegerField, |
256 | 38 | Manager, | 38 | Manager, |
257 | 39 | PROTECT, | ||
258 | 39 | ) | 40 | ) |
259 | 40 | from maasserver import ( | 41 | from maasserver import ( |
260 | 41 | DefaultMeta, | 42 | DefaultMeta, |
261 | @@ -594,7 +595,8 @@ | |||
262 | 594 | validators=[validate_hostname]) | 595 | validators=[validate_hostname]) |
263 | 595 | 596 | ||
264 | 596 | user = ForeignKey( | 597 | user = ForeignKey( |
266 | 597 | User, default=None, blank=True, null=True, editable=False) | 598 | User, default=None, blank=True, null=True, editable=False, |
267 | 599 | on_delete=PROTECT) | ||
268 | 598 | 600 | ||
269 | 599 | objects = StaticIPAddressManager() | 601 | objects = StaticIPAddressManager() |
270 | 600 | 602 |
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.