Merge ~blake-rouse/maas:rbac-pools-cache-perms into maas:master
- Git
- lp:~blake-rouse/maas
- rbac-pools-cache-perms
- Merge into 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) |
Related bugs: |
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.
Description of the change
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
1 | diff --git a/src/maasserver/models/__init__.py b/src/maasserver/models/__init__.py |
2 | index 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: |
65 | diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py |
66 | index 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 | |
85 | diff --git a/src/maasserver/models/resourcepool.py b/src/maasserver/models/resourcepool.py |
86 | index 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 | |
99 | diff --git a/src/maasserver/rbac.py b/src/maasserver/rbac.py |
100 | index 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 | |
188 | diff --git a/src/maasserver/testing/fixtures.py b/src/maasserver/testing/fixtures.py |
189 | index 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) |
231 | diff --git a/src/maasserver/tests/test_auth.py b/src/maasserver/tests/test_auth.py |
232 | index 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( |
266 | diff --git a/src/maasserver/tests/test_rbac.py b/src/maasserver/tests/test_rbac.py |
267 | index 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') |