Merge lp:~trapnine/maas/b1459762 into lp:~maas-committers/maas/trunk
- b1459762
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Andres Rodriguez |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4294 |
Proposed branch: | lp:~trapnine/maas/b1459762 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
275 lines (+63/-34) 3 files modified
src/maasserver/api/nodes.py (+5/-4) src/maasserver/api/tests/test_node.py (+30/-22) src/maasserver/api/tests/test_nodes.py (+28/-8) |
To merge this branch: | bzr merge lp:~trapnine/maas/b1459762 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Blake Rouse (community) | Approve | ||
Review via email: mp+271909@code.launchpad.net |
Commit message
API requires user to be admin to PUT any update to Node
fixes bug: https:/
Description of the change
Jeffrey C Jones (trapnine) wrote : | # |
Gavin Panella (allenap) wrote : | # |
I get the feeling that access to a node is or should be more nuanced.
* A unowned node can only be updated by an admin, with one exception
(IIRC): it can be acquired from the Ready state by a non-admin.
* An owned node can be modified by its owner. However, some parameters
may only be modifiable by an owner who is also an admin.
This branch seems to prevent non-admins from modifying anything about a
node, and I'm not sure that's what we want.
Blake Rouse (blake-rouse) wrote : | # |
@Gavin
I don't believe there is a single attribute that can be changed by a standard user when it is acquired. When you need to set the distro_series or something else those are exposed on other endpoints. The attributes that can be changed with this endpoint really are administrator only. Back when it was used to set the distro_series and other attributes it was different, this just has not been updated to fit the new flow of MAAS.
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~trapnine/maas/b1459762 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Get:3 http://
Get:4 http://
Get:5 http://
Get:6 http://
Hit http://
Hit http://
Ign http://
Ign http://
Hit http://
Get:7 http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 2,015 kB in 3s (565 kB/s)
Reading package lists...
sudo DEBIAN_
--
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~trapnine/maas/b1459762 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Ign http://
Ign http://
Get:2 http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Hit http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Hit http://
Get:8 http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Get:13 http://
Get:14 http://
Get:15 http://
Get:16 http://
Ign http://
Ign http://
Fetched 2,739 kB in 3s (781 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === modified file 'src/maasserver/api/nodes.py' |
2 | --- src/maasserver/api/nodes.py 2015-09-18 17:33:48 +0000 |
3 | +++ src/maasserver/api/nodes.py 2015-09-23 01:02:15 +0000 |
4 | @@ -309,6 +309,7 @@ |
5 | return Node.nodes.get_node_or_404( |
6 | system_id=system_id, user=request.user, perm=NODE_PERMISSION.VIEW) |
7 | |
8 | + @admin_method |
9 | def update(self, request, system_id): |
10 | """Update a specific Node. |
11 | |
12 | @@ -442,7 +443,7 @@ |
13 | encoding instead. |
14 | |
15 | Returns 404 if the node is not found. |
16 | - Returns 403 if the user does not have permission to stop the node. |
17 | + Returns 403 if the user does not have permission to start the node. |
18 | Returns 503 if the start-up attempted to allocate an IP address, |
19 | and there were no IP addresses available on the relevant cluster |
20 | interface. |
21 | @@ -702,7 +703,7 @@ |
22 | |
23 | Returns 404 if the node is not found. |
24 | Returns 403 if the user does not have permission to mark the node |
25 | - broken. |
26 | + fixed. |
27 | """ |
28 | comment = get_optional_param(request.POST, 'comment') |
29 | node = Node.nodes.get_node_or_404( |
30 | @@ -844,8 +845,8 @@ |
31 | |
32 | Returns 400 if the node is currently not allocated. |
33 | Returns 404 if the node could not be found. |
34 | - Returns 403 if the user does not have permission to abort the |
35 | - current operation. |
36 | + Returns 403 if the user does not have permission to set the storage |
37 | + layout. |
38 | """ |
39 | node = Node.nodes.get_node_or_404( |
40 | system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT) |
41 | |
42 | === modified file 'src/maasserver/api/tests/test_node.py' |
43 | --- src/maasserver/api/tests/test_node.py 2015-09-18 17:33:48 +0000 |
44 | +++ src/maasserver/api/tests/test_node.py 2015-09-23 01:02:15 +0000 |
45 | @@ -865,6 +865,7 @@ |
46 | self.assertTrue(node.skip_networking) |
47 | |
48 | def test_PUT_updates_node(self): |
49 | + self.become_admin() |
50 | # The api allows the updating of a Node. |
51 | node = factory.make_Node( |
52 | hostname='diane', owner=self.logged_in_user, |
53 | @@ -882,6 +883,7 @@ |
54 | self.assertEqual(1, Node.objects.filter(hostname='francis').count()) |
55 | |
56 | def test_PUT_omitted_hostname(self): |
57 | + self.become_admin() |
58 | hostname = factory.make_name('hostname') |
59 | arch = make_usable_architecture(self) |
60 | node = factory.make_Node( |
61 | @@ -893,6 +895,7 @@ |
62 | self.assertTrue(Node.objects.filter(hostname=hostname).exists()) |
63 | |
64 | def test_PUT_rejects_device(self): |
65 | + self.become_admin() |
66 | node = factory.make_Node( |
67 | installable=False, owner=self.logged_in_user) |
68 | response = self.client.put(self.get_node_uri(node)) |
69 | @@ -900,6 +903,7 @@ |
70 | httplib.NOT_FOUND, response.status_code, response.content) |
71 | |
72 | def test_PUT_ignores_unknown_fields(self): |
73 | + self.become_admin() |
74 | node = factory.make_Node( |
75 | owner=self.logged_in_user, |
76 | architecture=make_usable_architecture(self)) |
77 | @@ -919,11 +923,10 @@ |
78 | owner=self.logged_in_user, |
79 | power_type=original_power_type, |
80 | architecture=make_usable_architecture(self)) |
81 | - self.client.put( |
82 | - self.get_node_uri(node), |
83 | - {'power_type': new_power_type} |
84 | - ) |
85 | + response = self.client.put( |
86 | + self.get_node_uri(node), {'power_type': new_power_type}) |
87 | |
88 | + self.assertEqual(httplib.OK, response.status_code) |
89 | self.assertEqual( |
90 | new_power_type, reload_object(node).power_type) |
91 | |
92 | @@ -932,15 +935,15 @@ |
93 | new_power_type = factory.pick_power_type(but_not=original_power_type) |
94 | node = factory.make_Node( |
95 | owner=self.logged_in_user, power_type=original_power_type) |
96 | - self.client.put( |
97 | - self.get_node_uri(node), |
98 | - {'power_type': new_power_type} |
99 | - ) |
100 | + response = self.client.put( |
101 | + self.get_node_uri(node), {'power_type': new_power_type}) |
102 | |
103 | + self.assertEqual(httplib.FORBIDDEN, response.status_code) |
104 | self.assertEqual( |
105 | original_power_type, reload_object(node).power_type) |
106 | |
107 | def test_resource_uri_points_back_at_node(self): |
108 | + self.become_admin() |
109 | # When a Node is returned by the API, the field 'resource_uri' |
110 | # provides the URI for this Node. |
111 | node = factory.make_Node( |
112 | @@ -950,6 +953,7 @@ |
113 | self.get_node_uri(node), {'hostname': 'francis'}) |
114 | parsed_result = json.loads(response.content) |
115 | |
116 | + self.assertEqual(httplib.OK, response.status_code) |
117 | self.assertEqual( |
118 | reverse('node_handler', args=[parsed_result['system_id']]), |
119 | parsed_result['resource_uri']) |
120 | @@ -957,6 +961,7 @@ |
121 | def test_PUT_rejects_invalid_data(self): |
122 | # If the data provided to update a node is invalid, a 'Bad request' |
123 | # response is returned. |
124 | + self.become_admin() |
125 | node = factory.make_Node( |
126 | hostname='diane', owner=self.logged_in_user, |
127 | architecture=make_usable_architecture(self)) |
128 | @@ -969,19 +974,10 @@ |
129 | {'hostname': ["DNS name contains an empty label."]}, |
130 | parsed_result) |
131 | |
132 | - def test_PUT_refuses_to_update_invisible_node(self): |
133 | - # The request to update a single node is denied if the node isn't |
134 | - # visible by the user. |
135 | - other_node = factory.make_Node( |
136 | - status=NODE_STATUS.ALLOCATED, owner=factory.make_User()) |
137 | - |
138 | - response = self.client.put(self.get_node_uri(other_node)) |
139 | - |
140 | - self.assertEqual(httplib.FORBIDDEN, response.status_code) |
141 | - |
142 | def test_PUT_refuses_to_update_nonexistent_node(self): |
143 | # When updating a Node, the api returns a 'Not Found' (404) error |
144 | # if no node is found. |
145 | + self.become_admin() |
146 | url = reverse('node_handler', args=['invalid-uuid']) |
147 | response = self.client.put(url) |
148 | |
149 | @@ -1218,6 +1214,14 @@ |
150 | node = reload_object(node) |
151 | self.assertEqual(zone, node.zone) |
152 | |
153 | + def test_PUT_requires_admin(self): |
154 | + node = factory.make_Node( |
155 | + owner=self.logged_in_user, |
156 | + architecture=make_usable_architecture(self)) |
157 | + # PUT the node with no arguments - should get FORBIDDEN |
158 | + response = self.client.put(self.get_node_uri(node), {}) |
159 | + self.assertEqual(httplib.FORBIDDEN, response.status_code) |
160 | + |
161 | def test_PUT_zone_change_requires_admin(self): |
162 | new_zone = factory.make_Zone() |
163 | node = factory.make_Node( |
164 | @@ -1229,14 +1233,13 @@ |
165 | self.get_node_uri(node), |
166 | {'zone': new_zone.name}) |
167 | |
168 | - # Awkwardly, the request succeeds because for non-admins, "zone" is |
169 | - # an unknown parameter. Unknown parameters are ignored. |
170 | - self.assertEqual(httplib.OK, response.status_code) |
171 | - # The node's physical zone, however, has not been updated. |
172 | + self.assertEqual(httplib.FORBIDDEN, response.status_code) |
173 | + # Confirm the node's physical zone has not been updated. |
174 | node = reload_object(node) |
175 | self.assertEqual(old_zone, node.zone) |
176 | |
177 | def test_PUT_sets_disable_ipv4(self): |
178 | + self.become_admin() |
179 | original_setting = factory.pick_bool() |
180 | node = factory.make_Node( |
181 | owner=self.logged_in_user, |
182 | @@ -1252,6 +1255,7 @@ |
183 | self.assertEqual(new_setting, node.disable_ipv4) |
184 | |
185 | def test_PUT_leaves_disable_ipv4_unchanged_by_default(self): |
186 | + self.become_admin() |
187 | original_setting = factory.pick_bool() |
188 | node = factory.make_Node( |
189 | owner=self.logged_in_user, |
190 | @@ -1267,6 +1271,7 @@ |
191 | self.assertEqual(original_setting, node.disable_ipv4) |
192 | |
193 | def test_PUT_updates_boot_type(self): |
194 | + self.become_admin() |
195 | node = factory.make_Node( |
196 | owner=self.logged_in_user, |
197 | architecture=make_usable_architecture(self), |
198 | @@ -1282,6 +1287,7 @@ |
199 | self.assertEqual(node.boot_type, NODE_BOOT.DEBIAN) |
200 | |
201 | def test_PUT_updates_swap_size(self): |
202 | + self.become_admin() |
203 | node = factory.make_Node(owner=self.logged_in_user, |
204 | architecture=make_usable_architecture(self)) |
205 | response = self.client.put( |
206 | @@ -1293,6 +1299,7 @@ |
207 | self.assertEqual(node.swap_size, parsed_result['swap_size']) |
208 | |
209 | def test_PUT_updates_swap_size_suffixes(self): |
210 | + self.become_admin() |
211 | node = factory.make_Node(owner=self.logged_in_user, |
212 | architecture=make_usable_architecture(self)) |
213 | |
214 | @@ -1325,6 +1332,7 @@ |
215 | self.assertEqual(5000000000000, parsed_result['swap_size']) |
216 | |
217 | def test_PUT_updates_swap_size_invalid_suffix(self): |
218 | + self.become_admin() |
219 | node = factory.make_Node(owner=self.logged_in_user, |
220 | architecture=make_usable_architecture(self)) |
221 | response = self.client.put( |
222 | |
223 | === modified file 'src/maasserver/api/tests/test_nodes.py' |
224 | --- src/maasserver/api/tests/test_nodes.py 2015-09-15 23:19:40 +0000 |
225 | +++ src/maasserver/api/tests/test_nodes.py 2015-09-23 01:02:15 +0000 |
226 | @@ -1847,14 +1847,33 @@ |
227 | self.assertEqual(self.old_allocated_status, parsed_result['status']) |
228 | |
229 | def test_PUT_updates_node_folds_status(self): |
230 | - node = factory.make_Node( |
231 | - owner=self.logged_in_user, status=self.status, |
232 | - architecture=make_usable_architecture(self)) |
233 | - response = self.client.put( |
234 | - self.get_node_uri(node), {'hostname': factory.make_name('host')}) |
235 | - parsed_result = json.loads(response.content) |
236 | - |
237 | - self.assertEqual(httplib.OK, response.status_code) |
238 | + self.become_admin() |
239 | + node = factory.make_Node( |
240 | + owner=self.logged_in_user, status=self.status, |
241 | + architecture=make_usable_architecture(self)) |
242 | + response = self.client.put( |
243 | + self.get_node_uri(node), {'hostname': factory.make_name('host')}) |
244 | + parsed_result = json.loads(response.content) |
245 | + |
246 | + self.assertEqual(httplib.OK, response.status_code) |
247 | + self.assertEqual(self.old_allocated_status, parsed_result['status']) |
248 | + |
249 | + def test_PUT_update_forbidden_if_not_admin(self): |
250 | + node = factory.make_Node( |
251 | + owner=self.logged_in_user, status=self.status, |
252 | + architecture=make_usable_architecture(self)) |
253 | + |
254 | + # should get FORBIDDEN because user isn't admin |
255 | + response = self.client.put( |
256 | + self.get_node_uri(node), {'hostname': factory.make_name('host')}) |
257 | + self.assertEqual(httplib.FORBIDDEN, response.status_code) |
258 | + |
259 | + # confirm operation succeeds as admin |
260 | + self.become_admin() |
261 | + response = self.client.put( |
262 | + self.get_node_uri(node), {'hostname': factory.make_name('host')}) |
263 | + self.assertEqual(httplib.OK, response.status_code) |
264 | + parsed_result = json.loads(response.content) |
265 | self.assertEqual(self.old_allocated_status, parsed_result['status']) |
266 | |
267 | def test_GET_list_allocated_exposes_substatus(self): |
268 | @@ -1882,6 +1901,7 @@ |
269 | self.assertEqual(self.status, parsed_result['substatus']) |
270 | |
271 | def test_PUT_updates_exposes_substatus(self): |
272 | + self.become_admin() |
273 | node = factory.make_Node( |
274 | owner=self.logged_in_user, status=self.status, |
275 | architecture=make_usable_architecture(self)) |
Please consider if this new behavior makes sense for the MAAS API.
All Node Update PUTs via the API will REQUIRE an admin user, or you get a 403 FORBIDDEN. The tests continue to pass after making the requesting user an admin.
I fixed a couple doc typos as well.