improved permission handlin

This commit is contained in:
vabene1111 2020-06-17 13:18:28 +02:00
parent 2904d5938d
commit df8170fa55
2 changed files with 120 additions and 54 deletions

View File

@ -4,7 +4,7 @@ Source: https://djangosnippets.org/snippets/1703/
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import gettext as _
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy, reverse
@ -13,7 +13,15 @@ from rest_framework import permissions
from cookbook.models import ShareLink
# Helper Functions
def get_allowed_groups(groups_required):
"""
Builds a list of all groups equal or higher to the provided groups
This means checking for guest will also allow admins to access
:param groups_required: list or tuple of groups
:return: tuple of groups
"""
groups_allowed = tuple(groups_required)
if 'guest' in groups_required:
groups_allowed = groups_allowed + ('user', 'admin')
@ -22,15 +30,70 @@ def get_allowed_groups(groups_required):
return groups_allowed
def has_group_permission(user, groups):
"""
Tests if a given user is member of a certain group (or any higher group)
Superusers always bypass permission checks. Unauthenticated users cant be member of any
group thus always return false.
:param user: django auth user object
:param groups: list or tuple of groups the user should be checked for
:return: True if user is in allowed groups, false otherwise
"""
if not user.is_authenticated:
return False
groups_allowed = get_allowed_groups(groups)
if user.is_authenticated:
if user.is_superuser | bool(user.groups.filter(name__in=groups_allowed)):
return True
return False
def is_object_owner(user, obj):
"""
Tests if a given user is the owner of a given object
test performed by checking user against the objects user and create_by field (if exists)
superusers bypass all checks, unauthenticated users cannot own anything
:param user django auth user object
:param obj any object that should be tested
:return: true if user is owner of object, false otherwise
"""
# TODO this could be improved/cleaned up by adding get_owner methods to all models that allow owner checks
if not user.is_authenticated:
return False
if user.is_superuser:
return True
if owner := getattr(obj, 'created_by', None):
return owner == user
if owner := getattr(obj, 'user', None):
return owner == user
return False
def share_link_valid(recipe, share):
"""
Verifies the validity of a share uuid
:param recipe: recipe object
:param share: share uuid
:return: true if a share link with the given recipe and uuid exists, false otherwise
"""
try:
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
except ValidationError:
return False
# Django Views
def group_required(*groups_required):
"""Requires user membership in at least one of the groups passed in."""
"""
Decorator that tests the requesting user to be member of at least one of the provided groups
or higher level groups
:param groups_required: list of required groups
:return: true if member of group, false otherwise
"""
def in_groups(u):
groups_allowed = get_allowed_groups(groups_required)
if u.is_authenticated:
if u.is_superuser | bool(u.groups.filter(name__in=groups_allowed)):
return True
return False
return has_group_permission(u, groups_required)
return user_passes_test(in_groups, login_url='index')
@ -43,18 +106,10 @@ class GroupRequiredMixin(object):
groups_required = None
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('login'))
else:
if not request.user.is_superuser:
group_allowed = get_allowed_groups(self.groups_required)
user_groups = []
for group in request.user.groups.values_list('name', flat=True):
user_groups.append(group)
if len(set(user_groups).intersection(group_allowed)) <= 0:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
if not has_group_permission(request.user, self.groups_required):
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs)
@ -65,38 +120,55 @@ class OwnerRequiredMixin(object):
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('login'))
else:
obj = self.get_object()
if not (obj.created_by == request.user or request.user.is_superuser):
if not is_object_owner(request.user, self.get_object()):
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as its not owned by you!'))
return HttpResponseRedirect(reverse('index'))
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
def share_link_valid(recipe, share):
"""
Verifies if a share uuid is valid for a given recipe
"""
try:
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
except ValidationError:
return False
# Django Rest Framework Permission classes
class DRFOwnerPermissions(permissions.BasePermission):
class CustomIsOwner(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies user has ownership over object
(either user or created_by or user is request user)
"""
message = _('You cannot interact with this object as its not owned by you!')
def has_object_permission(self, request, view, obj):
if not request.user.is_authenticated:
return False
#if request.user.is_superuser:
# return True
if owner := getattr(obj, 'created_by', None):
return owner == request.user
if owner := getattr(obj, 'user', None):
return owner == request.user
return False
return is_object_owner(request.user, obj)
class CustomIsGuest(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the user is member of at least the group: guest
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
has_group_permission(request.user, ['guest'])
class CustomIsUser(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the user is member of at least the group: user
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
has_group_permission(request.user, ['guest'])
class CustomIsAdmin(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the user is member of at least the group: admin
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
has_group_permission(request.user, ['guest'])

View File

@ -15,7 +15,7 @@ from rest_framework import viewsets, permissions
from rest_framework.exceptions import APIException
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin, ListModelMixin
from cookbook.helper.permission_helper import group_required, DRFOwnerPermissions
from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin
from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.nextcloud import Nextcloud
@ -47,12 +47,9 @@ class UserNameViewSet(viewsets.ModelViewSet):
class UserPreferenceViewSet(viewsets.ModelViewSet):
"""
Update user preference settings
"""
queryset = UserPreference.objects.all()
serializer_class = UserPreferenceSerializer
permission_classes = [DRFOwnerPermissions, ]
permission_classes = [CustomIsOwner, ]
def perform_create(self, serializer):
if UserPreference.objects.filter(user=self.request.user).exists():
@ -60,23 +57,20 @@ class UserPreferenceViewSet(viewsets.ModelViewSet):
serializer.save(user=self.request.user)
def get_queryset(self):
# if self.request.user.is_superuser:
# return UserPreference.objects.all()
return UserPreference.objects.filter(user=self.request.user).all()
if self.request.user.is_superuser:
return self.queryset
return self.queryset.filter(user=self.request.user)
class RecipeBookViewSet(RetrieveModelMixin, UpdateModelMixin, ListModelMixin, viewsets.GenericViewSet):
"""
Update user preference settings
"""
queryset = RecipeBook.objects.all()
serializer_class = RecipeBookSerializer
permission_classes = [DRFOwnerPermissions, ]
permission_classes = [CustomIsOwner, CustomIsAdmin]
def get_queryset(self):
if self.request.user.is_superuser:
return RecipeBook.objects.all()
return RecipeBook.objects.filter(created_by=self.request.user).all()
return self.queryset
return self.queryset.filter(created_by=self.request.user)
class MealPlanViewSet(viewsets.ModelViewSet):