Merge ~blake-rouse/maas:rbac-pools-cache-perms into maas:master

Proposed by Blake Rouse
Status: Superseded
Proposed branch: ~blake-rouse/maas:rbac-pools-cache-perms
Merge into: maas:master
Diff against target: 351 lines (+162/-37)
7 files modified
src/maasserver/models/__init__.py (+23/-9)
src/maasserver/models/node.py (+4/-4)
src/maasserver/models/resourcepool.py (+2/-1)
src/maasserver/rbac.py (+49/-10)
src/maasserver/testing/fixtures.py (+25/-1)
src/maasserver/tests/test_auth.py (+24/-0)
src/maasserver/tests/test_rbac.py (+35/-12)
Reviewer Review Type Date Requested Status
MAAS Maintainers Pending
Review via email: mp+359062@code.launchpad.net

This proposal has been superseded by a proposal from 2018-11-20.

Commit message

Use a thread-local cache to cache resource pool identifiers per request.

To post a comment you must log in.
37c1e2a... by Blake Rouse

Add tests.

Unmerged commits

37c1e2a... by Blake Rouse

Add tests.

bcd535d... by Blake Rouse

Split 1.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/models/__init__.py b/src/maasserver/models/__init__.py
2index 3888e17..ca79112 100644
3--- a/src/maasserver/models/__init__.py
4+++ b/src/maasserver/models/__init__.py
5@@ -348,12 +348,10 @@ class MAASAuthorizationBackend(ModelBackend):
6 rbac_enabled = rbac.is_enabled()
7 visible_pools, admin_pools = [], []
8 if rbac_enabled:
9- visible_pools = rbac.get_resource_pools(
10- user.username, 'view').values_list(
11- 'id', flat=True)
12- admin_pools = rbac.get_resource_pools(
13- user.username, 'admin-machines').values_list(
14- 'id', flat=True)
15+ visible_pools = rbac.get_resource_pool_ids(
16+ user.username, 'view')
17+ admin_pools = rbac.get_resource_pool_ids(
18+ user.username, 'admin-machines')
19
20 # Sanity check that a `ResourcePool` is being checked against
21 # `ResourcePoolPermission`.
22@@ -363,6 +361,22 @@ class MAASAuthorizationBackend(ModelBackend):
23 'obj type of ResourcePool must be checked '
24 'against a `ResourcePoolPermission`.')
25
26+ # Handle node permissions without objects.
27+ if perm == NodePermission.admin and obj is None:
28+ # User wants to admin writes to all nodes (aka. create a node),
29+ # must be superuser for those permissions.
30+ return user.is_superuser
31+ elif perm == NodePermission.view and obj is None:
32+ # XXX 2018-11-20 blake_r: View permission without an obj is used
33+ # for device create as a standard user. Currently there is no
34+ # specific DevicePermission and no way for this code path to know
35+ # its for a device. So it is represented using this path.
36+ #
37+ # View is only used for the create action, modifying a created
38+ # device uses the appropriate `NodePermission.edit` scoped to the
39+ # device being editted.
40+ return True
41+
42 # ResourcePool permissions are handled specifically.
43 if isinstance(perm, ResourcePoolPermission):
44 return self._perm_resource_pool(
45@@ -386,7 +400,7 @@ class MAASAuthorizationBackend(ModelBackend):
46 rbac_enabled, user, obj, visible_pools, admin_pools)
47 return obj.pool_id is not None and can_edit
48 elif perm == NodePermission.admin:
49- return self._can_admin(
50+ return not obj.locked and self._can_admin(
51 rbac_enabled, user, obj, admin_pools)
52 else:
53 raise NotImplementedError(
54@@ -501,8 +515,8 @@ class MAASAuthorizationBackend(ModelBackend):
55
56 if perm == ResourcePoolPermission.edit:
57 if rbac_enabled:
58- return obj.id in rbac.get_resource_pools(
59- user.username, 'edit').values_list('id', flat=True)
60+ return obj.id in rbac.get_resource_pool_ids(
61+ user.username, 'edit')
62 return user.is_superuser
63 elif perm == ResourcePoolPermission.view:
64 if rbac_enabled:
65diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
66index 18beda4..cdbeb8a 100644
67--- a/src/maasserver/models/node.py
68+++ b/src/maasserver/models/node.py
69@@ -485,11 +485,11 @@ class BaseNodeManager(Manager, NodeQueriesMixin):
70 "Invalid permission check (invalid permission name: %s)." %
71 perm)
72 if rbac.is_enabled():
73- visible_pools = rbac.get_resource_pools(user.username, 'view')
74- admin_pools = rbac.get_resource_pools(
75+ visible_pools = rbac.get_resource_pool_ids(user.username, 'view')
76+ admin_pools = rbac.get_resource_pool_ids(
77 user.username, 'admin-machines')
78- condition |= Q(pool__in=admin_pools)
79- nodes = nodes.filter(pool__in=visible_pools)
80+ condition |= Q(pool_id__in=admin_pools)
81+ nodes = nodes.filter(pool_id__in=visible_pools)
82
83 return nodes.filter(condition)
84
85diff --git a/src/maasserver/models/resourcepool.py b/src/maasserver/models/resourcepool.py
86index 618f218..fc7af6c 100644
87--- a/src/maasserver/models/resourcepool.py
88+++ b/src/maasserver/models/resourcepool.py
89@@ -98,7 +98,8 @@ class ResourcePoolManager(Manager, ResourcePoolQueriesMixin):
90 # Circular imports.
91 from maasserver.rbac import rbac
92 if rbac.is_enabled():
93- return rbac.get_resource_pools(user.username, 'view')
94+ return self.filter(
95+ id__in=rbac.get_resource_pool_ids(user.username, 'view'))
96 return self.all()
97
98
99diff --git a/src/maasserver/rbac.py b/src/maasserver/rbac.py
100index ba24cf8..bff808c 100644
101--- a/src/maasserver/rbac.py
102+++ b/src/maasserver/rbac.py
103@@ -258,26 +258,65 @@ class RBACWrapper:
104 This marks a client as cleared that way only a new client is created
105 if the `rbac_url` is changed.
106 """
107+ self.clear_cache()
108 self._store.cleared = True
109
110 def is_enabled(self):
111 """Return whether MAAS has been configured to use RBAC."""
112 return self.client is not None
113
114- def get_resource_pools(self, user, permission):
115- """Get the resource pools that given user has the given permission on.
116+ def get_cache(self, resource, user, default=dict):
117+ """Return the cache for the `resource` and `user`."""
118+ cache = getattr(self._store, 'cache', None)
119+ if cache is None:
120+ cache = {}
121+ setattr(self._store, 'cache', cache)
122+ key = (resource, user)
123+ if key in cache:
124+ return cache[key]
125+ scoped = default()
126+ cache[key] = scoped
127+ return scoped
128+
129+ def clear_cache(self):
130+ """Clears the entire cache."""
131+ if hasattr(self._store, 'cache'):
132+ delattr(self._store, 'cache')
133+
134+ def _get_resource_pool_identifiers(self, user, permission):
135+ """Get the resource pool identifiers from RBAC.
136+
137+ Uses the thread-local cache so only one request is made to RBAC per
138+ request to MAAS.
139
140 @param user: The user name of the user.
141 @param permission: A permission that the user should
142 have on the resource pool.
143 """
144- pool_identifiers = self.client.allowed_for_user(
145- 'resource-pool', user, permission)
146- pools = ResourcePool.objects.all()
147- if pool_identifiers is not ALL_RESOURCES:
148+ cache = self.get_cache('resource-pool', user)
149+ pool_identifiers = cache.get(permission, None)
150+ if pool_identifiers is None:
151+ pool_identifiers = self.client.allowed_for_user(
152+ 'resource-pool', user, permission)
153+ cache[permission] = pool_identifiers
154+ return pool_identifiers
155+
156+ def get_resource_pool_ids(self, user, permission):
157+ """Get the resource pools ids that given user has the given
158+ permission on.
159+
160+ @param user: The user name of the user.
161+ @param permission: A permission that the user should
162+ have on the resource pool.
163+ """
164+ pool_identifiers = self._get_resource_pool_identifiers(
165+ user, permission)
166+ if pool_identifiers is ALL_RESOURCES:
167+ pool_ids = list(
168+ ResourcePool.objects.all().values_list('id', flat=True))
169+ else:
170 pool_ids = [int(identifier) for identifier in pool_identifiers]
171- pools = pools.filter(id__in=pool_ids)
172- return pools
173+ return pool_ids
174
175 def can_create_resource_pool(self, user):
176 """Return True if the `user` can create a resource pool.
177@@ -287,8 +326,8 @@ class RBACWrapper:
178
179 @param user: The user name of the user.
180 """
181- pool_identifiers = self.client.allowed_for_user(
182- 'resource-pool', user, 'edit')
183+ pool_identifiers = self._get_resource_pool_identifiers(
184+ user, 'edit')
185 return pool_identifiers is ALL_RESOURCES
186
187
188diff --git a/src/maasserver/testing/fixtures.py b/src/maasserver/testing/fixtures.py
189index 747d6eb..f1cdae2 100644
190--- a/src/maasserver/testing/fixtures.py
191+++ b/src/maasserver/testing/fixtures.py
192@@ -11,9 +11,13 @@ __all__ = [
193 import inspect
194 import logging
195
196+from django.db import connection
197 import fixtures
198 from maasserver.models.config import Config
199-from maasserver.rbac import rbac
200+from maasserver.rbac import (
201+ FakeRBACClient,
202+ rbac,
203+)
204 from maasserver.testing.factory import factory
205
206
207@@ -102,3 +106,23 @@ class RBACForceOffFixture(fixtures.Fixture):
208 rbac._get_rbac_url = orig_get_url
209
210 self.addCleanup(cleanup)
211+
212+
213+class RBACEnabled(fixtures.Fixture):
214+ """Fixture that enables RBAC."""
215+
216+ def _setUp(self):
217+ # Must be called inside a transaction.
218+ assert connection.in_atomic_block
219+
220+ Config.objects.set_config('rbac_url', 'http://rbac.example.com')
221+ client = FakeRBACClient()
222+ rbac._store.client = client
223+ rbac._store.cleared = False
224+ self.store = client.store
225+
226+ def cleanup():
227+ rbac._store.client = None
228+ rbac.clear()
229+
230+ self.addCleanup(cleanup)
231diff --git a/src/maasserver/tests/test_auth.py b/src/maasserver/tests/test_auth.py
232index e94107d..09153e9 100644
233--- a/src/maasserver/tests/test_auth.py
234+++ b/src/maasserver/tests/test_auth.py
235@@ -234,6 +234,30 @@ class TestMAASAuthorizationBackend(MAASServerTestCase, EnableRBACMixin):
236 NodePermission.admin,
237 make_allocated_node()))
238
239+ def test_admin_cannot_admin_locked_nodes(self):
240+ backend = MAASAuthorizationBackend()
241+ node = make_allocated_node()
242+ node.locked = True
243+ node.save()
244+ self.assertFalse(backend.has_perm(
245+ factory.make_admin(),
246+ NodePermission.admin, node))
247+
248+ def test_user_cannot_admin_all_nodes(self):
249+ backend = MAASAuthorizationBackend()
250+ self.assertFalse(
251+ backend.has_perm(factory.make_User(), NodePermission.admin))
252+
253+ def test_user_can_admin_all_nodes(self):
254+ backend = MAASAuthorizationBackend()
255+ self.assertTrue(
256+ backend.has_perm(factory.make_admin(), NodePermission.admin))
257+
258+ def test_user_can_view_all_nodes(self):
259+ backend = MAASAuthorizationBackend()
260+ self.assertTrue(
261+ backend.has_perm(factory.make_User(), NodePermission.view))
262+
263 def test_user_cannot_view_nodes_owned_by_others(self):
264 backend = MAASAuthorizationBackend()
265 self.assertFalse(backend.has_perm(
266diff --git a/src/maasserver/tests/test_rbac.py b/src/maasserver/tests/test_rbac.py
267index 721d552..f76238b 100644
268--- a/src/maasserver/tests/test_rbac.py
269+++ b/src/maasserver/tests/test_rbac.py
270@@ -254,24 +254,24 @@ class TestRBACWrapperGetResourcePools(MAASServerTestCase):
271 ResourcePool.objects.get_default_resource_pool())
272 self.store.add_pool(self.default_pool)
273
274- def test_get_resource_pools_unknown_user(self):
275+ def test_get_resource_pool_ids_unknown_user(self):
276 self.store.add_pool(factory.make_ResourcePool())
277 self.assertNotIn('user', self.store.allowed)
278 self.assertEqual(
279 [],
280- list(self.rbac.get_resource_pools('user', 'view')))
281+ list(self.rbac.get_resource_pool_ids('user', 'view')))
282
283- def test_get_resource_pools_user_allowed_all(self):
284+ def test_get_resource_pools_ids_user_allowed_all(self):
285 pool1 = factory.make_ResourcePool()
286 pool2 = factory.make_ResourcePool()
287 self.store.add_pool(pool1)
288 self.store.add_pool(pool2)
289 self.store.allow('user', ALL_RESOURCES, 'view')
290 self.assertCountEqual(
291- [self.default_pool, pool1, pool2],
292- self.rbac.get_resource_pools('user', 'view'))
293+ [self.default_pool.id, pool1.id, pool2.id],
294+ self.rbac.get_resource_pool_ids('user', 'view'))
295
296- def test_get_resource_pools_user_allowed_other_permission(self):
297+ def test_get_resource_pools_ids_user_allowed_other_permission(self):
298 pool1 = factory.make_ResourcePool()
299 pool2 = factory.make_ResourcePool()
300 self.store.add_pool(pool1)
301@@ -279,21 +279,44 @@ class TestRBACWrapperGetResourcePools(MAASServerTestCase):
302 self.store.allow('user', pool1, 'view')
303 self.store.allow('user', pool2, 'edit')
304 self.assertCountEqual(
305- [pool1],
306- self.rbac.get_resource_pools('user', 'view'))
307+ [pool1.id],
308+ self.rbac.get_resource_pool_ids('user', 'view'))
309 self.assertCountEqual(
310 [],
311- self.rbac.get_resource_pools('user', 'admin-machines'))
312+ self.rbac.get_resource_pool_ids('user', 'admin-machines'))
313
314- def test_get_resource_pools_user_allowed_some(self):
315+ def test_get_resource_pool_ids_user_allowed_some(self):
316 pool1 = factory.make_ResourcePool()
317 pool2 = factory.make_ResourcePool()
318 self.store.add_pool(pool1)
319 self.store.add_pool(pool2)
320 self.store.allow('user', pool1, 'view')
321 self.assertEqual(
322- sorted([pool1]),
323- sorted(self.rbac.get_resource_pools('user', 'view')))
324+ sorted([pool1.id]),
325+ sorted(self.rbac.get_resource_pool_ids('user', 'view')))
326+
327+ def test_get_resource_pool_ids_one_request_per_clear_cache(self):
328+ self.store.allow('user', self.default_pool, 'view')
329+ pools_one = self.rbac.get_resource_pool_ids('user', 'view')
330+ new_pool = factory.make_ResourcePool()
331+ self.store.allow('user', new_pool, 'view')
332+ pools_two = self.rbac.get_resource_pool_ids('user', 'view')
333+ self.rbac.clear_cache()
334+ pools_three = self.rbac.get_resource_pool_ids('user', 'view')
335+ self.assertItemsEqual([self.default_pool.id], pools_one)
336+ self.assertItemsEqual([self.default_pool.id], pools_two)
337+ self.assertItemsEqual([self.default_pool.id, new_pool.id], pools_three)
338+
339+ def test_get_resource_pool_ids_ALL_RESOURCES_always_returns_all(self):
340+ self.store.allow('user', ALL_RESOURCES, 'view')
341+ pools_one = self.rbac.get_resource_pool_ids('user', 'view')
342+ new_pool = factory.make_ResourcePool()
343+ pools_two = self.rbac.get_resource_pool_ids('user', 'view')
344+ self.rbac.clear_cache()
345+ pools_three = self.rbac.get_resource_pool_ids('user', 'view')
346+ self.assertItemsEqual([self.default_pool.id], pools_one)
347+ self.assertItemsEqual([self.default_pool.id, new_pool.id], pools_two)
348+ self.assertItemsEqual([self.default_pool.id, new_pool.id], pools_three)
349
350 def test_can_create_resource_pool_returns_True(self):
351 self.store.allow('user', ALL_RESOURCES, 'edit')

Subscribers

People subscribed via source and target branches