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
diff --git a/src/maasserver/models/__init__.py b/src/maasserver/models/__init__.py
index 3888e17..ca79112 100644
--- a/src/maasserver/models/__init__.py
+++ b/src/maasserver/models/__init__.py
@@ -348,12 +348,10 @@ class MAASAuthorizationBackend(ModelBackend):
348 rbac_enabled = rbac.is_enabled()348 rbac_enabled = rbac.is_enabled()
349 visible_pools, admin_pools = [], []349 visible_pools, admin_pools = [], []
350 if rbac_enabled:350 if rbac_enabled:
351 visible_pools = rbac.get_resource_pools(351 visible_pools = rbac.get_resource_pool_ids(
352 user.username, 'view').values_list(352 user.username, 'view')
353 'id', flat=True)353 admin_pools = rbac.get_resource_pool_ids(
354 admin_pools = rbac.get_resource_pools(354 user.username, 'admin-machines')
355 user.username, 'admin-machines').values_list(
356 'id', flat=True)
357355
358 # Sanity check that a `ResourcePool` is being checked against356 # Sanity check that a `ResourcePool` is being checked against
359 # `ResourcePoolPermission`.357 # `ResourcePoolPermission`.
@@ -363,6 +361,22 @@ class MAASAuthorizationBackend(ModelBackend):
363 'obj type of ResourcePool must be checked '361 'obj type of ResourcePool must be checked '
364 'against a `ResourcePoolPermission`.')362 'against a `ResourcePoolPermission`.')
365363
364 # Handle node permissions without objects.
365 if perm == NodePermission.admin and obj is None:
366 # User wants to admin writes to all nodes (aka. create a node),
367 # must be superuser for those permissions.
368 return user.is_superuser
369 elif perm == NodePermission.view and obj is None:
370 # XXX 2018-11-20 blake_r: View permission without an obj is used
371 # for device create as a standard user. Currently there is no
372 # specific DevicePermission and no way for this code path to know
373 # its for a device. So it is represented using this path.
374 #
375 # View is only used for the create action, modifying a created
376 # device uses the appropriate `NodePermission.edit` scoped to the
377 # device being editted.
378 return True
379
366 # ResourcePool permissions are handled specifically.380 # ResourcePool permissions are handled specifically.
367 if isinstance(perm, ResourcePoolPermission):381 if isinstance(perm, ResourcePoolPermission):
368 return self._perm_resource_pool(382 return self._perm_resource_pool(
@@ -386,7 +400,7 @@ class MAASAuthorizationBackend(ModelBackend):
386 rbac_enabled, user, obj, visible_pools, admin_pools)400 rbac_enabled, user, obj, visible_pools, admin_pools)
387 return obj.pool_id is not None and can_edit401 return obj.pool_id is not None and can_edit
388 elif perm == NodePermission.admin:402 elif perm == NodePermission.admin:
389 return self._can_admin(403 return not obj.locked and self._can_admin(
390 rbac_enabled, user, obj, admin_pools)404 rbac_enabled, user, obj, admin_pools)
391 else:405 else:
392 raise NotImplementedError(406 raise NotImplementedError(
@@ -501,8 +515,8 @@ class MAASAuthorizationBackend(ModelBackend):
501515
502 if perm == ResourcePoolPermission.edit:516 if perm == ResourcePoolPermission.edit:
503 if rbac_enabled:517 if rbac_enabled:
504 return obj.id in rbac.get_resource_pools(518 return obj.id in rbac.get_resource_pool_ids(
505 user.username, 'edit').values_list('id', flat=True)519 user.username, 'edit')
506 return user.is_superuser520 return user.is_superuser
507 elif perm == ResourcePoolPermission.view:521 elif perm == ResourcePoolPermission.view:
508 if rbac_enabled:522 if rbac_enabled:
diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
index 18beda4..cdbeb8a 100644
--- a/src/maasserver/models/node.py
+++ b/src/maasserver/models/node.py
@@ -485,11 +485,11 @@ class BaseNodeManager(Manager, NodeQueriesMixin):
485 "Invalid permission check (invalid permission name: %s)." %485 "Invalid permission check (invalid permission name: %s)." %
486 perm)486 perm)
487 if rbac.is_enabled():487 if rbac.is_enabled():
488 visible_pools = rbac.get_resource_pools(user.username, 'view')488 visible_pools = rbac.get_resource_pool_ids(user.username, 'view')
489 admin_pools = rbac.get_resource_pools(489 admin_pools = rbac.get_resource_pool_ids(
490 user.username, 'admin-machines')490 user.username, 'admin-machines')
491 condition |= Q(pool__in=admin_pools)491 condition |= Q(pool_id__in=admin_pools)
492 nodes = nodes.filter(pool__in=visible_pools)492 nodes = nodes.filter(pool_id__in=visible_pools)
493493
494 return nodes.filter(condition)494 return nodes.filter(condition)
495495
diff --git a/src/maasserver/models/resourcepool.py b/src/maasserver/models/resourcepool.py
index 618f218..fc7af6c 100644
--- a/src/maasserver/models/resourcepool.py
+++ b/src/maasserver/models/resourcepool.py
@@ -98,7 +98,8 @@ class ResourcePoolManager(Manager, ResourcePoolQueriesMixin):
98 # Circular imports.98 # Circular imports.
99 from maasserver.rbac import rbac99 from maasserver.rbac import rbac
100 if rbac.is_enabled():100 if rbac.is_enabled():
101 return rbac.get_resource_pools(user.username, 'view')101 return self.filter(
102 id__in=rbac.get_resource_pool_ids(user.username, 'view'))
102 return self.all()103 return self.all()
103104
104105
diff --git a/src/maasserver/rbac.py b/src/maasserver/rbac.py
index ba24cf8..bff808c 100644
--- a/src/maasserver/rbac.py
+++ b/src/maasserver/rbac.py
@@ -258,26 +258,65 @@ class RBACWrapper:
258 This marks a client as cleared that way only a new client is created258 This marks a client as cleared that way only a new client is created
259 if the `rbac_url` is changed.259 if the `rbac_url` is changed.
260 """260 """
261 self.clear_cache()
261 self._store.cleared = True262 self._store.cleared = True
262263
263 def is_enabled(self):264 def is_enabled(self):
264 """Return whether MAAS has been configured to use RBAC."""265 """Return whether MAAS has been configured to use RBAC."""
265 return self.client is not None266 return self.client is not None
266267
267 def get_resource_pools(self, user, permission):268 def get_cache(self, resource, user, default=dict):
268 """Get the resource pools that given user has the given permission on.269 """Return the cache for the `resource` and `user`."""
270 cache = getattr(self._store, 'cache', None)
271 if cache is None:
272 cache = {}
273 setattr(self._store, 'cache', cache)
274 key = (resource, user)
275 if key in cache:
276 return cache[key]
277 scoped = default()
278 cache[key] = scoped
279 return scoped
280
281 def clear_cache(self):
282 """Clears the entire cache."""
283 if hasattr(self._store, 'cache'):
284 delattr(self._store, 'cache')
285
286 def _get_resource_pool_identifiers(self, user, permission):
287 """Get the resource pool identifiers from RBAC.
288
289 Uses the thread-local cache so only one request is made to RBAC per
290 request to MAAS.
269291
270 @param user: The user name of the user.292 @param user: The user name of the user.
271 @param permission: A permission that the user should293 @param permission: A permission that the user should
272 have on the resource pool.294 have on the resource pool.
273 """295 """
274 pool_identifiers = self.client.allowed_for_user(296 cache = self.get_cache('resource-pool', user)
275 'resource-pool', user, permission)297 pool_identifiers = cache.get(permission, None)
276 pools = ResourcePool.objects.all()298 if pool_identifiers is None:
277 if pool_identifiers is not ALL_RESOURCES:299 pool_identifiers = self.client.allowed_for_user(
300 'resource-pool', user, permission)
301 cache[permission] = pool_identifiers
302 return pool_identifiers
303
304 def get_resource_pool_ids(self, user, permission):
305 """Get the resource pools ids that given user has the given
306 permission on.
307
308 @param user: The user name of the user.
309 @param permission: A permission that the user should
310 have on the resource pool.
311 """
312 pool_identifiers = self._get_resource_pool_identifiers(
313 user, permission)
314 if pool_identifiers is ALL_RESOURCES:
315 pool_ids = list(
316 ResourcePool.objects.all().values_list('id', flat=True))
317 else:
278 pool_ids = [int(identifier) for identifier in pool_identifiers]318 pool_ids = [int(identifier) for identifier in pool_identifiers]
279 pools = pools.filter(id__in=pool_ids)319 return pool_ids
280 return pools
281320
282 def can_create_resource_pool(self, user):321 def can_create_resource_pool(self, user):
283 """Return True if the `user` can create a resource pool.322 """Return True if the `user` can create a resource pool.
@@ -287,8 +326,8 @@ class RBACWrapper:
287326
288 @param user: The user name of the user.327 @param user: The user name of the user.
289 """328 """
290 pool_identifiers = self.client.allowed_for_user(329 pool_identifiers = self._get_resource_pool_identifiers(
291 'resource-pool', user, 'edit')330 user, 'edit')
292 return pool_identifiers is ALL_RESOURCES331 return pool_identifiers is ALL_RESOURCES
293332
294333
diff --git a/src/maasserver/testing/fixtures.py b/src/maasserver/testing/fixtures.py
index 747d6eb..f1cdae2 100644
--- a/src/maasserver/testing/fixtures.py
+++ b/src/maasserver/testing/fixtures.py
@@ -11,9 +11,13 @@ __all__ = [
11import inspect11import inspect
12import logging12import logging
1313
14from django.db import connection
14import fixtures15import fixtures
15from maasserver.models.config import Config16from maasserver.models.config import Config
16from maasserver.rbac import rbac17from maasserver.rbac import (
18 FakeRBACClient,
19 rbac,
20)
17from maasserver.testing.factory import factory21from maasserver.testing.factory import factory
1822
1923
@@ -102,3 +106,23 @@ class RBACForceOffFixture(fixtures.Fixture):
102 rbac._get_rbac_url = orig_get_url106 rbac._get_rbac_url = orig_get_url
103107
104 self.addCleanup(cleanup)108 self.addCleanup(cleanup)
109
110
111class RBACEnabled(fixtures.Fixture):
112 """Fixture that enables RBAC."""
113
114 def _setUp(self):
115 # Must be called inside a transaction.
116 assert connection.in_atomic_block
117
118 Config.objects.set_config('rbac_url', 'http://rbac.example.com')
119 client = FakeRBACClient()
120 rbac._store.client = client
121 rbac._store.cleared = False
122 self.store = client.store
123
124 def cleanup():
125 rbac._store.client = None
126 rbac.clear()
127
128 self.addCleanup(cleanup)
diff --git a/src/maasserver/tests/test_auth.py b/src/maasserver/tests/test_auth.py
index e94107d..09153e9 100644
--- a/src/maasserver/tests/test_auth.py
+++ b/src/maasserver/tests/test_auth.py
@@ -234,6 +234,30 @@ class TestMAASAuthorizationBackend(MAASServerTestCase, EnableRBACMixin):
234 NodePermission.admin,234 NodePermission.admin,
235 make_allocated_node()))235 make_allocated_node()))
236236
237 def test_admin_cannot_admin_locked_nodes(self):
238 backend = MAASAuthorizationBackend()
239 node = make_allocated_node()
240 node.locked = True
241 node.save()
242 self.assertFalse(backend.has_perm(
243 factory.make_admin(),
244 NodePermission.admin, node))
245
246 def test_user_cannot_admin_all_nodes(self):
247 backend = MAASAuthorizationBackend()
248 self.assertFalse(
249 backend.has_perm(factory.make_User(), NodePermission.admin))
250
251 def test_user_can_admin_all_nodes(self):
252 backend = MAASAuthorizationBackend()
253 self.assertTrue(
254 backend.has_perm(factory.make_admin(), NodePermission.admin))
255
256 def test_user_can_view_all_nodes(self):
257 backend = MAASAuthorizationBackend()
258 self.assertTrue(
259 backend.has_perm(factory.make_User(), NodePermission.view))
260
237 def test_user_cannot_view_nodes_owned_by_others(self):261 def test_user_cannot_view_nodes_owned_by_others(self):
238 backend = MAASAuthorizationBackend()262 backend = MAASAuthorizationBackend()
239 self.assertFalse(backend.has_perm(263 self.assertFalse(backend.has_perm(
diff --git a/src/maasserver/tests/test_rbac.py b/src/maasserver/tests/test_rbac.py
index 721d552..f76238b 100644
--- a/src/maasserver/tests/test_rbac.py
+++ b/src/maasserver/tests/test_rbac.py
@@ -254,24 +254,24 @@ class TestRBACWrapperGetResourcePools(MAASServerTestCase):
254 ResourcePool.objects.get_default_resource_pool())254 ResourcePool.objects.get_default_resource_pool())
255 self.store.add_pool(self.default_pool)255 self.store.add_pool(self.default_pool)
256256
257 def test_get_resource_pools_unknown_user(self):257 def test_get_resource_pool_ids_unknown_user(self):
258 self.store.add_pool(factory.make_ResourcePool())258 self.store.add_pool(factory.make_ResourcePool())
259 self.assertNotIn('user', self.store.allowed)259 self.assertNotIn('user', self.store.allowed)
260 self.assertEqual(260 self.assertEqual(
261 [],261 [],
262 list(self.rbac.get_resource_pools('user', 'view')))262 list(self.rbac.get_resource_pool_ids('user', 'view')))
263263
264 def test_get_resource_pools_user_allowed_all(self):264 def test_get_resource_pools_ids_user_allowed_all(self):
265 pool1 = factory.make_ResourcePool()265 pool1 = factory.make_ResourcePool()
266 pool2 = factory.make_ResourcePool()266 pool2 = factory.make_ResourcePool()
267 self.store.add_pool(pool1)267 self.store.add_pool(pool1)
268 self.store.add_pool(pool2)268 self.store.add_pool(pool2)
269 self.store.allow('user', ALL_RESOURCES, 'view')269 self.store.allow('user', ALL_RESOURCES, 'view')
270 self.assertCountEqual(270 self.assertCountEqual(
271 [self.default_pool, pool1, pool2],271 [self.default_pool.id, pool1.id, pool2.id],
272 self.rbac.get_resource_pools('user', 'view'))272 self.rbac.get_resource_pool_ids('user', 'view'))
273273
274 def test_get_resource_pools_user_allowed_other_permission(self):274 def test_get_resource_pools_ids_user_allowed_other_permission(self):
275 pool1 = factory.make_ResourcePool()275 pool1 = factory.make_ResourcePool()
276 pool2 = factory.make_ResourcePool()276 pool2 = factory.make_ResourcePool()
277 self.store.add_pool(pool1)277 self.store.add_pool(pool1)
@@ -279,21 +279,44 @@ class TestRBACWrapperGetResourcePools(MAASServerTestCase):
279 self.store.allow('user', pool1, 'view')279 self.store.allow('user', pool1, 'view')
280 self.store.allow('user', pool2, 'edit')280 self.store.allow('user', pool2, 'edit')
281 self.assertCountEqual(281 self.assertCountEqual(
282 [pool1],282 [pool1.id],
283 self.rbac.get_resource_pools('user', 'view'))283 self.rbac.get_resource_pool_ids('user', 'view'))
284 self.assertCountEqual(284 self.assertCountEqual(
285 [],285 [],
286 self.rbac.get_resource_pools('user', 'admin-machines'))286 self.rbac.get_resource_pool_ids('user', 'admin-machines'))
287287
288 def test_get_resource_pools_user_allowed_some(self):288 def test_get_resource_pool_ids_user_allowed_some(self):
289 pool1 = factory.make_ResourcePool()289 pool1 = factory.make_ResourcePool()
290 pool2 = factory.make_ResourcePool()290 pool2 = factory.make_ResourcePool()
291 self.store.add_pool(pool1)291 self.store.add_pool(pool1)
292 self.store.add_pool(pool2)292 self.store.add_pool(pool2)
293 self.store.allow('user', pool1, 'view')293 self.store.allow('user', pool1, 'view')
294 self.assertEqual(294 self.assertEqual(
295 sorted([pool1]),295 sorted([pool1.id]),
296 sorted(self.rbac.get_resource_pools('user', 'view')))296 sorted(self.rbac.get_resource_pool_ids('user', 'view')))
297
298 def test_get_resource_pool_ids_one_request_per_clear_cache(self):
299 self.store.allow('user', self.default_pool, 'view')
300 pools_one = self.rbac.get_resource_pool_ids('user', 'view')
301 new_pool = factory.make_ResourcePool()
302 self.store.allow('user', new_pool, 'view')
303 pools_two = self.rbac.get_resource_pool_ids('user', 'view')
304 self.rbac.clear_cache()
305 pools_three = self.rbac.get_resource_pool_ids('user', 'view')
306 self.assertItemsEqual([self.default_pool.id], pools_one)
307 self.assertItemsEqual([self.default_pool.id], pools_two)
308 self.assertItemsEqual([self.default_pool.id, new_pool.id], pools_three)
309
310 def test_get_resource_pool_ids_ALL_RESOURCES_always_returns_all(self):
311 self.store.allow('user', ALL_RESOURCES, 'view')
312 pools_one = self.rbac.get_resource_pool_ids('user', 'view')
313 new_pool = factory.make_ResourcePool()
314 pools_two = self.rbac.get_resource_pool_ids('user', 'view')
315 self.rbac.clear_cache()
316 pools_three = self.rbac.get_resource_pool_ids('user', 'view')
317 self.assertItemsEqual([self.default_pool.id], pools_one)
318 self.assertItemsEqual([self.default_pool.id, new_pool.id], pools_two)
319 self.assertItemsEqual([self.default_pool.id, new_pool.id], pools_three)
297320
298 def test_can_create_resource_pool_returns_True(self):321 def test_can_create_resource_pool_returns_True(self):
299 self.store.allow('user', ALL_RESOURCES, 'edit')322 self.store.allow('user', ALL_RESOURCES, 'edit')

Subscribers

People subscribed via source and target branches