Add InvenTreeRoleMixin

- Simplifies permission requirements for views
- e.g. 'part.view' rather than 'part.view_partcategory'
This commit is contained in:
Oliver Walters 2020-10-06 11:29:38 +11:00
parent d2e2e7511f
commit dc2c9aa662
5 changed files with 108 additions and 11 deletions

View File

@ -22,6 +22,7 @@ from django.views.generic.base import TemplateView
from part.models import Part, PartCategory from part.models import Part, PartCategory
from stock.models import StockLocation, StockItem from stock.models import StockLocation, StockItem
from common.models import InvenTreeSetting, ColorTheme from common.models import InvenTreeSetting, ColorTheme
from users.models import check_user_role, RuleSet
from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm
from .helpers import str2bool from .helpers import str2bool
@ -682,3 +683,56 @@ class DatabaseStatsView(AjaxView):
""" """
return ctx 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

View File

@ -44,6 +44,10 @@ class PartCategoryTree(TreeSerializer):
def get_items(self): def get_items(self):
return PartCategory.objects.all().prefetch_related('parts', 'children') return PartCategory.objects.all().prefetch_related('parts', 'children')
permission_classes = [
permissions.IsAuthenticated,
]
class CategoryList(generics.ListCreateAPIView): class CategoryList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartCategory objects. """ API endpoint for accessing a list of PartCategory objects.

View File

@ -12,7 +12,6 @@ from django.shortcuts import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.views.generic import DetailView, ListView, FormView, UpdateView 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.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput from django.forms import HiddenInput, CheckboxInput
from django.conf import settings 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 AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView from InvenTree.views import QRCodeView
from InvenTree.views import InvenTreeRoleMixin
from InvenTree.helpers import DownloadFile, str2bool from InvenTree.helpers import DownloadFile, str2bool
class PartIndex(PermissionRequiredMixin, ListView): class PartIndex(InvenTreeRoleMixin, ListView):
""" View for displaying list of Part objects """ View for displaying list of Part objects
""" """
model = Part model = Part
template_name = 'part/category.html' template_name = 'part/category.html'
context_object_name = 'parts' context_object_name = 'parts'
permission_required = ('part.view_part', 'part.view_partcategory')
role_required = 'part.view'
def get_queryset(self): def get_queryset(self):
return Part.objects.all().select_related('category') return Part.objects.all().select_related('category')
@ -658,7 +661,7 @@ class PartNotes(UpdateView):
return ctx return ctx
class PartDetail(PermissionRequiredMixin, DetailView): class PartDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for Part object """ Detail view for Part object
""" """
@ -666,7 +669,7 @@ class PartDetail(PermissionRequiredMixin, DetailView):
queryset = Part.objects.all().select_related('category') queryset = Part.objects.all().select_related('category')
template_name = 'part/detail.html' template_name = 'part/detail.html'
permission_required = 'part.view_part' role_required = 'part.view'
# Add in some extra context information based on query params # Add in some extra context information based on query params
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -869,7 +872,7 @@ class BomValidate(AjaxUpdateView):
return self.renderJsonResponse(request, form, data, context=self.get_context()) 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. """ View for uploading a BOM file, and handling BOM data importing.
The BOM upload process is as follows: The BOM upload process is as follows:
@ -905,7 +908,7 @@ class BomUpload(PermissionRequiredMixin, FormView):
missing_columns = [] missing_columns = []
allowed_parts = [] allowed_parts = []
permission_required = ('part.change_part', 'part.add_bomitem') role_required = ('part.change', 'part.add')
def get_success_url(self): def get_success_url(self):
part = self.get_object() part = self.get_object()
@ -1931,7 +1934,7 @@ class PartParameterDelete(AjaxDeleteView):
ajax_form_title = _('Delete Part Parameter') ajax_form_title = _('Delete Part Parameter')
class CategoryDetail(PermissionRequiredMixin, DetailView): class CategoryDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for PartCategory """ """ Detail view for PartCategory """
model = PartCategory model = PartCategory
@ -1939,7 +1942,7 @@ class CategoryDetail(PermissionRequiredMixin, DetailView):
queryset = PartCategory.objects.all().prefetch_related('children') queryset = PartCategory.objects.all().prefetch_related('children')
template_name = 'part/category_partlist.html' template_name = 'part/category_partlist.html'
permission_required = 'part.view_partcategory' role_required = 'part.view'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -2081,13 +2084,13 @@ class CategoryCreate(AjaxCreateView):
return initials return initials
class BomItemDetail(PermissionRequiredMixin, DetailView): class BomItemDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for BomItem """ """ Detail view for BomItem """
context_object_name = 'item' context_object_name = 'item'
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
template_name = 'part/bom-detail.html' template_name = 'part/bom-detail.html'
permission_required = 'part.view_bomitem' role_required = 'part.view'
class BomItemCreate(AjaxCreateView): class BomItemCreate(AjaxCreateView):

View File

@ -52,6 +52,10 @@ class StockCategoryTree(TreeSerializer):
def get_items(self): def get_items(self):
return StockLocation.objects.all().prefetch_related('stock_items', 'children') return StockLocation.objects.all().prefetch_related('stock_items', 'children')
permission_classes = [
permissions.IsAuthenticated,
]
class StockDetail(generics.RetrieveUpdateDestroyAPIView): class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" API detail endpoint for Stock object """ API detail endpoint for Stock object

View File

@ -329,3 +329,35 @@ def create_missing_rule_sets(sender, instance, **kwargs):
""" """
update_group_roles(instance) 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