mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 20:45:44 +00:00
[PUI] Add permissions to groups (#7621)
* Add permissions to group API * factor out permission formatting * add group permission details to UI * add nicer accordions with permissions * add group to models * Add Admin button to change permissions * add missing instance renderer * turn off default view permission to everything * add migration * fix rule assigment * Add now missing view permissions * Adjust test for the now new default permission count * add missing view permission * fix permissions for search test * adjust search testing to also account for missing permissions * adjust to new defaults * expand role testing --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
@ -62,6 +62,7 @@ class APITests(InvenTreeAPITestCase):
|
||||
"""Tests for the InvenTree API."""
|
||||
|
||||
fixtures = ['location', 'category', 'part', 'stock']
|
||||
roles = ['part.view']
|
||||
token = None
|
||||
auto_login = False
|
||||
|
||||
@ -132,6 +133,7 @@ class APITests(InvenTreeAPITestCase):
|
||||
|
||||
# Now log in!
|
||||
self.basicAuth()
|
||||
self.assignRole('part.view')
|
||||
|
||||
response = self.get(url)
|
||||
|
||||
@ -147,12 +149,17 @@ class APITests(InvenTreeAPITestCase):
|
||||
|
||||
role_names = roles.keys()
|
||||
|
||||
# By default, 'view' permissions are provided
|
||||
# By default, no permissions are provided
|
||||
for rule in RuleSet.RULESET_NAMES:
|
||||
self.assertIn(rule, role_names)
|
||||
|
||||
self.assertIn('view', roles[rule])
|
||||
if roles[rule] is None:
|
||||
continue
|
||||
|
||||
if rule == 'part':
|
||||
self.assertIn('view', roles[rule])
|
||||
else:
|
||||
self.assertNotIn('view', roles[rule])
|
||||
self.assertNotIn('add', roles[rule])
|
||||
self.assertNotIn('change', roles[rule])
|
||||
self.assertNotIn('delete', roles[rule])
|
||||
@ -297,6 +304,7 @@ class SearchTests(InvenTreeAPITestCase):
|
||||
'order',
|
||||
'sales_order',
|
||||
]
|
||||
roles = ['build.view', 'part.view']
|
||||
|
||||
def test_empty(self):
|
||||
"""Test empty request."""
|
||||
@ -331,6 +339,19 @@ class SearchTests(InvenTreeAPITestCase):
|
||||
{'search': '01', 'limit': 2, 'purchaseorder': {}, 'salesorder': {}},
|
||||
expected_code=200,
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data['purchaseorder'],
|
||||
{'error': 'User does not have permission to view this model'},
|
||||
)
|
||||
|
||||
# Add permissions and try again
|
||||
self.assignRole('purchase_order.view')
|
||||
self.assignRole('sales_order.view')
|
||||
response = self.post(
|
||||
reverse('api-search'),
|
||||
{'search': '01', 'limit': 2, 'purchaseorder': {}, 'salesorder': {}},
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
self.assertEqual(response.data['purchaseorder']['count'], 1)
|
||||
self.assertEqual(response.data['salesorder']['count'], 0)
|
||||
|
@ -71,6 +71,7 @@ class MiddlewareTests(InvenTreeTestCase):
|
||||
|
||||
def test_error_exceptions(self):
|
||||
"""Test that ignored errors are not logged."""
|
||||
self.assignRole('part.view')
|
||||
|
||||
def check(excpected_nbr=0):
|
||||
# Check that errors are empty
|
||||
|
@ -204,7 +204,8 @@ class UserMixin:
|
||||
ruleset.can_add = True
|
||||
|
||||
ruleset.save()
|
||||
break
|
||||
if not assign_all:
|
||||
break
|
||||
|
||||
|
||||
class PluginMixin:
|
||||
|
@ -160,7 +160,7 @@ class CompanyTest(InvenTreeAPITestCase):
|
||||
class ContactTest(InvenTreeAPITestCase):
|
||||
"""Tests for the Contact models."""
|
||||
|
||||
roles = []
|
||||
roles = ['purchase_order.view']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -266,7 +266,7 @@ class ContactTest(InvenTreeAPITestCase):
|
||||
class AddressTest(InvenTreeAPITestCase):
|
||||
"""Test cases for Address API endpoints."""
|
||||
|
||||
roles = []
|
||||
roles = ['purchase_order.view']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -2010,6 +2010,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
'supplier_part',
|
||||
'stock',
|
||||
]
|
||||
roles = ['return_order.view']
|
||||
|
||||
def test_options(self):
|
||||
"""Test the OPTIONS endpoint."""
|
||||
|
@ -512,7 +512,7 @@ class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||
Ensure that the required field details are provided!
|
||||
"""
|
||||
|
||||
roles = ['part.add']
|
||||
roles = ['part.add', 'part_category.view']
|
||||
|
||||
def test_part(self):
|
||||
"""Test the Part API OPTIONS."""
|
||||
@ -2149,7 +2149,7 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'stock', 'bom', 'company']
|
||||
|
||||
roles = ['part.add', 'part.change', 'part.delete']
|
||||
roles = ['part.add', 'part.change', 'part.delete', 'stock.view']
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the test case."""
|
||||
@ -2642,6 +2642,7 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
||||
|
||||
superuser = False
|
||||
is_staff = False
|
||||
roles = ['stocktake.view']
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'stock']
|
||||
|
||||
|
@ -162,7 +162,24 @@ class UserList(ListCreateAPI):
|
||||
filterset_fields = ['is_staff', 'is_active', 'is_superuser']
|
||||
|
||||
|
||||
class GroupDetail(RetrieveUpdateDestroyAPI):
|
||||
class GroupMixin:
|
||||
"""Mixin for Group API endpoints to add permissions filter."""
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer instance for this endpoint."""
|
||||
# Do we wish to include extra detail?
|
||||
try:
|
||||
params = self.request.query_params
|
||||
kwargs['permission_detail'] = InvenTree.helpers.str2bool(
|
||||
params.get('permission_detail', None)
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
|
||||
class GroupDetail(GroupMixin, RetrieveUpdateDestroyAPI):
|
||||
"""Detail endpoint for a particular auth group."""
|
||||
|
||||
queryset = Group.objects.all()
|
||||
@ -170,7 +187,7 @@ class GroupDetail(RetrieveUpdateDestroyAPI):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class GroupList(ListCreateAPI):
|
||||
class GroupList(GroupMixin, ListCreateAPI):
|
||||
"""List endpoint for all auth groups."""
|
||||
|
||||
queryset = Group.objects.all()
|
||||
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.12 on 2024-07-18 21:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0011_auto_20240523_1640"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="ruleset",
|
||||
name="can_view",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Permission to view items", verbose_name="View"
|
||||
),
|
||||
),
|
||||
]
|
@ -389,7 +389,7 @@ class RuleSet(models.Model):
|
||||
)
|
||||
|
||||
can_view = models.BooleanField(
|
||||
verbose_name=_('View'), default=True, help_text=_('Permission to view items')
|
||||
verbose_name=_('View'), default=False, help_text=_('Permission to view items')
|
||||
)
|
||||
|
||||
can_add = models.BooleanField(
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""DRF API serializers for the 'users' app."""
|
||||
|
||||
from django.contrib.auth.models import Group, Permission, User
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.db.models import Q
|
||||
|
||||
from rest_framework import serializers
|
||||
@ -31,7 +32,25 @@ class GroupSerializer(InvenTreeModelSerializer):
|
||||
"""Metaclass defines serializer fields."""
|
||||
|
||||
model = Group
|
||||
fields = ['pk', 'name']
|
||||
fields = ['pk', 'name', 'permissions']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra fields as required."""
|
||||
permission_detail = kwargs.pop('permission_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
try:
|
||||
if not permission_detail:
|
||||
self.fields.pop('permissions', None)
|
||||
except AppRegistryNotReady:
|
||||
pass
|
||||
|
||||
permissions = serializers.SerializerMethodField()
|
||||
|
||||
def get_permissions(self, group: Group):
|
||||
"""Return a list of permissions associated with the group."""
|
||||
return generate_permission_dict(group.permissions.all())
|
||||
|
||||
|
||||
class RoleSerializer(InvenTreeModelSerializer):
|
||||
@ -83,14 +102,19 @@ class RoleSerializer(InvenTreeModelSerializer):
|
||||
Q(user=user) | Q(group__user=user)
|
||||
).distinct()
|
||||
|
||||
perms = {}
|
||||
return generate_permission_dict(permissions)
|
||||
|
||||
for permission in permissions:
|
||||
perm, model = permission.codename.split('_')
|
||||
|
||||
if model not in perms:
|
||||
perms[model] = []
|
||||
def generate_permission_dict(permissions):
|
||||
"""Generate a dictionary of permissions for a given set of permissions."""
|
||||
perms = {}
|
||||
|
||||
perms[model].append(perm)
|
||||
for permission in permissions:
|
||||
perm, model = permission.codename.split('_')
|
||||
|
||||
return perms
|
||||
if model not in perms:
|
||||
perms[model] = []
|
||||
|
||||
perms[model].append(perm)
|
||||
|
||||
return perms
|
||||
|
@ -123,8 +123,8 @@ class RuleSetModelTest(TestCase):
|
||||
for model in models:
|
||||
permission_set.add(model)
|
||||
|
||||
# Every ruleset by default sets one permission, the "view" permission set
|
||||
self.assertEqual(group.permissions.count(), len(permission_set))
|
||||
# By default no permissions should be assigned
|
||||
self.assertEqual(group.permissions.count(), 0)
|
||||
|
||||
# Add some more rules
|
||||
for rule in rulesets:
|
||||
|
Reference in New Issue
Block a user