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,16 +30,71 @@ def get_allowed_groups(groups_required):
return groups_allowed
def group_required(*groups_required):
"""Requires user membership in at least one of the groups passed in."""
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)):
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):
"""
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):
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:
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):