Merge lp:~trapnine/maas/fix-1576417-controllers-admin-only into lp:~maas-committers/maas/trunk

Proposed by Jeffrey C Jones
Status: Merged
Approved by: Jeffrey C Jones
Approved revision: no longer in the source branch.
Merged at revision: 5027
Proposed branch: lp:~trapnine/maas/fix-1576417-controllers-admin-only
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 357 lines (+135/-12)
11 files modified
src/maasserver/api/rackcontrollers.py (+2/-0)
src/maasserver/api/tests/test_events.py (+2/-0)
src/maasserver/api/tests/test_rackcontroller.py (+33/-0)
src/maasserver/api/tests/test_regioncontroller.py (+1/-0)
src/maasserver/api/tests/test_tag.py (+42/-6)
src/maasserver/models/node.py (+8/-1)
src/maasserver/models/tests/test_node.py (+21/-0)
src/maasserver/static/js/angular/controllers/nodes_list.js (+5/-0)
src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js (+16/-0)
src/maasserver/static/partials/nodes-list.html (+2/-2)
src/maasserver/websockets/handlers/tests/test_controller.py (+3/-3)
To merge this branch: bzr merge lp:~trapnine/maas/fix-1576417-controllers-admin-only
Reviewer Review Type Date Requested Status
Jeffrey C Jones (community) Approve
Gavin Panella (community) Approve
Review via email: mp+294785@code.launchpad.net

Commit message

Non-admins can't see controllers.

Description of the change

Non-admins can't see controllers.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good, except that isSuperUser needs a simple test.

review: Approve
Revision history for this message
Jeffrey C Jones (trapnine) wrote :

Approved by Gavin.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/rackcontrollers.py'
2--- src/maasserver/api/rackcontrollers.py 2016-04-28 01:11:50 +0000
3+++ src/maasserver/api/rackcontrollers.py 2016-05-16 16:20:02 +0000
4@@ -154,6 +154,7 @@
5 "Import of boot images started on %s" % rack.hostname,
6 content_type=("text/plain; charset=%s" % settings.DEFAULT_CHARSET))
7
8+ @admin_method
9 @operation(idempotent=True)
10 def list_boot_images(self, request, system_id):
11 """List all available boot images.
12@@ -192,6 +193,7 @@
13 "Import of boot images started on all rack controllers",
14 content_type=("text/plain; charset=%s" % settings.DEFAULT_CHARSET))
15
16+ @admin_method
17 @operation(idempotent=True)
18 def describe_power_types(self, request):
19 """Query all of the rack controllers for power information.
20
21=== modified file 'src/maasserver/api/tests/test_events.py'
22--- src/maasserver/api/tests/test_events.py 2016-02-02 14:20:45 +0000
23+++ src/maasserver/api/tests/test_events.py 2016-05-16 16:20:02 +0000
24@@ -143,6 +143,7 @@
25 # Even when node ids are passed to "list," events for nodes are
26 # returned in event id order, not necessarily in the order of the
27 # node id arguments.
28+ self.become_admin()
29 nodes = [factory.make_Node() for _ in range(3)]
30 events = [factory.make_Event(node=node) for node in nodes]
31 response = self.client.get(
32@@ -310,6 +311,7 @@
33 self.assertEqual(parsed_result['count'], len(events))
34
35 def test_GET_query_doesnt_list_devices(self):
36+ self.become_admin()
37 machines = [
38 factory.make_Node(
39 agent_name=factory.make_name('agent-name'),
40
41=== modified file 'src/maasserver/api/tests/test_rackcontroller.py'
42--- src/maasserver/api/tests/test_rackcontroller.py 2016-04-15 22:14:33 +0000
43+++ src/maasserver/api/tests/test_rackcontroller.py 2016-05-16 16:20:02 +0000
44@@ -6,6 +6,7 @@
45 import http.client
46
47 from django.core.urlresolvers import reverse
48+from maasserver.api import rackcontrollers
49 from maasserver.models import node as node_module
50 from maasserver.testing.api import (
51 APITestCase,
52@@ -17,6 +18,7 @@
53 from maastesting.matchers import (
54 MockCalledOnce,
55 MockCalledOnceWith,
56+ MockNotCalled,
57 )
58
59
60@@ -87,6 +89,7 @@
61
62 def test_GET_list_boot_images(self):
63 rack = factory.make_RackController(owner=factory.make_User())
64+ self.become_admin()
65 response = self.client.get(
66 self.get_rack_uri(rack), {'op': 'list_boot_images'})
67 self.assertEqual(
68@@ -96,6 +99,14 @@
69 ['connected', 'images', 'status'],
70 json_load_bytes(response.content))
71
72+ def test_GET_list_boot_images_denied_if_not_admin(self):
73+ rack = factory.make_RackController(owner=factory.make_User())
74+ response = self.client.get(
75+ self.get_rack_uri(rack), {'op': 'list_boot_images'})
76+ self.assertEqual(
77+ http.client.FORBIDDEN, response.status_code,
78+ explain_unexpected_response(http.client.FORBIDDEN, response))
79+
80
81 class TestRackControllersAPI(APITestCase):
82 """Tests for /api/2.0/rackcontrollers/."""
83@@ -110,6 +121,7 @@
84 '/api/2.0/rackcontrollers/', reverse('rackcontrollers_handler'))
85
86 def test_read_returns_limited_fields(self):
87+ self.become_admin()
88 factory.make_RackController(owner=self.logged_in_user)
89 response = self.client.get(reverse('rackcontrollers_handler'))
90 parsed_result = json_load_bytes(response.content)
91@@ -159,3 +171,24 @@
92 self.assertEqual(
93 http.client.FORBIDDEN, response.status_code,
94 explain_unexpected_response(http.client.FORBIDDEN, response))
95+
96+ def test_GET_describe_power_types(self):
97+ get_all_power_types_from_clusters = self.patch(
98+ rackcontrollers, "get_all_power_types_from_clusters")
99+ self.become_admin()
100+ response = self.client.get(
101+ self.get_rack_uri(), {'op': 'describe_power_types'})
102+ self.assertEqual(
103+ http.client.OK, response.status_code,
104+ explain_unexpected_response(http.client.OK, response))
105+ self.assertThat(get_all_power_types_from_clusters, MockCalledOnce())
106+
107+ def test_GET_describe_power_types_denied_if_not_admin(self):
108+ get_all_power_types_from_clusters = self.patch(
109+ rackcontrollers, "get_all_power_types_from_clusters")
110+ response = self.client.get(
111+ self.get_rack_uri(), {'op': 'describe_power_types'})
112+ self.assertEqual(
113+ http.client.FORBIDDEN, response.status_code,
114+ explain_unexpected_response(http.client.FORBIDDEN, response))
115+ self.assertThat(get_all_power_types_from_clusters, MockNotCalled())
116
117=== modified file 'src/maasserver/api/tests/test_regioncontroller.py'
118--- src/maasserver/api/tests/test_regioncontroller.py 2016-04-13 02:20:16 +0000
119+++ src/maasserver/api/tests/test_regioncontroller.py 2016-05-16 16:20:02 +0000
120@@ -54,6 +54,7 @@
121 reverse('regioncontrollers_handler'))
122
123 def test_read_returns_limited_fields(self):
124+ self.become_admin()
125 factory.make_RegionController()
126 response = self.client.get(reverse('regioncontrollers_handler'))
127 parsed_result = json_load_bytes(response.content)
128
129=== modified file 'src/maasserver/api/tests/test_tag.py'
130--- src/maasserver/api/tests/test_tag.py 2016-05-12 19:07:37 +0000
131+++ src/maasserver/api/tests/test_tag.py 2016-05-16 16:20:02 +0000
132@@ -147,11 +147,8 @@
133 self.assertEqual(http.client.OK, response.status_code)
134 parsed_result = json.loads(
135 response.content.decode(settings.DEFAULT_CHARSET))
136- # XXX lamont Bug#1576417 : Region controllers should not be here
137- # either, but are the subject of said bug.
138 self.assertItemsEqual(
139- [machine.system_id, device.system_id,
140- region.system_id],
141+ [machine.system_id, device.system_id],
142 [r['system_id'] for r in parsed_result])
143
144 def test_GET_machines_returns_machines(self):
145@@ -196,8 +193,6 @@
146 [device.system_id],
147 [r['system_id'] for r in parsed_result])
148
149- # XXX lamont Bug#1576417: Add a test that non-admins do not get to fetch
150- # rack controllers.
151 def test_GET_rack_controllers_returns_rack_controllers(self):
152 self.become_admin()
153 tag = factory.make_Tag()
154@@ -221,7 +216,28 @@
155 [rack.system_id],
156 [r['system_id'] for r in parsed_result])
157
158+ def test_GET_rack_controllers_returns_no_rack_controllers_nonadmin(self):
159+ tag = factory.make_Tag()
160+ machine = factory.make_Node()
161+ device = factory.make_Device()
162+ rack = factory.make_RackController()
163+ region = factory.make_RegionController()
164+ # Create a second node that isn't tagged.
165+ factory.make_Node()
166+ machine.tags.add(tag)
167+ device.tags.add(tag)
168+ rack.tags.add(tag)
169+ region.tags.add(tag)
170+ response = self.client.get(
171+ self.get_tag_uri(tag), {'op': 'rack_controllers'})
172+
173+ self.assertEqual(http.client.OK, response.status_code)
174+ parsed_result = json.loads(
175+ response.content.decode(settings.DEFAULT_CHARSET))
176+ self.assertItemsEqual([], parsed_result)
177+
178 def test_GET_region_controllers_returns_region_controllers(self):
179+ self.become_admin()
180 tag = factory.make_Tag()
181 machine = factory.make_Node()
182 device = factory.make_Device()
183@@ -243,6 +259,26 @@
184 [region.system_id],
185 [r['system_id'] for r in parsed_result])
186
187+ def test_GET_region_controllers_returns_no_controllers_nonadmin(self):
188+ tag = factory.make_Tag()
189+ machine = factory.make_Node()
190+ device = factory.make_Device()
191+ rack = factory.make_RackController()
192+ region = factory.make_RegionController()
193+ # Create a second node that isn't tagged.
194+ factory.make_Node()
195+ machine.tags.add(tag)
196+ device.tags.add(tag)
197+ rack.tags.add(tag)
198+ region.tags.add(tag)
199+ response = self.client.get(
200+ self.get_tag_uri(tag), {'op': 'region_controllers'})
201+
202+ self.assertEqual(http.client.OK, response.status_code)
203+ parsed_result = json.loads(
204+ response.content.decode(settings.DEFAULT_CHARSET))
205+ self.assertItemsEqual([], parsed_result)
206+
207 def test_GET_nodes_hides_invisible_nodes(self):
208 user2 = factory.make_User()
209 node1 = factory.make_Node()
210
211=== modified file 'src/maasserver/models/node.py'
212--- src/maasserver/models/node.py 2016-05-12 19:07:37 +0000
213+++ src/maasserver/models/node.py 2016-05-16 16:20:02 +0000
214@@ -409,7 +409,14 @@
215 if user.is_superuser:
216 # Admin is allowed to see all nodes.
217 return nodes
218- elif perm == NODE_PERMISSION.VIEW:
219+ # Non-admins aren't allowed to see controllers.
220+ nodes = nodes.exclude(
221+ Q(node_type__in=[
222+ NODE_TYPE.RACK_CONTROLLER,
223+ NODE_TYPE.REGION_CONTROLLER,
224+ NODE_TYPE.REGION_AND_RACK_CONTROLLER,
225+ ]))
226+ if perm == NODE_PERMISSION.VIEW:
227 return nodes.filter(Q(owner__isnull=True) | Q(owner=user))
228 elif perm == NODE_PERMISSION.EDIT:
229 return nodes.filter(owner=user)
230
231=== modified file 'src/maasserver/models/tests/test_node.py'
232--- src/maasserver/models/tests/test_node.py 2016-05-12 19:07:37 +0000
233+++ src/maasserver/models/tests/test_node.py 2016-05-16 16:20:02 +0000
234@@ -3255,6 +3255,9 @@
235 def test_get_nodes_with_admin_perm_returns_all_nodes_for_admin(self):
236 user = factory.make_User()
237 nodes = [self.make_node(user) for counter in range(5)]
238+ nodes.append(factory.make_RackController())
239+ nodes.append(factory.make_RegionController())
240+ nodes.append(factory.make_RegionRackController())
241 self.assertItemsEqual(
242 nodes,
243 Node.objects.get_nodes(
244@@ -3282,6 +3285,24 @@
245 from_nodes=Node.objects.all())
246 )
247
248+ def test_get_nodes_non_admin_hides_controllers(self):
249+ user = factory.make_User()
250+ user_visible_nodes = [self.make_node(user), self.make_node(None)]
251+ admin_visible_nodes = user_visible_nodes + [
252+ self.make_node(factory.make_User()),
253+ factory.make_RackController(owner=user),
254+ factory.make_RackController(owner=None),
255+ factory.make_RegionController(),
256+ factory.make_RegionRackController(),
257+ ]
258+ self.assertItemsEqual(
259+ admin_visible_nodes,
260+ Node.objects.get_nodes(
261+ factory.make_admin(), NODE_PERMISSION.ADMIN))
262+ self.assertItemsEqual(
263+ user_visible_nodes,
264+ Node.objects.get_nodes(user, NODE_PERMISSION.VIEW))
265+
266 def test_filter_nodes_by_spaces(self):
267 # Create a throwaway node and a throwaway space.
268 # (to ensure they are filtered out.)
269
270=== modified file 'src/maasserver/static/js/angular/controllers/nodes_list.js'
271--- src/maasserver/static/js/angular/controllers/nodes_list.js 2016-04-29 19:31:59 +0000
272+++ src/maasserver/static/js/angular/controllers/nodes_list.js 2016-05-16 16:20:02 +0000
273@@ -585,6 +585,11 @@
274 });
275 };
276
277+ // Return true if the authenticated user is super user.
278+ $scope.isSuperUser = function() {
279+ return UsersManager.isSuperUser();
280+ };
281+
282 // Load the required managers for this controller. The ServicesManager
283 // is required by the maasControllerStatus directive that is used
284 // in the partial for this controller.
285
286=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js'
287--- src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js 2016-04-29 07:41:09 +0000
288+++ src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js 2016-05-16 16:20:02 +0000
289@@ -134,6 +134,22 @@
290 return null;
291 }
292
293+ describe("isSuperUser", function() {
294+ it("returns true if the user is a superuser", function() {
295+ var controller = makeController();
296+ spyOn(UsersManager, "getAuthUser").and.returnValue(
297+ { is_superuser: true });
298+ expect($scope.isSuperUser()).toBe(true);
299+ });
300+
301+ it("returns false if the user is not a superuser", function() {
302+ var controller = makeController();
303+ spyOn(UsersManager, "getAuthUser").and.returnValue(
304+ { is_superuser: false });
305+ expect($scope.isSuperUser()).toBe(false);
306+ });
307+ });
308+
309 it("sets title and page on $rootScope", function() {
310 var controller = makeController();
311 expect($rootScope.title).toBe("Machines");
312
313=== modified file 'src/maasserver/static/partials/nodes-list.html'
314--- src/maasserver/static/partials/nodes-list.html 2016-05-11 19:01:48 +0000
315+++ src/maasserver/static/partials/nodes-list.html 2016-05-16 16:20:02 +0000
316@@ -11,8 +11,8 @@
317 data-ng-class="{ active: currentpage === 'devices' }"
318 data-ng-click="toggleTab('devices')">{$ devices.length $} <ng-pluralize count="devices.length" when="{'one': 'Device', 'other': 'Devices'}"></ng-pluralize>
319 </a>
320- <span class="divide"></span>
321- <a href=""
322+ <span data-ng-show="isSuperUser()" class="divide"></span>
323+ <a href="" data-ng-show="isSuperUser()"
324 data-ng-class="{ active: currentpage === 'controllers' }"
325 data-ng-click="toggleTab('controllers')">{$ controllers.length $} <ng-pluralize count="controllers.length" when="{'one': 'Controller', 'other': 'Controllers'}"></ng-pluralize>
326 </a>
327
328=== modified file 'src/maasserver/websockets/handlers/tests/test_controller.py'
329--- src/maasserver/websockets/handlers/tests/test_controller.py 2016-04-22 16:25:08 +0000
330+++ src/maasserver/websockets/handlers/tests/test_controller.py 2016-05-16 16:20:02 +0000
331@@ -21,7 +21,7 @@
332 factory.make_RackController()
333
334 def test_last_image_sync(self):
335- owner = factory.make_User()
336+ owner = factory.make_admin()
337 handler = ControllerHandler(owner, {})
338 node = factory.make_RackController(owner=owner)
339 result = handler.list({})
340@@ -34,7 +34,7 @@
341 node.last_image_sync))
342
343 def test_last_image_sync_returns_none_for_none(self):
344- owner = factory.make_User()
345+ owner = factory.make_admin()
346 handler = ControllerHandler(owner, {})
347 node = factory.make_RackController(owner=owner, last_image_sync=None)
348 result = handler.list({})
349@@ -45,7 +45,7 @@
350 self.assertIsNone(data.get("last_image_sync"))
351
352 def test_list_ignores_devices_and_nodes(self):
353- owner = factory.make_User()
354+ owner = factory.make_admin()
355 handler = ControllerHandler(owner, {})
356 # Create a device.
357 factory.make_Node(owner=owner, node_type=NODE_TYPE.DEVICE)