Skip to content

Commit

Permalink
Add permission caching
Browse files Browse the repository at this point in the history
  • Loading branch information
matllubos committed Jan 17, 2022
1 parent c5b783e commit 4885ac4
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 15 deletions.
10 changes: 10 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,13 @@ Commands
--------

We right now have implemented cores with a dynamic permission check. But database permissions should be somehow stored inside a database. The command `sync_permissions` is there for this purpose. The command will create new permissions and update already generated permissions.


Setting
-------

You can turn on permission caching with this settings::

IS_CORE_PERM_USE_CACHE = False
IS_CORE_PERM_CACHE_NAME = 'default' # name where the permissions are cached
IS_CORE_PERM_CACHE_TIMEOUT': 60 * 10 # timeout to cache permissions (10min)
25 changes: 24 additions & 1 deletion example/dj/apps/issue_tracker/tests/rest_permissions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from django.test import override_settings
from django.core.cache import cache

from germanium.decorators import login
from germanium.test_cases.rest import RestTestCase
from germanium.tools.http import assert_http_redirect, assert_http_ok, assert_http_forbidden, assert_http_bad_request, assert_http_accepted
from germanium.tools.http import (
assert_http_redirect, assert_http_ok, assert_http_forbidden, assert_http_bad_request, assert_http_accepted
)

from fperms.models import Perm

Expand Down Expand Up @@ -83,3 +88,21 @@ def test_user_with_permission_should_do_allowed_operations(self):
assert_http_forbidden(self.put('/api/issue/{}/'.format(issue.pk), {}))
assert_http_accepted(self.delete('/api/user/{}/'.format(user.pk)))
assert_http_forbidden(self.delete('/api/issue/{}/'.format(issue.pk)))

@login(is_superuser=False)
@override_settings(IS_CORE_PERM_USE_CACHE=True)
def test_user_permissions_should_be_cached(self):
self.sync_permissions()
self.create_issue()
self.create_user('new_user', 'password', 'test@email.com')
logged_user = self.logged_user.user
assert_http_forbidden(self.get('/api/issue/'))
issue_read_permission = Perm.objects.get(codename='{}__{}'.format('issue', 'read'))

# Add permission but permissions are still cached
logged_user.fperms.add(issue_read_permission)
assert_http_forbidden(self.get('/api/issue/'))

# clear cache reset permissions
cache.clear()
assert_http_ok(self.get('/api/issue/'))
4 changes: 3 additions & 1 deletion example/dj/apps/issue_tracker/tests/ui_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from germanium.decorators import login
from germanium.test_cases.client import ClientTestCase
from germanium.tools.http import assert_http_redirect, assert_http_ok, assert_http_forbidden, assert_http_bad_request, assert_http_accepted
from germanium.tools.http import (
assert_http_redirect, assert_http_ok, assert_http_forbidden, assert_http_bad_request, assert_http_accepted
)

from fperms.models import Perm, Group

Expand Down
7 changes: 7 additions & 0 deletions example/dj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,10 @@ def _(val): return val
'PASSWORD': '',
},
}

CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'default_cache',
}
}
21 changes: 21 additions & 0 deletions fperms_iscore/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.conf import settings as django_settings


DEFAULTS = {
'IS_CORE_PERM_USE_CACHE': False,
'IS_CORE_PERM_CACHE_NAME': 'default',
'IS_CORE_PERM_CACHE_TIMEOUT': 60 * 10, # 10 min
}


class Settings:

def __getattr__(self, attr):
if attr not in DEFAULTS:
raise AttributeError('Invalid fperms_iscore setting: "{}"'.format(attr))

default = DEFAULTS[attr]
return getattr(django_settings, attr, default(self) if callable(default) else default)


settings = Settings()
50 changes: 37 additions & 13 deletions fperms_iscore/permissions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django.core.cache import caches
from django.core.exceptions import ImproperlyConfigured

from fperms import get_perm_model

from fperms_iscore import enums
from fperms_iscore.conf import settings

from is_core.auth.permissions import IsAuthenticated

Expand All @@ -16,25 +18,52 @@
GENERAL_CACHE_NAME = '__all__'


def _get_perm_slug_from_data(perm_type, perm_codename, object_ct=None, object_id=None):
def _get_perm_slug_from_data(perm_type, perm_codename, object_ct_id=None, object_id=None):
slug = '{}|{}'.format(perm_type, perm_codename)
if object_ct and object_id:
return '{}|{}|{}'.format(slug, object_ct.pk, object_id)
if object_ct_id and object_id:
return '{}|{}|{}'.format(slug, object_ct_id, object_id)
return slug


def _get_perm_slug_from_obj(perm_type, perm_codename, obj=None):
from django.contrib.contenttypes.models import ContentType

object_ct = object_id = None
object_ct_id = object_id = None
if obj is not None:
object_ct = ContentType.objects.get_for_model(obj)
object_ct_id = ContentType.objects.get_for_model(obj).pk
object_id = obj.pk
return _get_perm_slug_from_data(perm_type, perm_codename, object_ct, object_id)
return _get_perm_slug_from_data(perm_type, perm_codename, object_ct_id, object_id)


def _get_perm_slug(perm):
return _get_perm_slug_from_data(perm.type, perm.codename, perm.content_type, perm.object_id)
return _get_perm_slug_from_data(perm.type, perm.codename, perm.content_type_id, perm.object_id)


def get_cache():
return caches[settings.IS_CORE_PERM_CACHE_NAME] if settings.IS_CORE_PERM_USE_CACHE else None


def get_all_user_perm_slugs(request):
if hasattr(request.user, '_fperms_is_core_user_perm_slugs'):
return request.user._fperms_is_core_user_perm_slugs

cache_key = None
perm_slugs = None
cache = get_cache()

if cache:
assert hasattr(request, 'session'), 'The cached permissions requres session middleware to be installed, ' \
'and come before the message middleware in the MIDDLEWARE list'
cache_key = f'fperms_is_core-{request.user.pk}-{request.session.session_key}'
perm_slugs = cache.get(cache_key)

if perm_slugs is None:
perm_slugs = set(_get_perm_slug(p) for p in request.user.fperms.all_perms())
if cache:
cache.set(cache_key, perm_slugs, settings.IS_CORE_PERM_CACHE_TIMEOUT)

request.user._fperms_is_core_user_perm_slugs = perm_slugs
return perm_slugs


class BaseFPermPermission(IsAuthenticated):
Expand All @@ -56,14 +85,9 @@ def has_permission(self, name, request, view, obj=None):
if user.is_superuser:
return True

if not hasattr(user, '_fperms_is_core_user_perm_slugs'):
user._fperms_is_core_user_perm_slugs = set(
_get_perm_slug(p) for p in user.fperms.all_perms()
)

permission_name = self._get_name(name, request, view, obj)

user_perm_slugs = user._fperms_is_core_user_perm_slugs
user_perm_slugs = get_all_user_perm_slugs(request)
return (
_get_perm_slug_from_obj(enums.PERM_TYPE_CORE, permission_name) in user_perm_slugs
or (
Expand Down

0 comments on commit 4885ac4

Please sign in to comment.