From 6a1634ef1212ecc84892d85943d27e6a44e6f5a2 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 27 May 2019 15:39:06 +0200 Subject: [PATCH 3/8] django_rbac: add auth_level arg to permission methods (#33515) Internally we bypass _user_has_perm and similar internal methods fixed signatures with an annotation on the user object. --- src/django_rbac/backends.py | 13 +++++++---- src/django_rbac/exceptions.py | 2 ++ src/django_rbac/managers.py | 4 ++-- src/django_rbac/models.py | 43 +++++++++++++++++++++++++++++++++-- 4 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 src/django_rbac/exceptions.py diff --git a/src/django_rbac/backends.py b/src/django_rbac/backends.py index 36d4f147..df37525e 100644 --- a/src/django_rbac/backends.py +++ b/src/django_rbac/backends.py @@ -52,10 +52,13 @@ class DjangoRBACBackend(object): - `''`: contains a boolean, it indicates that the user own at least on permision on a model of this application. ''' - if not hasattr(user_obj, '_rbac_perms_cache'): - perms_cache = {} + auth_level = getattr(user_obj, '_auth_level', None) + cache_suffix = '_auth_level' if auth_level else '' + perms_cache = getattr(user_obj, '_rbac_perms_cache' + cache_suffix, {}) + + if not perms_cache: Permission = utils.get_permission_model() - qs = Permission.objects.for_user(user_obj) + qs = Permission.objects.for_user(user_obj, max_auth_level=auth_level) ct_ct = ContentType.objects.get_for_model(ContentType) qs = qs.select_related('operation') for permission in qs: @@ -83,8 +86,8 @@ class DjangoRBACBackend(object): permissions.update(perms) # optimization for has_module_perms perms_cache[app_label] = True - user_obj._rbac_perms_cache = perms_cache - return user_obj._rbac_perms_cache + setattr(user_obj, '_rbac_perms_cache' + cache_suffix, perms_cache) + return perms_cache def get_all_permissions(self, user_obj, obj=None): if user_obj.is_anonymous(): diff --git a/src/django_rbac/exceptions.py b/src/django_rbac/exceptions.py new file mode 100644 index 00000000..493ba899 --- /dev/null +++ b/src/django_rbac/exceptions.py @@ -0,0 +1,2 @@ +class InsufficientAuthLevel(Exception): + pass diff --git a/src/django_rbac/managers.py b/src/django_rbac/managers.py index 95225449..0378b2e4 100644 --- a/src/django_rbac/managers.py +++ b/src/django_rbac/managers.py @@ -78,12 +78,12 @@ class PermissionQueryset(query.QuerySet): '''Filter permission whose target matches target''' return self.by_target_ct(target).filter(target_id=target.pk) - def for_user(self, user): + def for_user(self, user, max_auth_level=None): '''Retrieve all permissions hold by an user through its role and inherited roles. ''' Role = utils.get_role_model() - roles = Role.objects.for_user(user=user) + roles = Role.objects.for_user(user=user, max_auth_level=max_auth_level) return self.filter(roles__in=roles) def cleanup(self): diff --git a/src/django_rbac/models.py b/src/django_rbac/models.py index cbe8ce01..d53230da 100644 --- a/src/django_rbac/models.py +++ b/src/django_rbac/models.py @@ -20,6 +20,7 @@ from django.contrib import auth from django.core.validators import MinValueValidator from . import utils, constants, managers, backends +from .exceptions import InsufficientAuthLevel @six.python_2_unicode_compatible @@ -330,6 +331,39 @@ class PermissionMixin(models.Model): def get_all_permissions(self, obj=None): return _user_get_all_permissions(self, obj) + def _check_auth_level(perm_func): + """Add authentication level check to a permission control function. + + perm_func can be passed a new keyword argument 'auth_level'. If + present, expect perm_func to be ran two times, once with the user + object annotated with an '_auth_level' attribute, and once without. If + the return values do not match, ie some permissions are not granted + when the user authentication level is taken into account, the decorator + will take care of raising an InsufficientAuthLevel exception. + """ + def wrapped_perm_func(self, *args, **kwargs): + auth_level = kwargs.pop('auth_level', None) + auth_level_result = None + + if auth_level: + self._auth_level = auth_level + auth_level_result = perm_func(self, *args, **kwargs) + self._auth_level = None + if auth_level_result is True: + # Performance trick: if the function returns True, assume + # that we can return right away. + return True + + new_result = perm_func(self, *args, **kwargs) + if auth_level and auth_level_result != new_result: + # Let the application know that permission could be granted + # with higher authentication level. + raise InsufficientAuthLevel + + return new_result + return wrapped_perm_func + + @_check_auth_level def has_perm(self, perm, obj=None): """ Returns True if the user has the specified permission. This method @@ -346,7 +380,7 @@ class PermissionMixin(models.Model): # Otherwise we need to check the backends. return _user_has_perm(self, perm, obj) - def has_perms(self, perm_list, obj=None): + def has_perms(self, perm_list, obj=None, auth_level=None): """ Returns True if the user has each of the specified permissions. If object is passed, it checks if the user has all required perms for this @@ -357,10 +391,11 @@ class PermissionMixin(models.Model): return True for perm in perm_list: - if not self.has_perm(perm, obj): + if not self.has_perm(perm, obj, auth_level=auth_level): return False return True + @_check_auth_level def has_module_perms(self, app_label): """ Returns True if the user has any permissions in the given app label. @@ -372,6 +407,7 @@ class PermissionMixin(models.Model): return _user_has_module_perms(self, app_label) + @_check_auth_level def filter_by_perm(self, perm_or_perms, qs): results = [] for backend in auth.get_backends(): @@ -382,6 +418,7 @@ class PermissionMixin(models.Model): else: return qs + @_check_auth_level def has_perm_any(self, perm_or_perms): # Active superusers have all permissions. if self.is_active and self.is_superuser: @@ -393,6 +430,7 @@ class PermissionMixin(models.Model): return True return False + @_check_auth_level def has_ou_perm(self, perm, ou): # Active superusers have all permissions. if self.is_active and self.is_superuser: @@ -404,6 +442,7 @@ class PermissionMixin(models.Model): return True return False + @_check_auth_level def ous_with_perm(self, perm, queryset=None): return backends.DjangoRBACBackend().ous_with_perm(self, perm, queryset=queryset) -- 2.20.1