[model] cache user permissions #145#153
Conversation
| self.phone_number = None | ||
|
|
||
| @cached_property | ||
| def get_permissions(self): |
There was a problem hiding this comment.
if it's a property, we don't need to name it get_permissions, we can name it just permissions.
The other problem is that cached_property does not cache using the cache storage, it caches only in memory, so the query will be executed again at subsequent requests.
We have to do something like organizations_dict: cache it in the cache storage (one key for each user) and invalidate the cache whenever the permissions or the groups of a user are changed.
Please study this PR and try to replicate what has been done there: https://github.com/openwisp/openwisp-users/pull/134/files.
| try: | ||
| del self.__dict__['get_permissions'] | ||
| except KeyError: | ||
| pass |
There was a problem hiding this comment.
why this? Looks like something that shouldn't be here.
|
|
||
| permission = Permission.objects.filter(codename='add_organization') | ||
| user.user_permissions.add(*permission) | ||
| self.assertEqual(user.get_all_permissions(), user.get_permissions) |
There was a problem hiding this comment.
please use assertNumQueries to ensure the query is executed only at the first call
There was a problem hiding this comment.
I sent this to help you: 9d55c31
It will log queries to the console when you debug in ./manage shell_plus.
Look at this:
# flush cache
redis-cli
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> quitLaunch shell and get user:
./manage.py shell_plus
In [1]: user = User.objects.first()
(0.001) SELECT "openwisp_users_user"."password", "openwisp_users_user"."last_login", "openwisp_users_user"."is_superuser", "openwisp_users_user"."username", "openwisp_users_user"."first_name", "openwisp_users_user"."last_name", "openwisp_users_user"."is_staff", "openwisp_users_user"."is_active", "openwisp_users_user"."date_joined", "openwisp_users_user"."id", "openwisp_users_user"."email", "openwisp_users_user"."bio", "openwisp_users_user"."url", "openwisp_users_user"."company", "openwisp_users_user"."location", "openwisp_users_user"."phone_number" FROM "openwisp_users_user" ORDER BY "openwisp_users_user"."id" ASC LIMIT 1; args=()
Calling user.organizations_dict the first time generates a query:
In [2]: user.organizations_dict
(0.000) SELECT "openwisp_users_organizationuser"."created", "openwisp_users_organizationuser"."modified", "openwisp_users_organizationuser"."is_admin", "openwisp_users_organizationuser"."id", "openwisp_users_organizationuser"."user_id", "openwisp_users_organizationuser"."organization_id", "openwisp_users_organization"."name", "openwisp_users_organization"."is_active", "openwisp_users_organization"."created", "openwisp_users_organization"."modified", "openwisp_users_organization"."slug", "openwisp_users_organization"."id", "openwisp_users_organization"."description", "openwisp_users_organization"."email", "openwisp_users_organization"."url", "openwisp_users_organizationowner"."created", "openwisp_users_organizationowner"."modified", "openwisp_users_organizationowner"."id", "openwisp_users_organizationowner"."organization_user_id", "openwisp_users_organizationowner"."organization_id" FROM "openwisp_users_organizationuser" INNER JOIN "openwisp_users_organization" ON ("openwisp_users_organizationuser"."organization_id" = "openwisp_users_organization"."id") LEFT OUTER JOIN "openwisp_users_organizationowner" ON ("openwisp_users_organizationuser"."id" = "openwisp_users_organizationowner"."organization_user_id") WHERE ("openwisp_users_organization"."is_active" = 1 AND "openwisp_users_organizationuser"."user_id" = '07e322d84afb40cd899b04904287160c') ORDER BY "openwisp_users_organization"."name" ASC, "openwisp_users_organizationuser"."user_id" ASC; args=(True, '07e322d84afb40cd899b04904287160c')
Out[2]: {'20135c30-d486-4d68-993f-322b8acb51c4': {'is_admin': True, 'is_owner': False}}
Create a new user instance for the same user and access organizations_dict again, see how no query is generated (retrieved from cache):
In [3]: testuser = User.objects.first()
(0.000) SELECT "openwisp_users_user"."password", "openwisp_users_user"."last_login", "openwisp_users_user"."is_superuser", "openwisp_users_user"."username", "openwisp_users_user"."first_name", "openwisp_users_user"."last_name", "openwisp_users_user"."is_staff", "openwisp_users_user"."is_active", "openwisp_users_user"."date_joined", "openwisp_users_user"."id", "openwisp_users_user"."email", "openwisp_users_user"."bio", "openwisp_users_user"."url", "openwisp_users_user"."company", "openwisp_users_user"."location", "openwisp_users_user"."phone_number" FROM "openwisp_users_user" ORDER BY "openwisp_users_user"."id" ASC LIMIT 1; args=()
In [4]: testuser.organizations_dict
Out[4]: {'20135c30-d486-4d68-993f-322b8acb51c4': {'is_admin': True, 'is_owner': False}}
The same happens if you close shell_plus and launch it again. The cache during manual testing resides in redis (according to our test/settings.py). This means the cache survives requests and is shared among different processes (eg: application server, websockets server, celery workers, management commands).
We have to achieve a similar result with this patch for user.permissions.
nemesifier
left a comment
There was a problem hiding this comment.
Can you create an alternative has_perm method (see the one in django) to make it use the new cached permissions?
Let's not override the one from django to avoid causing issues, we add a new one for openwisp internal usage only, we'll use it here: openwisp/openwisp-utils#123 (review)
|
ok @nemesisdesign thanks for the feedback. let me adjust them 😄 |
ea3d339 to
7e92de7
Compare
f9d1117 to
0429490
Compare
nemesifier
left a comment
There was a problem hiding this comment.
Great progress @NoumbissiValere, it's almost ready, see below for suggestions to make this even better!
| http GET localhost:8000/api/v1/firmware/build/ "Authorization: Bearer $TOKEN" | ||
|
|
||
| User permissions | ||
| --------------- |
There was a problem hiding this comment.
the length of this must be equal to the length of the heading.
I would also move this section after the organization membership helpers and call it User permissions helper for consistency.
| self.phone_number = None | ||
|
|
||
| @property | ||
| def permissions(self): |
There was a problem hiding this comment.
let's add a docstring:
"""
Returns the user permissions from the cache, if the cache is
empty it will call self.get_all_permissions() and cache the result
"""| and model.lower() == perm.split('_')[-1] | ||
| ): | ||
| return True | ||
| return False |
There was a problem hiding this comment.
Why don't we just do:
def has_permission(self, permission):
return permission in self.permissions
Since in openwisp-utils, we get the permission like {app_label}.{model}.
This method needs a brief mention in the README after User permissions
|
|
||
| All user permissions are cached and can be accessed as follows ``obj.permissions``. This cache | ||
| is updated each time the user's permissions or group is changed. | ||
|
|
There was a problem hiding this comment.
The ``User`` model of openwisp-users provides a ``permissions`` helper which returns the user's permissions from the cache, cache invalidation is handled automatically.
.. code-block:: python
>>> user.permissions
... {'account.add_emailaddress',
'account.change_emailaddress',
'account.delete_emailaddress',
'account.view_emailaddress',
'openwisp_users.add_organizationuser',
'openwisp_users.add_user',
'openwisp_users.change_organizationuser',
'openwisp_users.change_user',
'openwisp_users.delete_organizationuser',
'openwisp_users.delete_user'}
Closed issue by adding a helper to User model which caches all permissions for a particular user. Fixes #145
0429490 to
5952fea
Compare
0776a01 to
1f3422e
Compare
7d67d48 to
a0f46d8
Compare
[bug] Organization field is missing when importing a CA or cert. openwisp#152
Related to openwisp#152
Closed issue by adding a helper to
User model which caches all
permissions for a particular user.
Fixes #145