From dc2c9aa662e9a4af344ed60da0db84045da1e3a0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 6 Oct 2020 11:29:38 +1100 Subject: [PATCH] Add InvenTreeRoleMixin - Simplifies permission requirements for views - e.g. 'part.view' rather than 'part.view_partcategory' --- InvenTree/InvenTree/views.py | 54 ++++++++++++++++++++++++++++++++++++ InvenTree/part/api.py | 4 +++ InvenTree/part/views.py | 25 +++++++++-------- InvenTree/stock/api.py | 4 +++ InvenTree/users/models.py | 32 +++++++++++++++++++++ 5 files changed, 108 insertions(+), 11 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index d940229ebe..9cd4aeb514 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -22,6 +22,7 @@ from django.views.generic.base import TemplateView from part.models import Part, PartCategory from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme +from users.models import check_user_role, RuleSet from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm from .helpers import str2bool @@ -682,3 +683,56 @@ class DatabaseStatsView(AjaxView): """ return ctx + + +class InvenTreeRoleMixin(PermissionRequiredMixin): + """ + Permission class based on user roles, not user 'permissions'. + + To specify which role is required for the mixin, + set the class attribute 'role_required' to something like the following: + + role_required = 'part.add' + role_required = [ + 'part.change', + 'build.add', + ] + """ + + # By default, no roles are required + # Roles must be specified + role_required = None + + def has_permission(self): + """ + Determine if the current user + """ + + roles_required = [] + + if type(self.role_required) is str: + roles_required.append(self.role_required) + elif type(self.role_required) in [list, tuple]: + roles_required = self.role_required + + # List of permissions that will be required + permissions = [] + + user = self.request.user + + # Superuser can have any permissions they desire + if user.is_superuser: + return True + + print(type(self), "Required roles:", roles_required) + + for required in roles_required: + + (role, permission) = required.split('.') + + # Return False if the user does not have *any* of the required roles + if not check_user_role(user, role, permission): + return False + + # We did not fail any required checks + return True diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index d4aeec5bd9..d643b8671a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -44,6 +44,10 @@ class PartCategoryTree(TreeSerializer): def get_items(self): return PartCategory.objects.all().prefetch_related('parts', 'children') + permission_classes = [ + permissions.IsAuthenticated, + ] + class CategoryList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartCategory objects. diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 8e0980c35e..e352cddb08 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -12,7 +12,6 @@ from django.shortcuts import HttpResponseRedirect from django.utils.translation import gettext_lazy as _ from django.urls import reverse, reverse_lazy from django.views.generic import DetailView, ListView, FormView, UpdateView -from django.contrib.auth.mixins import PermissionRequiredMixin from django.forms.models import model_to_dict from django.forms import HiddenInput, CheckboxInput from django.conf import settings @@ -39,17 +38,21 @@ from .admin import PartResource from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView +from InvenTree.views import InvenTreeRoleMixin from InvenTree.helpers import DownloadFile, str2bool -class PartIndex(PermissionRequiredMixin, ListView): +class PartIndex(InvenTreeRoleMixin, ListView): """ View for displaying list of Part objects """ + model = Part template_name = 'part/category.html' context_object_name = 'parts' - permission_required = ('part.view_part', 'part.view_partcategory') + + role_required = 'part.view' + def get_queryset(self): return Part.objects.all().select_related('category') @@ -658,7 +661,7 @@ class PartNotes(UpdateView): return ctx -class PartDetail(PermissionRequiredMixin, DetailView): +class PartDetail(InvenTreeRoleMixin, DetailView): """ Detail view for Part object """ @@ -666,7 +669,7 @@ class PartDetail(PermissionRequiredMixin, DetailView): queryset = Part.objects.all().select_related('category') template_name = 'part/detail.html' - permission_required = 'part.view_part' + role_required = 'part.view' # Add in some extra context information based on query params def get_context_data(self, **kwargs): @@ -869,7 +872,7 @@ class BomValidate(AjaxUpdateView): return self.renderJsonResponse(request, form, data, context=self.get_context()) -class BomUpload(PermissionRequiredMixin, FormView): +class BomUpload(InvenTreeRoleMixin, FormView): """ View for uploading a BOM file, and handling BOM data importing. The BOM upload process is as follows: @@ -905,7 +908,7 @@ class BomUpload(PermissionRequiredMixin, FormView): missing_columns = [] allowed_parts = [] - permission_required = ('part.change_part', 'part.add_bomitem') + role_required = ('part.change', 'part.add') def get_success_url(self): part = self.get_object() @@ -1931,7 +1934,7 @@ class PartParameterDelete(AjaxDeleteView): ajax_form_title = _('Delete Part Parameter') -class CategoryDetail(PermissionRequiredMixin, DetailView): +class CategoryDetail(InvenTreeRoleMixin, DetailView): """ Detail view for PartCategory """ model = PartCategory @@ -1939,7 +1942,7 @@ class CategoryDetail(PermissionRequiredMixin, DetailView): queryset = PartCategory.objects.all().prefetch_related('children') template_name = 'part/category_partlist.html' - permission_required = 'part.view_partcategory' + role_required = 'part.view' def get_context_data(self, **kwargs): @@ -2081,13 +2084,13 @@ class CategoryCreate(AjaxCreateView): return initials -class BomItemDetail(PermissionRequiredMixin, DetailView): +class BomItemDetail(InvenTreeRoleMixin, DetailView): """ Detail view for BomItem """ context_object_name = 'item' queryset = BomItem.objects.all() template_name = 'part/bom-detail.html' - permission_required = 'part.view_bomitem' + role_required = 'part.view' class BomItemCreate(AjaxCreateView): diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 790de7d879..ba802b75d9 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -52,6 +52,10 @@ class StockCategoryTree(TreeSerializer): def get_items(self): return StockLocation.objects.all().prefetch_related('stock_items', 'children') + permission_classes = [ + permissions.IsAuthenticated, + ] + class StockDetail(generics.RetrieveUpdateDestroyAPIView): """ API detail endpoint for Stock object diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 3bd976f0a4..24b0318695 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -329,3 +329,35 @@ def create_missing_rule_sets(sender, instance, **kwargs): """ update_group_roles(instance) + + +def check_user_role(user, role, permission): + """ + Check if a user has a particular role:permission combination. + + If the user is a superuser, this will return True + """ + + if user.is_superuser: + return True + + for group in user.groups.all(): + + for rule in group.rule_sets.all(): + + if rule.name == role: + + if permission == 'add' and rule.can_add: + return True + + if permission == 'change' and rule.can_change: + return True + + if permission == 'view' and rule.can_view: + return True + + if permission == 'delete' and rule.can_delete: + return True + + # No matching permissions found + return False