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 import messages
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
@ -13,7 +13,15 @@ from rest_framework import permissions
from cookbook.models import ShareLink from cookbook.models import ShareLink
# Helper Functions
def get_allowed_groups(groups_required): 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) groups_allowed = tuple(groups_required)
if 'guest' in groups_required: if 'guest' in groups_required:
groups_allowed = groups_allowed + ('user', 'admin') groups_allowed = groups_allowed + ('user', 'admin')
@ -22,15 +30,70 @@ def get_allowed_groups(groups_required):
return groups_allowed 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): 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): def in_groups(u):
groups_allowed = get_allowed_groups(groups_required) return has_group_permission(u, groups_required)
if u.is_authenticated:
if u.is_superuser | bool(u.groups.filter(name__in=groups_allowed)):
return True
return False
return user_passes_test(in_groups, login_url='index') return user_passes_test(in_groups, login_url='index')
@ -43,18 +106,10 @@ class GroupRequiredMixin(object):
groups_required = None groups_required = None
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated: if not has_group_permission(request.user, self.groups_required):
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!')) messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('login')) return HttpResponseRedirect(reverse_lazy('index'))
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'))
return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs) 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!')) messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('login')) return HttpResponseRedirect(reverse_lazy('login'))
else: else:
obj = self.get_object() if not is_object_owner(request.user, self.get_object()):
if not (obj.created_by == request.user or request.user.is_superuser):
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as its not owned by you!')) messages.add_message(request, messages.ERROR, _('You cannot interact with this object as its not owned by you!'))
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('index'))
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs) return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
def share_link_valid(recipe, share): # Django Rest Framework Permission classes
"""
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
class CustomIsOwner(permissions.BasePermission):
class DRFOwnerPermissions(permissions.BasePermission):
""" """
Custom permission class for django rest framework views Custom permission class for django rest framework views
verifies user has ownership over object verifies user has ownership over object
(either user or created_by or user is request user) (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): def has_object_permission(self, request, view, obj):
if not request.user.is_authenticated: return is_object_owner(request.user, obj)
return False
#if request.user.is_superuser:
# return True class CustomIsGuest(permissions.BasePermission):
if owner := getattr(obj, 'created_by', None): """
return owner == request.user Custom permission class for django rest framework views
if owner := getattr(obj, 'user', None): verifies the user is member of at least the group: guest
return owner == request.user """
return False 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.exceptions import APIException
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin, ListModelMixin 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.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -47,12 +47,9 @@ class UserNameViewSet(viewsets.ModelViewSet):
class UserPreferenceViewSet(viewsets.ModelViewSet): class UserPreferenceViewSet(viewsets.ModelViewSet):
"""
Update user preference settings
"""
queryset = UserPreference.objects.all() queryset = UserPreference.objects.all()
serializer_class = UserPreferenceSerializer serializer_class = UserPreferenceSerializer
permission_classes = [DRFOwnerPermissions, ] permission_classes = [CustomIsOwner, ]
def perform_create(self, serializer): def perform_create(self, serializer):
if UserPreference.objects.filter(user=self.request.user).exists(): if UserPreference.objects.filter(user=self.request.user).exists():
@ -60,23 +57,20 @@ class UserPreferenceViewSet(viewsets.ModelViewSet):
serializer.save(user=self.request.user) serializer.save(user=self.request.user)
def get_queryset(self): def get_queryset(self):
# if self.request.user.is_superuser: if self.request.user.is_superuser:
# return UserPreference.objects.all() return self.queryset
return UserPreference.objects.filter(user=self.request.user).all() return self.queryset.filter(user=self.request.user)
class RecipeBookViewSet(RetrieveModelMixin, UpdateModelMixin, ListModelMixin, viewsets.GenericViewSet): class RecipeBookViewSet(RetrieveModelMixin, UpdateModelMixin, ListModelMixin, viewsets.GenericViewSet):
"""
Update user preference settings
"""
queryset = RecipeBook.objects.all() queryset = RecipeBook.objects.all()
serializer_class = RecipeBookSerializer serializer_class = RecipeBookSerializer
permission_classes = [DRFOwnerPermissions, ] permission_classes = [CustomIsOwner, CustomIsAdmin]
def get_queryset(self): def get_queryset(self):
if self.request.user.is_superuser: if self.request.user.is_superuser:
return RecipeBook.objects.all() return self.queryset
return RecipeBook.objects.filter(created_by=self.request.user).all() return self.queryset.filter(created_by=self.request.user)
class MealPlanViewSet(viewsets.ModelViewSet): class MealPlanViewSet(viewsets.ModelViewSet):