from functools import wraps
from urllib.parse import urlparse
from django.conf import settings
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.mixins import PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.auth.models import Permission
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, resolve_url
DEFAULT_403 = getattr(settings, 'DJEM_DEFAULT_403', False)
def _get_user_log_verbosity():
return getattr(settings, 'DJEM_PERM_LOG_VERBOSITY', 0)
[docs]class ObjectPermissionsBackend(BaseBackend):
def _get_model_permission(self, perm, user_obj):
verbosity = _get_user_log_verbosity()
if not verbosity:
access = user_obj.has_perm(perm)
else:
access = user_obj.logged_has_perm(perm)
# Add the log from the model-level check to the log for the
# object-level check, replacing the "result" line
log = user_obj.get_last_log(raw=True)
log.pop()
log.append('Model-level Result: {}\n'.format('Granted' if access else 'Denied'))
user_obj.log(*log, tag='auto')
return access
def _get_object_permission(self, perm, user_obj, obj, from_name):
"""
Test if a user has a permission on a specific model object.
``from_name`` can be either "user" or "group", to determine permissions
using the user object itself or the groups it belongs to, respectively.
"""
if not user_obj.is_active: # pragma: no cover
# An inactive user won't normally get this far as they would not
# pass the model-level permissions check
return False
try:
perm_cache = user_obj._olp_cache
except AttributeError:
# OLP cache dictionary will not exist by default if not using
# OLPMixin and no permissions have yet been checked on this user
# object
perm_cache = user_obj._olp_cache = {}
perm_cache_name = '{0}-{1}-{2}'.format(from_name, perm, obj.pk)
if perm_cache_name not in perm_cache:
access_fn_name = '_{0}_can_{1}'.format(
from_name,
perm.split('.')[-1]
)
access_fn = getattr(obj, access_fn_name, None)
if not access_fn:
# No function defined on obj to determine access - assume
# access should be granted if no explicit object-level logic
# exists to determine otherwise
access = None
else:
try:
if from_name == 'user':
access = access_fn(user_obj)
else:
access = access_fn(user_obj.groups.all())
except PermissionDenied:
access = False
perm_cache[perm_cache_name] = access
return perm_cache[perm_cache_name]
def _get_object_permissions(self, user_obj, obj, from_name=None):
"""
Return a set of the permissions a user has on a specific model object.
``from_name`` can be either "user" or "group", to determine permissions
using the user object itself or the groups it belongs to, respectively.
It can also be None to determine the users permissions from both sources.
"""
if not obj or not user_obj.is_active or user_obj.is_anonymous:
return set()
perms_for_model = Permission.objects.filter(
content_type__app_label=obj._meta.app_label,
content_type__model=obj._meta.model_name,
).values_list('content_type__app_label', 'codename')
perms_for_model = ['{0}.{1}'.format(app, name) for app, name in perms_for_model]
if user_obj.is_superuser and not getattr(settings, 'DJEM_UNIVERSAL_OLP', False):
# Superusers get all permissions, regardless of obj or from_name,
# unless using "universal" OLP, in which case they are subject to
# the same OLP logic as regular users
perms = set(perms_for_model)
else:
# If using any level of automated logging for permissions, create a
# temporary log to act as the target for any log entries (either
# automatic entries appended by this backend or any manual entries
# that may be present in object-level access methods). The usual
# automatic log started as part of OLPMixin.has_perm() will not
# have been created, so this acts as a replacement.
log_verbosity = _get_user_log_verbosity()
if log_verbosity:
user_obj.start_log('temp-{0}'.format(obj.pk))
perms = set()
for perm in perms_for_model:
if not self._get_model_permission(perm, user_obj):
continue
user_access = None
group_access = None
# Check user first, unless only checking for group
if from_name != 'group':
user_access = self._get_object_permission(perm, user_obj, obj, 'user')
# Check group if user didn't grant the permission, unless only
# checking for user
if not user_access and from_name != 'user':
group_access = self._get_object_permission(perm, user_obj, obj, 'group')
# The permission is granted if either of the user or group
# checks grant it, or if neither of them have a defined
# object-level access method
if user_access or group_access or (user_access is None and group_access is None):
perms.add(perm)
# Remove the temporary log, if one was created
if log_verbosity:
user_obj.discard_log()
return perms
def get_user_permissions(self, user_obj, obj=None):
return self._get_object_permissions(user_obj, obj, 'user')
def get_group_permissions(self, user_obj, obj=None):
return self._get_object_permissions(user_obj, obj, 'group')
def get_all_permissions(self, user_obj, obj=None):
return self._get_object_permissions(user_obj, obj)
def has_perm(self, user_obj, perm, obj=None):
if not obj:
return False # not dealing with non-object permissions
if not self._get_model_permission(perm, user_obj):
return False
user_access = self._get_object_permission(perm, user_obj, obj, 'user')
group_access = None
# Check group if user didn't grant the permission
if not user_access:
group_access = self._get_object_permission(perm, user_obj, obj, 'group')
# The permission is granted if either of the user or group
# checks grant it, or if neither of them have a defined
# object-level access method
return user_access or group_access or (user_access is None and group_access is None)
def _check_perms(perms, user, view_kwargs):
for perm in perms:
if isinstance(perm, str):
obj = None
else:
perm, obj_arg = perm # expand two-tuple
obj_pk = view_kwargs[obj_arg]
# Get the model this permission belongs to
try:
perm_app, perm_code = perm.split('.')
perm_obj = Permission.objects.get(
content_type__app_label=perm_app,
codename=perm_code
)
except (ValueError, Permission.DoesNotExist):
# Treat malformed (missing a '.') or non-existent
# permission names as permission denied
raise PermissionDenied
model = perm_obj.content_type.model_class()
# Get the object instance using the inferred model and the
# primary key passed to the view
obj = get_object_or_404(model, pk=obj_pk)
# Swap out the primary key with the instance itself in the view
# kwargs, so the view doesn't have to query for it again
view_kwargs[obj_arg] = obj
if not user.has_perm(perm, obj):
raise PermissionDenied
[docs]def permission_required(*perms, **kwargs):
"""
Replacement for Django's ``permission_required`` decorator, providing
support for object-level permissions. Instead of accepting either a string
or an iterable of strings naming the permission/s to check, this version
accepts multiple positional arguments, one for each permission to check.
These arguments can be either strings or two-tuples. If two-tuples, the
items should be:
- a string naming the permission to check (in the <app label>.<permission code>
format)
- a string naming the keyword argument of the view that contains the
primary key of the object to check the permission against
Behaviour of the ``login_url`` and ``raise_exception`` keyword arguments is
as per the original, except that the default value for ``raise_exception``
can be specified with the ``DJEM_DEFAULT_403`` setting.
"""
login_url = kwargs.pop('login_url', None)
raise_exception = kwargs.pop('raise_exception', DEFAULT_403)
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
# First, check if the user has the permission (even anon users)
try:
_check_perms(perms, request.user, kwargs)
except PermissionDenied:
# In case the 403 handler should be called, raise the exception
if raise_exception:
raise
else:
return view_func(request, *args, **kwargs)
# As the last resort, show the login form
path = request.build_absolute_uri()
resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
# If the login url is the same scheme and net location then just
# use the path as the "next" url.
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
current_scheme, current_netloc = urlparse(path)[:2]
if ((not login_scheme or login_scheme == current_scheme)
and (not login_netloc or login_netloc == current_netloc)):
path = request.get_full_path()
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(path, resolved_login_url)
return _wrapped_view
return decorator
[docs]class PermissionRequiredMixin(DjangoPermissionRequiredMixin):
"""
CBV mixin which verifies that the current user has all specified
permissions, on the specified object where applicable.
"""
raise_exception = DEFAULT_403
def has_permission(self, view_kwargs):
perms = self.get_permission_required()
try:
_check_perms(perms, self.request.user, view_kwargs)
except PermissionDenied:
return False
else:
return True
# Overridden to pass kwargs to has_permission() and skip the immediate
# parent's dispatch() when calling the super method (because it attempts
# to call has_permission without the kwargs).
def dispatch(self, request, *args, **kwargs):
if not self.has_permission(kwargs):
return self.handle_no_permission()
# Skip DjangoPermissionRequiredMixin.dispatch() and call *its* parent directly
return super(DjangoPermissionRequiredMixin, self).dispatch(request, *args, **kwargs)