Merge lp:~trapnine/maas/b1459762 into lp:~maas-committers/maas/trunk

Proposed by Jeffrey C Jones
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
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://launchpad.net/bugs/1459762

To post a comment you must log in.
Revision history for this message
Jeffrey C Jones (trapnine) wrote :

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.

Revision history for this message
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.

review: Needs Information
Revision history for this message
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.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks Blake.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.0 MiB)

The attempt to merge lp:~trapnine/maas/b1459762 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Get:2 http://security.ubuntu.com trusty-security Release [63.5 kB]
Get:3 http://security.ubuntu.com trusty-security/main Sources [94.6 kB]
Get:4 http://security.ubuntu.com trusty-security/universe Sources [30.6 kB]
Get:5 http://security.ubuntu.com trusty-security/main amd64 Packages [344 kB]
Get:6 http://security.ubuntu.com trusty-security/universe amd64 Packages [116 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:7 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:8 http://nova.clouds.archive.ubuntu.com trusty-updates Release [63.5 kB]
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
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [234 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [136 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [619 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [313 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
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,015 kB in 3s (565 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 pyt...

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

The attempt to merge lp:~trapnine/maas/b1459762 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:2 http://security.ubuntu.com trusty-security Release [63.5 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [63.5 kB]
Get:5 http://security.ubuntu.com trusty-security/main Sources [95.8 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:6 http://security.ubuntu.com trusty-security/universe Sources [30.6 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [344 kB]
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
Get:8 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [235 kB]
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [136 kB]
Get:10 http://security.ubuntu.com trusty-security/universe amd64 Packages [116 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [620 kB]
Get:12 http://security.ubuntu.com trusty-security/main Translation-en [187 kB]
Get:13 http://security.ubuntu.com trusty-security/universe Translation-en [67.8 kB]
Get:14 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [312 kB]
Get:15 http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en [300 kB]
Get:16 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en [166 kB]
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,739 kB in 3s (781 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-neti...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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))